@gotcos/glasses-server 6.94.4 → 6.98.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +147 -0
- package/README.md +52 -0
- package/managed-runtime-contract.json +13 -0
- package/package.json +3 -1
- package/server/index.ts +19 -1
- package/server/lib/attached-provider-adapter.ts +35 -1
- package/server/lib/claude-bridge.ts +47 -0
- package/server/lib/codex-bridge.ts +11 -1
- package/server/lib/codex-rollout-watermark.ts +42 -0
- package/server/lib/cursor-bridge.ts +2 -1
- package/server/lib/dictation-clean-models.ts +2 -0
- package/server/lib/dictation-clean.ts +2 -2
- package/server/lib/dictation-polish-pipeline.ts +22 -2
- package/server/lib/dictation-prewarm.ts +16 -0
- package/server/lib/direct-bridge.ts +14 -2
- package/server/lib/direct-sse.ts +6 -1
- package/server/lib/display-bus.ts +1 -1
- package/server/lib/fork-thread.ts +32 -1
- package/server/lib/held-naming-batches.ts +36 -0
- package/server/lib/job-audit-caller.ts +20 -0
- package/server/lib/lens-gist-engines.ts +17 -5
- package/server/lib/live-cos-ask.ts +29 -6
- package/server/lib/live-cue-verdicts.ts +43 -8
- package/server/lib/live-cues-engine.ts +4 -1
- package/server/lib/live-cues-filter.ts +50 -0
- package/server/lib/live-cues-general-prompts.ts +137 -0
- package/server/lib/live-cues-general-topic.ts +245 -0
- package/server/lib/live-cues-general.ts +715 -0
- package/server/lib/live-cues-v2-data.ts +453 -0
- package/server/lib/live-cues-v2-grounded.ts +262 -0
- package/server/lib/live-cues-v2-judge-b.ts +59 -0
- package/server/lib/live-cues-v2-names.ts +49 -0
- package/server/lib/live-cues-v2-notes.ts +8 -4
- package/server/lib/live-cues-v2-open-items.ts +132 -0
- package/server/lib/live-cues-v2-pacing.ts +45 -0
- package/server/lib/live-cues-v2-prompts-kinds.ts +9 -4
- package/server/lib/live-cues-v2-settings.ts +89 -10
- package/server/lib/live-cues-v2.ts +570 -47
- package/server/lib/mac-recording-asr.ts +6 -3
- package/server/lib/mac-speaker-call-human.ts +371 -0
- package/server/lib/mac-speaker-call-interval-human.ts +499 -0
- package/server/lib/mac-speaker-call-interval-projection.ts +233 -0
- package/server/lib/mac-speaker-call-original-view.ts +328 -0
- package/server/lib/mac-speaker-call-producer-worker.ts +615 -0
- package/server/lib/mac-speaker-call-producer.ts +539 -0
- package/server/lib/mac-speaker-call-store.ts +264 -0
- package/server/lib/mac-speaker-call-view.ts +373 -0
- package/server/lib/mac-speaker-live-compute.ts +72 -0
- package/server/lib/mac-speaker-live-memory.ts +414 -0
- package/server/lib/mac-speaker-live-projection.ts +126 -0
- package/server/lib/mac-speaker-live-save.ts +90 -0
- package/server/lib/mac-speaker-live-store.ts +71 -0
- package/server/lib/mac-speaker-live-worker.ts +58 -0
- package/server/lib/mac-speaker-live.ts +175 -0
- package/server/lib/mac-speaker-profile-extractor.ts +15 -10
- package/server/lib/mac-speaker-shadow-repair.ts +307 -0
- package/server/lib/mac-voice-assignment.ts +44 -26
- package/server/lib/mac-voice-display.ts +195 -0
- package/server/lib/mac-voice-naming-batch.ts +1067 -0
- package/server/lib/mac-voice-suggestions.ts +548 -0
- package/server/lib/maintenance-lifecycle.ts +2 -0
- package/server/lib/media-store.ts +2 -1
- package/server/lib/meeting-ask-pasted.ts +135 -0
- package/server/lib/meeting-ask-review.ts +8 -1
- package/server/lib/meeting-ask-runner.ts +4 -1
- package/server/lib/meeting-ask-runtime.ts +189 -17
- package/server/lib/meeting-ask-store.ts +2 -0
- package/server/lib/meeting-ask-web.ts +284 -13
- package/server/lib/meeting-batch-progress.ts +51 -5
- package/server/lib/meeting-batch-transcribe.ts +35 -2
- package/server/lib/meeting-fact-check.ts +3 -0
- package/server/lib/meeting-finalization-jobs.ts +69 -2
- package/server/lib/meeting-public-topic.ts +6 -1
- package/server/lib/meeting-speaker-review.ts +182 -17
- package/server/lib/meeting-store.ts +6 -1
- package/server/lib/parakeet.ts +51 -5
- package/server/lib/pending-batch-retention.ts +58 -0
- package/server/lib/private-reference.ts +38 -0
- package/server/lib/provider-usage.ts +561 -0
- package/server/lib/public-https-url.ts +32 -0
- package/server/lib/query-job-runtime.ts +3 -0
- package/server/lib/rich-media-safety.ts +23 -2
- package/server/lib/speaker-shadow-decide.ts +503 -0
- package/server/lib/speaker-shadow-models.ts +150 -0
- package/server/lib/speaker-shadow-numeric.ts +143 -0
- package/server/lib/speaker-shadow-pair-source.ts +91 -0
- package/server/lib/speaker-shadow-paired.ts +268 -0
- package/server/lib/speaker-shadow-report.ts +113 -0
- package/server/lib/speaker-shadow-timeline.ts +210 -0
- package/server/lib/speaker-shadow-worker.ts +312 -0
- package/server/lib/speaker-shadow.ts +788 -0
- package/server/lib/token-audit.ts +36 -6
- package/server/lib/transcript-cleanup.ts +3 -2
- package/server/lib/voice-directory.ts +8 -4
- package/server/lib/web-lookup-ledger.ts +151 -0
- package/server/lib/work-review-runtime.ts +2 -0
- package/server/lib/work-search.ts +3 -1
- package/server/lib/xlsx-text.ts +277 -0
- package/server/routes/health.ts +5 -0
- package/server/routes/live-cues.ts +36 -14
- package/server/routes/meeting.ts +506 -49
- package/server/routes/speaker-shadow.ts +79 -0
- package/server/routes/transcribe-stream.ts +114 -33
- package/server/routes/voice.ts +3 -0
- package/server/scripts/live-cues-v3-mutation-gate.mjs +311 -0
- package/server/scripts/mac-voice-assignment-6930-mutation-gate.mjs +5 -5
- package/shared/live-cues-v2.ts +38 -5
- package/shared/media-attachment.ts +2 -0
- package/shared/meeting-ask.ts +11 -0
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
/** Read-only call-wide anonymous view. Packet hashes prove integrity, NOT native
|
|
2
|
+
* authentication. Only the trusted offline producer may grant qualified admission
|
|
3
|
+
* after independent QA. Full export replay belongs in that producer, never HTTP. */
|
|
4
|
+
import { createHash } from 'node:crypto'
|
|
5
|
+
import { closeSync, constants, fstatSync, openSync, readSync } from 'node:fs'
|
|
6
|
+
import { dataPath } from './data-dir.js'
|
|
7
|
+
import { validateMacSpeaker256State, type MacSpeaker256SnapshotInput, type MacSpeaker256State, type MacSpeaker256Group } from './mac-speaker-live-memory.js'
|
|
8
|
+
import { inspectMacSpeakerShadowCallSource } from './mac-speaker-shadow-repair.js'
|
|
9
|
+
import { readMacSpeakerMachineEvidence } from './mac-speaker-machine-evidence.js'
|
|
10
|
+
import { hasMacSpeakerProtectedRows, isMacSpeakerHumanProtected } from './mac-speaker-protection.js'
|
|
11
|
+
import { boundWords, renderMarkdown } from './mac-speaker-repair-render.js'
|
|
12
|
+
import { applyMacCallHumanAnnotation, macCallHumanPaths, readMacCallHumanFile } from './mac-speaker-call-human.js'
|
|
13
|
+
import { intervalSamples, nativeTurns } from './speaker-shadow-decide.js'
|
|
14
|
+
import { createMacSpeakerCallIntervalProjector, prepareMacSpeakerCallIntervalSupport, validateMacSpeakerCallIntervalSupport, type MacSpeakerCallIntervalMembership, type MacSpeakerCallIntervalSupport } from './mac-speaker-call-interval-projection.js'
|
|
15
|
+
import { applyMacIntervalHumanAnnotation, inspectMacIntervalHumanJournal, macIntervalHumanPaths, type MacIntervalHumanMetadata } from './mac-speaker-call-interval-human.js'
|
|
16
|
+
import { inspectOriginalViewTransaction } from './mac-speaker-call-original-view.js'
|
|
17
|
+
|
|
18
|
+
type Doc = Record<string, any>
|
|
19
|
+
const HASH = /^[a-f0-9]{64}$/
|
|
20
|
+
const VOICE = /^Voice (?:[1-9]|[1-9]\d)$/
|
|
21
|
+
const sha = (s: string) => createHash('sha256').update(s).digest('hex')
|
|
22
|
+
const digest = (v: unknown) => sha(JSON.stringify(v))
|
|
23
|
+
function refuse(s: string): never { throw new Error(`Mac call view: ${s}`) }
|
|
24
|
+
const MAX_PACKET = 16 * 1024 * 1024
|
|
25
|
+
// Exact-byte identities only; a changed packet/base/ledger never inherits trust.
|
|
26
|
+
const checkedPackets = new Set<string>()
|
|
27
|
+
const checkedBaseProofs = new Map<string, ReturnType<typeof readMacSpeakerMachineEvidence>['status']>()
|
|
28
|
+
function remember<K, V>(cache: Map<K, V>, key: K, value: V): void { if (cache.size >= 8) cache.delete(cache.keys().next().value!); cache.set(key, value) }
|
|
29
|
+
export interface MacSpeakerCallPacket {
|
|
30
|
+
version: 1 | 2; policy: 'fluid256-call-view-v1' | 'fluid256-call-interval-view-v1'; admission: 'off' | 'qualified'
|
|
31
|
+
projectionPolicy: 'strict' | 'anonymous-estimate' | 'source-bound-intervals'
|
|
32
|
+
captureId: string; generation: string; revision: number
|
|
33
|
+
baseSHA256: string; ledgerSHA256: string; sourceBindingSHA256: string
|
|
34
|
+
stateSHA256: string; modelSHA256: string; cliSHA256: string
|
|
35
|
+
rawBindings: MacSpeaker256State['rawBindings']; groups: MacSpeaker256Group[]
|
|
36
|
+
protectedRawChunkIndices: number[]
|
|
37
|
+
aliasProtectedRawChunkIndices: number[]
|
|
38
|
+
displayLabels: Record<string, string>
|
|
39
|
+
provenance: { nativeAuthenticated: false; timingBasis: 'native-exclusive-intersect-machine-production'; speakerPurityVerified: false; excludedProductionSegments?: ReturnType<typeof inspectMacSpeakerShadowCallSource>['excludedProductionSegments']; nativeSHA256?: string; timelinePCMSHA256?: string }
|
|
40
|
+
intervalSupport?: MacSpeakerCallIntervalSupport
|
|
41
|
+
packetSHA256: string
|
|
42
|
+
}
|
|
43
|
+
export interface MacSpeakerCallView {
|
|
44
|
+
doc: Doc
|
|
45
|
+
markdown?: string
|
|
46
|
+
metadata: { status: 'base' | 'applied'; reason: string; mode: 'strict' | 'anonymous-estimate' | 'alias-evaluation' | 'source-bound-intervals'; revision?: number; generation?: string; packetSHA256?: string; changedChunks: number; changedWords: number; groups: Array<{ identityId: string; label: string; memberVoices: string[]; intervals: MacSpeaker256Group['intervals'] }>; localization: 'estimated-ASR-with-native-support' | 'inherited-machine-estimate'; speakerPurityVerified: false; humanAnnotation?: { revision: number; annotationSHA256: string; members: number }; humanAnnotationInvalid?: true; intervalHumanAnnotation?: MacIntervalHumanMetadata; intervalHumanAnnotationInvalid?: true; intervalHumanRecoveryRequired?: true; intervalMembership?: MacSpeakerCallIntervalMembership; supportSHA256?: string; effectiveWordView?: { version: 1; anonymousGroups: Array<{ identityId: string; label: string }>; unresolvedLabel: 'Ext' } }
|
|
47
|
+
}
|
|
48
|
+
/** Bind every independently checked admission/source stamp to interval support.
|
|
49
|
+
* Admission is intentionally excluded: offline QA may grant an off packet without
|
|
50
|
+
* changing the underlying evidence. This integrity hash is not authentication. */
|
|
51
|
+
function intervalSourceBinding(p: Pick<MacSpeakerCallPacket, 'captureId' | 'generation' | 'revision' | 'baseSHA256' | 'ledgerSHA256' | 'sourceBindingSHA256' | 'stateSHA256' | 'modelSHA256' | 'cliSHA256' | 'rawBindings' | 'groups' | 'aliasProtectedRawChunkIndices' | 'displayLabels'>): string {
|
|
52
|
+
return digest([p.captureId, p.generation, p.revision, p.baseSHA256, p.ledgerSHA256, p.sourceBindingSHA256, p.stateSHA256, p.modelSHA256, p.cliSHA256, p.rawBindings, p.groups, p.aliasProtectedRawChunkIndices, p.displayLabels])
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Expensive pure preparation: call only off-thread/offline with full verified inputs. */
|
|
56
|
+
export function prepareMacSpeakerCallPacket(input: MacSpeaker256SnapshotInput, state: MacSpeaker256State, options: { admission?: 'off' | 'qualified'; projectionPolicy?: 'strict' | 'anonymous-estimate' | 'source-bound-intervals'; intervalMembership?: MacSpeakerCallIntervalMembership } = {}): MacSpeakerCallPacket {
|
|
57
|
+
validateMacSpeaker256State(state, input)
|
|
58
|
+
const { generation: _g, expectedStateSHA256: _e, ...source } = input
|
|
59
|
+
const inspected = inspectMacSpeakerShadowCallSource(source)
|
|
60
|
+
const intervalPolicy = options.projectionPolicy === 'source-bound-intervals'
|
|
61
|
+
if (options.intervalMembership !== undefined && !intervalPolicy) refuse('interval membership requires interval policy')
|
|
62
|
+
const displayLabels: Record<string, string> = {}
|
|
63
|
+
// A display hint is a retained, already estimated Voice label, not a new
|
|
64
|
+
// anonymous namespace. Identity claims live ONLY in the separate interval
|
|
65
|
+
// metadata, never on unsupported occurrences of this same display hint.
|
|
66
|
+
for (const g of state.groups) {
|
|
67
|
+
const identity = state.identities.find(i => i.identityId === g.identityId && !i.retired)
|
|
68
|
+
if (intervalPolicy) {
|
|
69
|
+
if (!identity || identity.ordinal < 1 || identity.ordinal > 999999) refuse('admitted anonymous identity display bound')
|
|
70
|
+
displayLabels[g.identityId] = `Anonymous group ${identity.ordinal}`
|
|
71
|
+
continue
|
|
72
|
+
}
|
|
73
|
+
const hint = identity?.birthIntervals[0]?.originalVoice
|
|
74
|
+
if (!hint || !VOICE.test(hint) || !g.memberVoices.includes(hint)) refuse('retained anonymous display hint')
|
|
75
|
+
displayLabels[g.identityId] = hint
|
|
76
|
+
}
|
|
77
|
+
const body = {
|
|
78
|
+
version: 1 as const, policy: 'fluid256-call-view-v1' as const, admission: options.admission ?? 'off', projectionPolicy: options.projectionPolicy ?? 'strict',
|
|
79
|
+
captureId: state.captureId, generation: state.generation, revision: state.revision,
|
|
80
|
+
baseSHA256: sha(input.currentJSON), ledgerSHA256: sha(input.ledgerJSONL), sourceBindingSHA256: state.sourceBindingSHA256,
|
|
81
|
+
stateSHA256: state.stateSHA256, modelSHA256: state.modelSHA256, cliSHA256: state.cliSHA256,
|
|
82
|
+
rawBindings: structuredClone(state.rawBindings), groups: structuredClone(state.groups),
|
|
83
|
+
protectedRawChunkIndices: [...inspected.protection].sort((a, b) => a - b), aliasProtectedRawChunkIndices: [...inspected.humanProtection].sort((a, b) => a - b), displayLabels,
|
|
84
|
+
provenance: { nativeAuthenticated: false as const, timingBasis: 'native-exclusive-intersect-machine-production' as const, speakerPurityVerified: false as const, excludedProductionSegments: structuredClone(inspected.excludedProductionSegments) },
|
|
85
|
+
}
|
|
86
|
+
let packet: MacSpeakerCallPacket = { ...body, packetSHA256: digest(body) }
|
|
87
|
+
if (intervalPolicy) {
|
|
88
|
+
const rawUsedSamples = state.rawBindings.map(r => inspected.placed.find(p => p.rawChunkIndex === r.rawChunkIndex)!.usedSampleCount)
|
|
89
|
+
const intervalSupport = prepareMacSpeakerCallIntervalSupport({ rawBindings: state.rawBindings, groups: state.groups, displayLabels, protectedRawChunkIndices: [...inspected.humanProtection] }, {
|
|
90
|
+
version: 1, membership: options.intervalMembership ?? 'accepted-only', sourceSHA256: intervalSourceBinding(body), nativeSHA256: state.provenance.nativeSHA256, timelinePCMSHA256: input.timeline.pcmSHA256, totalSamples: input.timeline.total, rawUsedSamples,
|
|
91
|
+
nativeIntervals: intervalSamples(inspected.turns, input.timeline.total).filter(([lo, hi]) => hi > lo).map(([startSample, endSample, nativeCluster]) => ({ startSample, endSample, nativeCluster })),
|
|
92
|
+
})
|
|
93
|
+
const intervalBody = { ...body, version: 2 as const, policy: 'fluid256-call-interval-view-v1' as const, projectionPolicy: 'source-bound-intervals' as const, intervalSupport, provenance: { ...body.provenance, nativeSHA256: intervalSupport.nativeSHA256, timelinePCMSHA256: intervalSupport.timelinePCMSHA256 } }
|
|
94
|
+
packet = { ...intervalBody, packetSHA256: digest(intervalBody) }
|
|
95
|
+
}
|
|
96
|
+
if (Buffer.byteLength(JSON.stringify(packet)) > MAX_PACKET) refuse('packet byte bound')
|
|
97
|
+
return packet
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function checkPacket(p: MacSpeakerCallPacket, baseJSON: string, ledger: string, doc: Doc): void {
|
|
101
|
+
const intervalPolicy = p?.version === 2 && p.policy === 'fluid256-call-interval-view-v1' && p.projectionPolicy === 'source-bound-intervals'
|
|
102
|
+
const legacyPolicy = p?.version === 1 && p.policy === 'fluid256-call-view-v1' && ['strict', 'anonymous-estimate'].includes(p.projectionPolicy) && p.intervalSupport === undefined
|
|
103
|
+
if (!p || !intervalPolicy && !legacyPolicy || !['off', 'qualified'].includes(p.admission) || p.captureId !== doc.sessionId || typeof p.generation !== 'string' || !p.generation || p.generation.length > 128 || !Number.isSafeInteger(p.revision) || p.revision < 1) refuse('packet identity')
|
|
104
|
+
const { packetSHA256, ...body } = p
|
|
105
|
+
if ([packetSHA256, p.baseSHA256, p.ledgerSHA256, p.sourceBindingSHA256, p.stateSHA256, p.modelSHA256, p.cliSHA256].some(h => !HASH.test(h)) || packetSHA256 !== digest(body) || p.baseSHA256 !== sha(baseJSON) || p.ledgerSHA256 !== sha(ledger)) refuse('packet/source/ledger drift')
|
|
106
|
+
const cacheKey = `${packetSHA256}:${p.baseSHA256}:${p.ledgerSHA256}`
|
|
107
|
+
if (checkedPackets.has(cacheKey)) return
|
|
108
|
+
if (p.provenance?.nativeAuthenticated !== false || p.provenance?.speakerPurityVerified !== false || p.provenance?.timingBasis !== 'native-exclusive-intersect-machine-production') refuse('provenance')
|
|
109
|
+
if (!Array.isArray(doc.chunkEntries) || !Array.isArray(doc.chunks) || !Array.isArray(p.rawBindings) || p.rawBindings.length !== doc.chunkEntries.length || p.rawBindings.length > 6000 || !Array.isArray(p.groups) || p.groups.length > 99 || !Array.isArray(p.protectedRawChunkIndices) || p.protectedRawChunkIndices.length > 6000) refuse('packet bounds')
|
|
110
|
+
for (const indices of [p.protectedRawChunkIndices, p.aliasProtectedRawChunkIndices]) {
|
|
111
|
+
if (!Array.isArray(indices) || indices.length > 6000) refuse('protected indices')
|
|
112
|
+
const protectedSet = new Set(indices)
|
|
113
|
+
if (protectedSet.size !== indices.length || [...protectedSet].some(i => !Number.isInteger(i) || i < 0 || i >= p.rawBindings.length)) refuse('protected indices')
|
|
114
|
+
}
|
|
115
|
+
for (const [i, r] of p.rawBindings.entries()) {
|
|
116
|
+
const e = doc.chunkEntries[i]
|
|
117
|
+
if (r.rawChunkIndex !== i || e.chunkIndex !== i || !HASH.test(r.audioSHA256) || !HASH.test(r.pcmSHA256) || !HASH.test(r.originalRowSHA256) || !Number.isSafeInteger(r.startSample) || !Number.isSafeInteger(r.endSample) || r.startSample < 0 || r.endSample <= r.startSample || r.endSample - r.startSample > 480000 || r.startSample !== Math.round(e.chunk.elapsed * 16) || e.chunk.audioSha256 && e.chunk.audioSha256 !== r.audioSHA256) refuse('raw binding')
|
|
118
|
+
}
|
|
119
|
+
const quarantined = p.provenance.excludedProductionSegments ?? []
|
|
120
|
+
if (!Array.isArray(quarantined) || quarantined.length > 40000) refuse('quarantine bound')
|
|
121
|
+
for (const q of quarantined) {
|
|
122
|
+
const raw = p.rawBindings[q.rawChunkIndex], row = doc.chunkEntries[q.rawChunkIndex]?.chunk?.segments?.[q.segmentIndex]
|
|
123
|
+
if (!raw || !row || !Number.isInteger(q.segmentIndex) || q.segmentIndex < 0 || q.reason !== 'generic-segment-crosses-source-end' || !HASH.test(q.rowSHA256) || q.rowSHA256 !== digest(row) || row.speaker !== 'Ext' || doc.chunkEntries[q.rawChunkIndex].chunk.speaker !== 'Ext' || q.startSec !== row.startSec || q.endSec !== row.endSec || q.sourceSamples !== raw.endSample - raw.startSample || q.overrunSamples <= 0 || q.overrunSamples > 16000 || Math.abs(q.overrunSamples - (q.endSec * 16000 - q.sourceSamples)) > 1e-6 || !p.protectedRawChunkIndices.includes(q.rawChunkIndex) || !p.aliasProtectedRawChunkIndices.includes(q.rawChunkIndex)) refuse('quarantine binding')
|
|
124
|
+
}
|
|
125
|
+
const ids = new Set<string>(), labels = new Set<string>(), voices = new Set<string>()
|
|
126
|
+
let intervals = 0
|
|
127
|
+
for (const g of p.groups) {
|
|
128
|
+
if (typeof g.identityId !== 'string' || g.identityId.length > 128 || ids.has(g.identityId) || !HASH.test(g.groupSHA256) || !Array.isArray(g.memberVoices) || g.memberVoices.length < 2 || g.memberVoices.length > 99 || !g.memberVoices.every(v => VOICE.test(v) && !voices.has(v)) || new Set(g.memberVoices).size !== g.memberVoices.length || !g.memberVoices.includes(g.keepVoiceHint)) refuse('group identity')
|
|
129
|
+
ids.add(g.identityId); g.memberVoices.forEach(v => voices.add(v))
|
|
130
|
+
const label = p.displayLabels?.[g.identityId]
|
|
131
|
+
if (labels.has(label) || (intervalPolicy ? !/^Anonymous group [1-9]\d{0,5}$/.test(label) : !VOICE.test(label) || !g.memberVoices.includes(label))) refuse('anonymous display label')
|
|
132
|
+
labels.add(label)
|
|
133
|
+
if (!Array.isArray(g.witnesses) || g.witnesses.length !== 2 || !Array.isArray(g.intervals) || (intervals += g.intervals.length) > 40000) refuse('group bounds')
|
|
134
|
+
const [a, b] = g.witnesses
|
|
135
|
+
for (const w of [a, b]) if (!HASH.test(w.evidenceId) || !Number.isSafeInteger(w.startSample) || !Number.isSafeInteger(w.endSample) || w.endSample <= w.startSample || !Array.isArray(w.rawChunkIndices) || !w.rawChunkIndices.length || w.rawChunkIndices.length > 6000 || !Array.isArray(w.pcmSHA256s) || w.pcmSHA256s.length !== w.rawChunkIndices.length || w.pcmSHA256s.some(h => !HASH.test(h))) refuse('witness')
|
|
136
|
+
if (a.startSample < b.endSample && b.startSample < a.endSample || a.rawChunkIndices.some(i => b.rawChunkIndices.includes(i)) || a.pcmSHA256s.some(h => b.pcmSHA256s.includes(h))) refuse('dependent witnesses')
|
|
137
|
+
for (const s of g.intervals) {
|
|
138
|
+
const r = p.rawBindings[s.rawChunkIndex]
|
|
139
|
+
if (!r || s.identityId !== g.identityId || s.timingBasis !== p.provenance.timingBasis || !g.memberVoices.includes(s.originalVoice) || s.audioSHA256 !== r.audioSHA256 || s.pcmSHA256 !== r.pcmSHA256 || !Number.isSafeInteger(s.startSample) || !Number.isSafeInteger(s.endSample) || s.startSample < r.startSample || s.endSample > r.endSample || s.endSample <= s.startSample || !Array.isArray(s.evidenceIds) || s.evidenceIds.length !== 2 || s.evidenceIds.some(h => !g.witnesses.some(w => w.evidenceId === h))) refuse('interval binding')
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
if (intervalPolicy) {
|
|
143
|
+
const support = p.intervalSupport
|
|
144
|
+
if (!support || support.sourceSHA256 !== intervalSourceBinding(p) || support.nativeSHA256 !== p.provenance.nativeSHA256 || support.timelinePCMSHA256 !== p.provenance.timelinePCMSHA256) refuse('interval source provenance')
|
|
145
|
+
for (const g of p.groups) if (g.groupSHA256 !== digest({ nativeCluster: g.nativeCluster, memberVoices: g.memberVoices, keepVoiceHint: g.keepVoiceHint, witnesses: g.witnesses, intervals: g.intervals.map(({ identityId: _id, ...i }) => i) })) refuse('admission group proof')
|
|
146
|
+
// New display labels cannot inherit an unrelated occurrence already in BASE.
|
|
147
|
+
const hasDisplayCollision = (row: unknown): boolean => {
|
|
148
|
+
if (Array.isArray(row)) return row.some(hasDisplayCollision)
|
|
149
|
+
if (!row || typeof row !== 'object') return false
|
|
150
|
+
const value = row as Doc
|
|
151
|
+
return typeof value.speaker === 'string' && labels.has(value.speaker) || Object.values(value).some(v => v && typeof v === 'object' && hasDisplayCollision(v))
|
|
152
|
+
}
|
|
153
|
+
if (hasDisplayCollision(doc)) refuse('anonymous display collision with BASE')
|
|
154
|
+
validateMacSpeakerCallIntervalSupport({ rawBindings: p.rawBindings, groups: p.groups, displayLabels: p.displayLabels, protectedRawChunkIndices: p.aliasProtectedRawChunkIndices }, support)
|
|
155
|
+
}
|
|
156
|
+
if (checkedPackets.size >= 8) checkedPackets.delete(checkedPackets.values().next().value!)
|
|
157
|
+
checkedPackets.add(cacheKey)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Shared structural/source validator for trusted offline publication callers.
|
|
161
|
+
* This proves integrity and exact support geometry, not native authenticity or
|
|
162
|
+
* independent speaker purity. Publication still needs its qualification receipt. */
|
|
163
|
+
export function validateMacSpeakerCallPacket(baseJSON: string, ledgerJSONL: string, packet: MacSpeakerCallPacket): void {
|
|
164
|
+
if (Buffer.byteLength(baseJSON) > 32 * 1024 * 1024 || Buffer.byteLength(ledgerJSONL) > 8 * 1024 * 1024 || Buffer.byteLength(JSON.stringify(packet)) > MAX_PACKET) refuse('byte bound')
|
|
165
|
+
const base = JSON.parse(baseJSON) as Doc
|
|
166
|
+
checkPacket(packet, baseJSON, ledgerJSONL, base)
|
|
167
|
+
if (readMacSpeakerMachineEvidence(base).status === 'invalid') refuse('base-machine-proof-invalid')
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Offline qualification must compare packet native geometry to the ACTUAL
|
|
171
|
+
* hash-pinned export, not a caller's self-resealed projection. HTTP readers use
|
|
172
|
+
* only the packet published by that trusted, source-verified caller. */
|
|
173
|
+
export function validateMacSpeakerCallIntervalNativeSource(packet: MacSpeakerCallPacket, nativeJSON: string): void {
|
|
174
|
+
const support = packet.intervalSupport
|
|
175
|
+
if (packet.version !== 2 || packet.projectionPolicy !== 'source-bound-intervals' || !support || Buffer.byteLength(nativeJSON) > 64 * 1024 * 1024 || sha(nativeJSON) !== support.nativeSHA256) refuse('native source binding')
|
|
176
|
+
const native = JSON.parse(nativeJSON)
|
|
177
|
+
if (!Array.isArray(native?.segments) || native.segments.length > 20000) refuse('native source schema')
|
|
178
|
+
for (const n of native.segments) if (!n || typeof n.speakerId !== 'string' || !n.speakerId || n.speakerId.length > 100 || !Number.isFinite(n.startTimeSeconds) || !Number.isFinite(n.endTimeSeconds) || n.startTimeSeconds < 0 || n.endTimeSeconds <= n.startTimeSeconds || n.endTimeSeconds > support.totalSamples / 16000 + 1) refuse('native source turn bound')
|
|
179
|
+
const { turns, dropped } = nativeTurns(native.segments, support.totalSamples / 16000)
|
|
180
|
+
if (dropped !== 0) refuse('native source dropped turn')
|
|
181
|
+
const intervals = intervalSamples(turns, support.totalSamples).filter(([lo, hi]) => hi > lo).map(([startSample, endSample, nativeCluster]) => ({ startSample, endSample, nativeCluster }))
|
|
182
|
+
if (digest(intervals) !== digest(support.nativeIntervals)) refuse('native source geometry drift')
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** Evaluation aliases intentionally test an aggregate hypothesis over residuals.
|
|
186
|
+
* They can NEVER be selected by the HTTP loader below. They assert no real name. */
|
|
187
|
+
export function resolveMacSpeakerCallView(baseJSON: string, ledgerJSONL: string, packet: MacSpeakerCallPacket | null, options: { mode?: 'strict' | 'anonymous-estimate' | 'alias-evaluation' | 'source-bound-intervals'; allowEvaluation?: boolean } = {}): MacSpeakerCallView {
|
|
188
|
+
const base = JSON.parse(baseJSON) as Doc
|
|
189
|
+
const mode = options.mode ?? packet?.projectionPolicy ?? 'strict'
|
|
190
|
+
const localization = mode === 'strict' || mode === 'source-bound-intervals' ? 'estimated-ASR-with-native-support' as const : 'inherited-machine-estimate' as const
|
|
191
|
+
const fallback = (reason: string): MacSpeakerCallView => ({ doc: base, metadata: { status: 'base', reason, mode, changedChunks: 0, changedWords: 0, groups: [], localization, speakerPurityVerified: false } })
|
|
192
|
+
try {
|
|
193
|
+
if (!packet) return fallback('packet-absent')
|
|
194
|
+
if (!['strict', 'anonymous-estimate', 'alias-evaluation', 'source-bound-intervals'].includes(mode)) refuse('view mode')
|
|
195
|
+
if (Buffer.byteLength(baseJSON) > 32 * 1024 * 1024 || Buffer.byteLength(ledgerJSONL) > 8 * 1024 * 1024 || Buffer.byteLength(JSON.stringify(packet)) > MAX_PACKET) refuse('byte bound')
|
|
196
|
+
checkPacket(packet, baseJSON, ledgerJSONL, base)
|
|
197
|
+
if (mode === 'alias-evaluation' ? options.allowEvaluation !== true : packet.admission !== 'qualified') return fallback(mode === 'alias-evaluation' ? 'evaluation-not-authorized' : 'packet-off')
|
|
198
|
+
if (mode === 'anonymous-estimate' && packet.projectionPolicy !== 'anonymous-estimate') refuse('packet policy mismatch')
|
|
199
|
+
if ((mode === 'source-bound-intervals') !== (packet.projectionPolicy === 'source-bound-intervals')) refuse('interval packet policy mismatch')
|
|
200
|
+
// Old qualified names are always validated against immutable BASE bytes.
|
|
201
|
+
let proofStatus = checkedBaseProofs.get(packet.baseSHA256)
|
|
202
|
+
if (!proofStatus) { proofStatus = readMacSpeakerMachineEvidence(base).status; remember(checkedBaseProofs, packet.baseSHA256, proofStatus) }
|
|
203
|
+
if (proofStatus === 'invalid') return fallback('base-machine-proof-invalid')
|
|
204
|
+
if (mode === 'source-bound-intervals') {
|
|
205
|
+
const projected = createMacSpeakerCallIntervalProjector({ rawBindings: packet.rawBindings, groups: packet.groups, displayLabels: packet.displayLabels, protectedRawChunkIndices: packet.aliasProtectedRawChunkIndices }, packet.intervalSupport!).project(base)
|
|
206
|
+
// This is an HQ word-backed closed-call policy. Without that surface the
|
|
207
|
+
// legacy reviewer cannot interpret its anonymous namespace safely.
|
|
208
|
+
if (!projected.usableHQWords) return fallback('no-usable-hq-words')
|
|
209
|
+
const anonymousGroups = packet.groups.map(g => ({ identityId: g.identityId, label: packet.displayLabels[g.identityId] }))
|
|
210
|
+
return { doc: projected.doc, metadata: { status: 'applied', reason: 'source-bound-anonymous-interval-view', mode, revision: packet.revision, generation: packet.generation, packetSHA256: packet.packetSHA256, changedChunks: projected.changedChunks, changedWords: projected.changedWords, groups: packet.groups.map(g => ({ identityId: g.identityId, label: packet.displayLabels[g.identityId], memberVoices: g.memberVoices, intervals: g.intervals })), localization, speakerPurityVerified: false, intervalMembership: packet.intervalSupport!.membership, supportSHA256: packet.intervalSupport!.supportSHA256, ...(projected.usableHQWords ? { effectiveWordView: { version: 1 as const, anonymousGroups, unresolvedLabel: 'Ext' as const } } : {}) } }
|
|
211
|
+
}
|
|
212
|
+
const doc = structuredClone(base), protectedRaw = new Set(mode === 'anonymous-estimate' ? packet.aliasProtectedRawChunkIndices : packet.protectedRawChunkIndices)
|
|
213
|
+
const existingVoiceHints = new Set(base.chunks.map((c: Doc) => c.speaker).filter((label: unknown) => typeof label === 'string' && VOICE.test(label)))
|
|
214
|
+
const byRaw = new Map<number, Array<{ g: MacSpeaker256Group; lo: number; hi: number; voice: string }>>()
|
|
215
|
+
const aliases = new Map(packet.groups.flatMap(g => g.memberVoices.map(v => [v, g] as const)))
|
|
216
|
+
// Nested/HQ estimates must agree with their immutable source turn. A
|
|
217
|
+
// different accepted Voice in a mixed turn is not authorized by the root's
|
|
218
|
+
// aggregate recurrence decision, even if that other Voice has its own group.
|
|
219
|
+
const sourceGroups = new Map<number, string>()
|
|
220
|
+
let basePosition = 0
|
|
221
|
+
for (const e of base.chunkEntries) {
|
|
222
|
+
if (typeof e.chunk.text !== 'string' || !e.chunk.text.trim()) continue
|
|
223
|
+
const root = base.chunks[basePosition++]
|
|
224
|
+
const rawGroup = aliases.get(e.chunk.speaker)?.identityId
|
|
225
|
+
if (rawGroup && aliases.get(root?.speaker)?.identityId === rawGroup) sourceGroups.set(e.chunkIndex, rawGroup)
|
|
226
|
+
}
|
|
227
|
+
const agreesWithSource = (raw: number, voice: unknown): boolean => typeof voice === 'string' && sourceGroups.has(raw) && aliases.get(voice)?.identityId === sourceGroups.get(raw)
|
|
228
|
+
for (const g of packet.groups) for (const s of g.intervals) { const list = byRaw.get(s.rawChunkIndex) ?? []; list.push({ g, lo: s.startSample, hi: s.endSample, voice: s.originalVoice }); byRaw.set(s.rawChunkIndex, list) }
|
|
229
|
+
for (const list of byRaw.values()) list.sort((a, b) => a.lo - b.lo || a.hi - b.hi)
|
|
230
|
+
const labelFor = (raw: number, lo: number, hi: number, voice: unknown): string | null => {
|
|
231
|
+
if (protectedRaw.has(raw) || typeof voice !== 'string' || !VOICE.test(voice) || hi <= lo) return null
|
|
232
|
+
if (mode !== 'strict') { const hint = mode === 'anonymous-estimate' ? packet.displayLabels[aliases.get(voice)?.identityId ?? ''] : aliases.get(voice)?.keepVoiceHint; return hint && existingVoiceHints.has(hint) ? hint : null }
|
|
233
|
+
let end = lo, group: MacSpeaker256Group | null = null
|
|
234
|
+
for (const s of byRaw.get(raw) ?? []) {
|
|
235
|
+
if (s.voice !== voice || s.hi <= lo || s.lo >= hi) continue
|
|
236
|
+
if (group && group.identityId !== s.g.identityId || s.lo > end) return null
|
|
237
|
+
group = s.g; end = Math.max(end, s.hi)
|
|
238
|
+
}
|
|
239
|
+
return end >= hi && group && existingVoiceHints.has(packet.displayLabels[group.identityId]) ? packet.displayLabels[group.identityId] : null
|
|
240
|
+
}
|
|
241
|
+
let changedChunks = 0, changedWords = 0, position = 0
|
|
242
|
+
for (const e of doc.chunkEntries) {
|
|
243
|
+
const raw = e.chunkIndex, c = e.chunk, r = packet.rawBindings[raw]
|
|
244
|
+
if (typeof c.text !== 'string' || !c.text.trim()) continue
|
|
245
|
+
const root = doc.chunks[position++]
|
|
246
|
+
if (mode === 'anonymous-estimate') {
|
|
247
|
+
if (!root || protectedRaw.has(raw) || hasMacSpeakerProtectedRows(c) || hasMacSpeakerProtectedRows(root)) continue
|
|
248
|
+
let changed = false
|
|
249
|
+
// This policy consolidates an ALREADY automatic Voice estimate. A
|
|
250
|
+
// structural Ext/Unknown field stays unchanged. Nested fields may use
|
|
251
|
+
// only the source turn's accepted group, never an independent group.
|
|
252
|
+
for (const row of [root, c]) {
|
|
253
|
+
for (const nested of [...(row.segments ?? []), ...(row.finalSegments ?? []), ...(row.words ?? [])]) {
|
|
254
|
+
if (!agreesWithSource(raw, nested.speaker)) continue
|
|
255
|
+
const to = labelFor(raw, r.startSample, r.endSample, nested.speaker)
|
|
256
|
+
if (to && to !== nested.speaker) { nested.speaker = to; changed = true }
|
|
257
|
+
}
|
|
258
|
+
const to = labelFor(raw, r.startSample, r.endSample, row.speaker)
|
|
259
|
+
if (to && to !== row.speaker) { row.speaker = to; changed = true }
|
|
260
|
+
}
|
|
261
|
+
if (changed) changedChunks++
|
|
262
|
+
continue
|
|
263
|
+
}
|
|
264
|
+
if (!root || protectedRaw.has(raw) || hasMacSpeakerProtectedRows(c) || hasMacSpeakerProtectedRows(root) || !VOICE.test(root.speaker) || root.speaker !== c.speaker) continue
|
|
265
|
+
const segs = Array.isArray(c.segments) && c.segments.length ? c.segments : [{ speaker: c.speaker, startSec: 0, endSec: (r.endSample - r.startSample) / 16000 }]
|
|
266
|
+
const labels = segs.map((s: Doc) => labelFor(raw, r.startSample + Math.round(s.startSec * 16000), r.startSample + Math.round(s.endSec * 16000), s.speaker))
|
|
267
|
+
const to = labels[0]
|
|
268
|
+
if (!to || labels.some((v: unknown) => v !== to)) continue
|
|
269
|
+
// A root claim cannot silently extend beyond another stored localization
|
|
270
|
+
// surface. Final segments and ASR words have their own clocks; every
|
|
271
|
+
// declared row must be covered, even when its speaker field is absent.
|
|
272
|
+
if (mode === 'strict' && [root, c].some(row => ['segments', 'finalSegments', 'words'].some(key => row[key] !== undefined && (!Array.isArray(row[key]) || row[key].some((nested: Doc) => {
|
|
273
|
+
const start = key === 'words' ? nested.start : nested.startSec, end = key === 'words' ? nested.end : nested.endSec
|
|
274
|
+
return !Number.isFinite(start) || !Number.isFinite(end) || start < 0 || end <= start || (nested.speaker !== undefined && nested.speaker !== root.speaker) || labelFor(raw, r.startSample + Math.round(start * 16000), r.startSample + Math.round(end * 16000), root.speaker) !== to
|
|
275
|
+
}))))) continue
|
|
276
|
+
const from = root.speaker
|
|
277
|
+
for (const row of [root, c]) { row.speaker = to; for (const key of ['segments', 'finalSegments', 'words']) for (const nested of row[key] ?? []) if (nested.speaker === from) nested.speaker = to }
|
|
278
|
+
if (to !== base.chunks[position - 1].speaker) changedChunks++
|
|
279
|
+
}
|
|
280
|
+
// HQ word seconds index concatenated RAW files, including silent files.
|
|
281
|
+
for (const segment of doc.batchSegments ?? []) {
|
|
282
|
+
if (!Number.isInteger(segment.startChunkIdx) || !Number.isInteger(segment.endChunkIdx) || segment.startChunkIdx < 0 || segment.endChunkIdx < segment.startChunkIdx || segment.endChunkIdx >= packet.rawBindings.length || !Array.isArray(segment.speakerWords)) refuse('batch geometry')
|
|
283
|
+
const clock: Array<{ raw: number; lo: number; hi: number }> = []; let offset = 0
|
|
284
|
+
for (let raw = segment.startChunkIdx; raw <= segment.endChunkIdx; raw++) { const r = packet.rawBindings[raw], n = r.endSample - r.startSample; clock.push({ raw, lo: offset, hi: offset + n }); offset += n }
|
|
285
|
+
for (const w of segment.speakerWords) {
|
|
286
|
+
if (isMacSpeakerHumanProtected(w) || !VOICE.test(w.speaker)) continue
|
|
287
|
+
const lo = Math.round(w.start * 16000), hi = Math.round(w.end * 16000)
|
|
288
|
+
if (!Number.isSafeInteger(lo) || !Number.isSafeInteger(hi) || lo < 0 || hi <= lo || hi > offset) continue
|
|
289
|
+
// Raw PCM intervals are contiguous and ordered even when ASR words are
|
|
290
|
+
// not. Avoid scanning every raw file for every word on the HTTP path.
|
|
291
|
+
let left = 0, right = clock.length
|
|
292
|
+
while (left < right) { const mid = (left + right) >>> 1; if (clock[mid].hi <= lo) left = mid + 1; else right = mid }
|
|
293
|
+
const parts: typeof clock = []
|
|
294
|
+
for (let i = left; i < clock.length && clock[i].lo < hi; i++) parts.push(clock[i])
|
|
295
|
+
const targets = parts.map(c => { const r = packet.rawBindings[c.raw]; return mode === 'anonymous-estimate' && !agreesWithSource(c.raw, w.speaker) ? null : labelFor(c.raw, r.startSample + Math.max(0, lo - c.lo), r.startSample + Math.min(c.hi - c.lo, hi - c.lo), w.speaker) })
|
|
296
|
+
if (targets.length && targets[0] && targets.every(t => t === targets[0])) { if (w.speaker !== targets[0]) changedWords++; w.speaker = targets[0] }
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
return { doc, metadata: { status: 'applied', reason: 'source-bound-anonymous-view', mode, revision: packet.revision, generation: packet.generation, packetSHA256: packet.packetSHA256, changedChunks, changedWords, groups: packet.groups.map(g => ({ identityId: g.identityId, label: mode === 'alias-evaluation' ? g.keepVoiceHint : packet.displayLabels[g.identityId], memberVoices: g.memberVoices, intervals: g.intervals })), localization, speakerPurityVerified: false } }
|
|
300
|
+
} catch (e) { return fallback(e instanceof Error ? e.message : 'invalid-packet') }
|
|
301
|
+
}
|
|
302
|
+
/** Legacy naming actions cannot represent projected multi-source/word scopes. */
|
|
303
|
+
export function isEffectiveMacSpeakerCallView(view: MacSpeakerCallView): boolean {
|
|
304
|
+
return !!view.metadata.intervalHumanAnnotation || view.metadata.status === 'applied' && (view.metadata.changedChunks > 0 || view.metadata.changedWords > 0 || view.metadata.mode === 'source-bound-intervals' && !!view.metadata.effectiveWordView)
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/** Exact prefix-only label edits; refuses transcript drift instead of guessing. */
|
|
308
|
+
export function renderMacSpeakerCallMarkdown(markdown: string, base: Doc, view: MacSpeakerCallView): string {
|
|
309
|
+
if (!(view.metadata.status === 'applied' && view.metadata.changedWords) && !view.metadata.intervalHumanAnnotation?.confirmedWords.length) return markdown
|
|
310
|
+
return renderMarkdown(markdown, boundWords(base, view.doc))
|
|
311
|
+
}
|
|
312
|
+
function boundedFile(path: string, maximum: number): string {
|
|
313
|
+
const fd = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK)
|
|
314
|
+
try {
|
|
315
|
+
const st = fstatSync(fd); if (!st.isFile() || st.size > maximum) refuse('file bound')
|
|
316
|
+
const buffer = Buffer.allocUnsafe(st.size + 1); let length = 0
|
|
317
|
+
while (length < buffer.length) { const n = readSync(fd, buffer, length, buffer.length - length, null); if (!n) break; length += n }
|
|
318
|
+
const after = fstatSync(fd)
|
|
319
|
+
if (length !== st.size || st.size !== after.size || st.mtimeMs !== after.mtimeMs || st.ctimeMs !== after.ctimeMs) refuse('file changed during read')
|
|
320
|
+
return buffer.subarray(0, length).toString('utf8')
|
|
321
|
+
} finally { closeSync(fd) }
|
|
322
|
+
}
|
|
323
|
+
export function readMacSpeakerCallView(sidecarPath: string, baseJSON: string, markdownPath?: string): MacSpeakerCallView {
|
|
324
|
+
let humanPresent = false, intervalPresent = false, recoveryRequired = false
|
|
325
|
+
let verifiedLegacyView: MacSpeakerCallView | undefined
|
|
326
|
+
try {
|
|
327
|
+
const doc = JSON.parse(baseJSON)
|
|
328
|
+
if (!/^[A-Za-z0-9:_-]{3,96}$/.test(doc.sessionId) || !sidecarPath.endsWith('.g2-chunks.json')) return resolveMacSpeakerCallView(baseJSON, '', null)
|
|
329
|
+
const mdPath = markdownPath ?? sidecarPath.replace(/\.g2-chunks\.json$/, '.md')
|
|
330
|
+
const files = { sidecarPath, meetingPath: mdPath }, paths = macCallHumanPaths(files, doc.sessionId), intervalPaths = macIntervalHumanPaths(files, doc.sessionId)
|
|
331
|
+
// Inspect the archival intent before looking at active interval artifacts:
|
|
332
|
+
// removal may have landed while the durable completion receipt has not.
|
|
333
|
+
const originalTransaction = inspectOriginalViewTransaction(files, doc.sessionId)
|
|
334
|
+
recoveryRequired = originalTransaction.status === 'prepared' || originalTransaction.status === 'recovery-invalid'
|
|
335
|
+
let packet: MacSpeakerCallPacket | null = null
|
|
336
|
+
if (!recoveryRequired) {
|
|
337
|
+
try { packet = JSON.parse(boundedFile(sidecarPath.replace(/\.g2-chunks\.json$/, '.speaker-call.json'), MAX_PACKET)) as MacSpeakerCallPacket } catch { /* Human decisions survive automatic withdrawal. */ }
|
|
338
|
+
}
|
|
339
|
+
humanPresent = true // An unreadable annotation must refuse competing writes.
|
|
340
|
+
const annotation = readMacCallHumanFile(paths.annotation, MAX_PACKET, true)
|
|
341
|
+
humanPresent = annotation !== null
|
|
342
|
+
let ledger = ''
|
|
343
|
+
try { ledger = boundedFile(dataPath('meeting-corrections', `${doc.sessionId.replace(/:/g, '_')}.jsonl`), 8 * 1024 * 1024) } catch (e) { if ((e as NodeJS.ErrnoException).code !== 'ENOENT') throw e }
|
|
344
|
+
// Independently validated older human decisions remain visible if only the
|
|
345
|
+
// newer interval transaction is invalid or needs recovery.
|
|
346
|
+
verifiedLegacyView = applyMacCallHumanAnnotation(baseJSON, ledger, annotation?.text ?? null, resolveMacSpeakerCallView(baseJSON, ledger, null))
|
|
347
|
+
try {
|
|
348
|
+
const legacyMarkdown = boundedFile(mdPath, MAX_PACKET)
|
|
349
|
+
verifiedLegacyView.markdown = renderMacSpeakerCallMarkdown(legacyMarkdown, doc, verifiedLegacyView)
|
|
350
|
+
} catch (e) { if ((e as NodeJS.ErrnoException).code !== 'ENOENT') throw e }
|
|
351
|
+
if (recoveryRequired) throw new Error('original view recovery required')
|
|
352
|
+
intervalPresent = true
|
|
353
|
+
const intervalAnnotation = readMacCallHumanFile(intervalPaths.annotation, MAX_PACKET, true)
|
|
354
|
+
intervalPresent = intervalAnnotation !== null
|
|
355
|
+
// A prepared or malformed journal is an explicit recovery state, even when
|
|
356
|
+
// its sidecar replacement landed. Never present that half-commit as saved.
|
|
357
|
+
const journal = inspectMacIntervalHumanJournal(files, doc.sessionId)
|
|
358
|
+
recoveryRequired = recoveryRequired || journal.status === 'prepared' || journal.status === 'recovery-invalid'
|
|
359
|
+
let view = applyMacCallHumanAnnotation(baseJSON, ledger, annotation?.text ?? null, resolveMacSpeakerCallView(baseJSON, ledger, packet))
|
|
360
|
+
let markdown: string
|
|
361
|
+
try { markdown = boundedFile(mdPath, MAX_PACKET) } catch (e) { if ((e as NodeJS.ErrnoException).code === 'ENOENT' && !intervalPresent && !recoveryRequired) return view; throw e }
|
|
362
|
+
if (recoveryRequired) throw new Error('interval annotation recovery required')
|
|
363
|
+
view = applyMacIntervalHumanAnnotation({ baseJSON, markdown, ledgerJSONL: ledger, legacyAnnotationJSON: annotation?.text ?? null, annotationJSON: intervalAnnotation?.text ?? null, packet, view })
|
|
364
|
+
view.markdown = renderMacSpeakerCallMarkdown(markdown, doc, view)
|
|
365
|
+
return view
|
|
366
|
+
} catch {
|
|
367
|
+
const view = verifiedLegacyView ?? resolveMacSpeakerCallView(baseJSON, '', null)
|
|
368
|
+
if (humanPresent || intervalPresent || recoveryRequired) { view.metadata.humanAnnotationInvalid = true; view.metadata.reason = 'human-annotation-invalid' }
|
|
369
|
+
if (intervalPresent) view.metadata.intervalHumanAnnotationInvalid = true
|
|
370
|
+
if (recoveryRequired) { view.metadata.intervalHumanRecoveryRequired = true; view.metadata.reason = 'interval-human-recovery-required' }
|
|
371
|
+
return view
|
|
372
|
+
}
|
|
373
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/** Bounded pure worker ownership and opaque, single-use publication authority. */
|
|
2
|
+
import { Worker } from 'node:worker_threads'
|
|
3
|
+
import { createRequire } from 'node:module'
|
|
4
|
+
import { pathToFileURL } from 'node:url'
|
|
5
|
+
import { randomUUID } from 'node:crypto'
|
|
6
|
+
import type { MacSpeakerLiveComputeInput, MacSpeakerLiveComputePrepared, MacSpeakerLiveComputeResult, MacSpeakerLivePreparedPublication } from './mac-speaker-live-worker.js'
|
|
7
|
+
import type { MacWindowEmbeddingReceipt } from './mac-speaker-windows.js'
|
|
8
|
+
export type { MacSpeakerLiveComputeInput, MacSpeakerLiveComputePrepared, MacSpeakerLiveComputeResult } from './mac-speaker-live-worker.js'
|
|
9
|
+
export interface MacSpeakerLivePublicationCapability {readonly kind:'mac-speaker-live-compute'}
|
|
10
|
+
interface Binding {captureId:string;generation:string;sourceBindingSHA256:string;fileBindingsSHA256:string;revision:number;voiceHighWater:number;mode:'mac-native-v1'}
|
|
11
|
+
const capabilities=new WeakMap<object,{binding:Binding;publication:MacSpeakerLivePreparedPublication}>()
|
|
12
|
+
/** A plain object, resealed state or copied capability never confers authority. */
|
|
13
|
+
export function takeMacSpeakerLivePublication(capability:unknown,expected:Binding):MacSpeakerLivePreparedPublication|null{
|
|
14
|
+
if(!capability||typeof capability!=='object')return null
|
|
15
|
+
const record=capabilities.get(capability);if(!record)return null
|
|
16
|
+
capabilities.delete(capability)
|
|
17
|
+
if(Object.keys(record.binding).some(key=>record.binding[key as keyof Binding]!==expected[key as keyof Binding]))return null
|
|
18
|
+
return record.publication
|
|
19
|
+
}
|
|
20
|
+
function createWorker():Worker{
|
|
21
|
+
const url=new URL('./mac-speaker-live-worker'+(import.meta.url.endsWith('.ts')?'.ts':'.js'),import.meta.url)
|
|
22
|
+
const registration=url.pathname.endsWith('.ts')?pathToFileURL(createRequire(import.meta.url).resolve('tsx/esm/api')).href:null
|
|
23
|
+
return new Worker(`(async()=>{if(${JSON.stringify(registration)}){const tsx=await import(${JSON.stringify(registration)});tsx.register()}await import(${JSON.stringify(url.href)})})().catch(e=>{throw e})`,{eval:true,resourceLimits:{maxOldGenerationSizeMb:192,stackSizeMb:4}})
|
|
24
|
+
}
|
|
25
|
+
let busy=false
|
|
26
|
+
export async function startMacSpeakerLiveCompute(input:MacSpeakerLiveComputeInput,options:{timeoutMs?:number;workerFactory?:()=>Worker}={}){
|
|
27
|
+
if(busy)throw Error('compute_lane_busy')
|
|
28
|
+
if(options.timeoutMs!==undefined&&(!Number.isFinite(options.timeoutMs)||options.timeoutMs<=0||options.timeoutMs>5000))throw Error('compute_deadline_bound')
|
|
29
|
+
if(input.profileJSONBytes.byteLength>32*1024*1024)throw Error('compute_input_bound')
|
|
30
|
+
const binding:Binding=Object.freeze({captureId:input.captureId,generation:input.generation,sourceBindingSHA256:input.sourceBindingSHA256,fileBindingsSHA256:input.fileBindingsSHA256,revision:input.revision,voiceHighWater:input.voiceHighWater,mode:'mac-native-v1'})
|
|
31
|
+
busy=true
|
|
32
|
+
let worker:Worker
|
|
33
|
+
try{worker=(options.workerFactory??createWorker)()}catch(error){busy=false;throw error}
|
|
34
|
+
const job=randomUUID(),budget=options.timeoutMs??5000;let used=0,next=0,closing:Promise<void>|undefined,waiting:{id:number;resolve:(v:any)=>void;reject:(e:Error)=>void;timer:ReturnType<typeof setTimeout>;deadline:number;started:number}|undefined
|
|
35
|
+
const close=():Promise<void>=>{
|
|
36
|
+
if(!closing){const prior=waiting;waiting=undefined;if(prior){clearTimeout(prior.timer);prior.reject(Error('compute_cancelled'))}
|
|
37
|
+
closing=worker.terminate().then(()=>{}).finally(()=>{busy=false})}
|
|
38
|
+
return closing
|
|
39
|
+
}
|
|
40
|
+
let terminalError:Error|undefined
|
|
41
|
+
const fail=(error:Error)=>{terminalError??=error;const prior=waiting;waiting=undefined;if(prior){clearTimeout(prior.timer);used+=Date.now()-prior.started}void close().then(()=>prior?.reject(error),()=>prior?.reject(error))}
|
|
42
|
+
worker.on('error',error=>fail(error));worker.on('exit',()=>{if(!closing)fail(Error('compute_worker_exited'))})
|
|
43
|
+
worker.on('message',(reply:{id:number;job:string;ok:boolean;value?:unknown;error?:string})=>{
|
|
44
|
+
const pending=waiting;if(!pending)return
|
|
45
|
+
if(reply.id!==pending.id||reply.job!==job)return fail(Error('compute_stale_reply'))
|
|
46
|
+
if(Date.now()>=pending.deadline)return fail(Error('compute_timeout'))
|
|
47
|
+
if(!reply.ok)return fail(Error(reply.error??'compute_failed'))
|
|
48
|
+
clearTimeout(pending.timer);waiting=undefined;used+=Date.now()-pending.started;pending.resolve(reply.value)
|
|
49
|
+
})
|
|
50
|
+
const request=(op:string,payload:Record<string,unknown>,transfer:readonly ArrayBuffer[]=[]):Promise<any>=>new Promise((resolve,reject)=>{
|
|
51
|
+
if(terminalError||closing||waiting){reject(terminalError??Error('compute_not_available'));return}
|
|
52
|
+
const remaining=budget-used;if(remaining<=0){void close().then(()=>reject(Error('compute_timeout')));return}
|
|
53
|
+
const id=++next,started=Date.now(),deadline=started+remaining
|
|
54
|
+
const timer=setTimeout(()=>fail(Error('compute_timeout')),remaining)
|
|
55
|
+
waiting={id,resolve,reject,timer,deadline,started}
|
|
56
|
+
try{worker.postMessage({id,job,op,...payload},transfer)}catch(error){fail(error instanceof Error?error:Error('compute_post_failed'))}
|
|
57
|
+
})
|
|
58
|
+
try{
|
|
59
|
+
const prepared=await request('prepare',{input},[input.profileJSONBytes.buffer as ArrayBuffer])as MacSpeakerLiveComputePrepared
|
|
60
|
+
return{prepared,close,async finish(receipt?:MacWindowEmbeddingReceipt){
|
|
61
|
+
try{
|
|
62
|
+
const result=await request('finish',{receipt})as MacSpeakerLiveComputeResult
|
|
63
|
+
// Keep the authority's payload private; public state/result copies may be
|
|
64
|
+
// inspected or mutated by callers without changing certified bytes.
|
|
65
|
+
const capability=Object.freeze({kind:'mac-speaker-live-compute' as const})
|
|
66
|
+
capabilities.set(capability,{binding,publication:result.publication})
|
|
67
|
+
const {publication,...publicResult}=result
|
|
68
|
+
return{...structuredClone(publicResult),capability}
|
|
69
|
+
}finally{await close()}
|
|
70
|
+
}}
|
|
71
|
+
}catch(error){await close();throw error}
|
|
72
|
+
}
|