@camstack/addon-post-analysis 1.2.208 → 1.2.210
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{dist-WxrHfFRz.mjs → dist-D2rFaMex.mjs} +185 -70
- package/dist/{dist-CoLA3NHh.js → dist-FHSaZkf_.js} +196 -69
- package/dist/embedding-encoder/index.js +1 -1
- package/dist/embedding-encoder/index.mjs +1 -1
- package/dist/pipeline-analytics/_stub.js +1 -1
- package/dist/pipeline-analytics/{_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-CHmvWGlL.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-4gDJk8ED.mjs} +3 -3
- package/dist/pipeline-analytics/{_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-BYQbvSbq.mjs → _virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-B2Kde85q.mjs} +1 -1
- package/dist/pipeline-analytics/{hostInit-B5t-MBOX.mjs → hostInit-Dgzccgln.mjs} +3 -3
- package/dist/pipeline-analytics/index.js +823 -33
- package/dist/pipeline-analytics/index.mjs +823 -33
- package/dist/pipeline-analytics/remoteEntry.js +1 -1
- package/package.json +1 -1
|
@@ -2,7 +2,7 @@ Object.defineProperties(exports, {
|
|
|
2
2
|
__esModule: { value: true },
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
|
-
const require_dist = require("../dist-
|
|
5
|
+
const require_dist = require("../dist-FHSaZkf_.js");
|
|
6
6
|
let node_fs = require("node:fs");
|
|
7
7
|
node_fs = require_dist.__toESM(node_fs, 1);
|
|
8
8
|
let node_path = require("node:path");
|
|
@@ -3452,7 +3452,7 @@ var SceneConfirmGate = class {
|
|
|
3452
3452
|
};
|
|
3453
3453
|
}
|
|
3454
3454
|
};
|
|
3455
|
-
function clamp$
|
|
3455
|
+
function clamp$2(v, lo, hi) {
|
|
3456
3456
|
return Math.max(lo, Math.min(v, hi));
|
|
3457
3457
|
}
|
|
3458
3458
|
/**
|
|
@@ -3467,10 +3467,10 @@ async function cropSnapshotRoi(encoded, roi) {
|
|
|
3467
3467
|
const frameW = meta.width ?? 0;
|
|
3468
3468
|
const frameH = meta.height ?? 0;
|
|
3469
3469
|
if (frameW <= 0 || frameH <= 0) throw new Error("snapshot has no decodable dimensions");
|
|
3470
|
-
const left = clamp$
|
|
3471
|
-
const top = clamp$
|
|
3472
|
-
const width = clamp$
|
|
3473
|
-
const height = clamp$
|
|
3470
|
+
const left = clamp$2(Math.round(roi.x * frameW), 0, frameW - 1);
|
|
3471
|
+
const top = clamp$2(Math.round(roi.y * frameH), 0, frameH - 1);
|
|
3472
|
+
const width = clamp$2(Math.round(roi.width * frameW), 1, frameW - left);
|
|
3473
|
+
const height = clamp$2(Math.round(roi.height * frameH), 1, frameH - top);
|
|
3474
3474
|
return {
|
|
3475
3475
|
crop: await (0, sharp.default)(encoded).extract({
|
|
3476
3476
|
left,
|
|
@@ -7520,7 +7520,8 @@ function cooldownKey(rule, subject) {
|
|
|
7520
7520
|
const classKey = keysPerClass(rule, subject) && first !== void 0 ? `:c:${first}` : "";
|
|
7521
7521
|
const scopeRef = subject.systemEvent !== void 0 ? systemEventCooldownScope(subject.systemEvent) : `d:${subject.deviceId}`;
|
|
7522
7522
|
const kindKey = subject.systemEvent !== void 0 ? `:k:${subject.systemEvent.kind}` : "";
|
|
7523
|
-
|
|
7523
|
+
const phaseKey = subject.packagePhase !== void 0 ? `:p:${subject.packagePhase}` : "";
|
|
7524
|
+
return rule.throttle.scope === "rule" ? `r:${rule.id}${kindKey}${phaseKey}${classKey}` : `r:${rule.id}:${scopeRef}${phaseKey}${classKey}`;
|
|
7524
7525
|
}
|
|
7525
7526
|
/** True when the rule fired within its cooldown window before `now`. */
|
|
7526
7527
|
function isCoolingDown(rule, lastFiredAt, now) {
|
|
@@ -9957,6 +9958,10 @@ var en_default = {
|
|
|
9957
9958
|
"one": "{{count}} other {{classNoun}}",
|
|
9958
9959
|
"other": "{{count}} other {{classNoun}}"
|
|
9959
9960
|
},
|
|
9961
|
+
"package.delivered.title": "Parcel delivered",
|
|
9962
|
+
"package.delivered.body": "Delivered at {{camera}}{{inZones}}",
|
|
9963
|
+
"package.picked-up.title": "Parcel collected",
|
|
9964
|
+
"package.picked-up.body": "Collected from {{camera}}{{inZones}}",
|
|
9960
9965
|
"occupancy.body": "{{scope}} {{op}} ({{count}}/{{capacity}})",
|
|
9961
9966
|
"occupancy.op.occupied": "occupied",
|
|
9962
9967
|
"occupancy.op.free": "free",
|
|
@@ -10208,6 +10213,10 @@ var it_default = {
|
|
|
10208
10213
|
"one": "{{count}} altro {{classNoun}}",
|
|
10209
10214
|
"other": "{{count}} altri {{classNoun}}"
|
|
10210
10215
|
},
|
|
10216
|
+
"package.delivered.title": "Pacco consegnato",
|
|
10217
|
+
"package.delivered.body": "Consegnato su {{camera}}{{inZones}}",
|
|
10218
|
+
"package.picked-up.title": "Pacco ritirato",
|
|
10219
|
+
"package.picked-up.body": "Ritirato da {{camera}}{{inZones}}",
|
|
10211
10220
|
"occupancy.body": "{{scope}} {{op}} ({{count}}/{{capacity}})",
|
|
10212
10221
|
"occupancy.op.occupied": "occupata",
|
|
10213
10222
|
"occupancy.op.free": "libera",
|
|
@@ -12050,6 +12059,7 @@ function bodyKeyFor(entry) {
|
|
|
12050
12059
|
const subject = entry.payload.subject;
|
|
12051
12060
|
if (subject.systemEvent !== void 0) return subject.systemEvent.bodyKey ?? "";
|
|
12052
12061
|
if (subject.occupancy !== void 0) return "occupancy.body";
|
|
12062
|
+
if (subject.packagePhase !== void 0) return `package.${subject.packagePhase}.body`;
|
|
12053
12063
|
if (entry.payload.enhancement === true) return "detection.body.enhanced";
|
|
12054
12064
|
return entry.recordKind === "track-end" ? "detection.body.trackEnd" : "detection.body";
|
|
12055
12065
|
}
|
|
@@ -12057,6 +12067,8 @@ function bodyKeyFor(entry) {
|
|
|
12057
12067
|
function titleKeyFor(entry) {
|
|
12058
12068
|
const systemEvent = entry.payload.subject.systemEvent;
|
|
12059
12069
|
if (systemEvent !== void 0) return systemEvent.titleKey ?? "";
|
|
12070
|
+
const packagePhase = entry.payload.subject.packagePhase;
|
|
12071
|
+
if (packagePhase !== void 0) return `package.${packagePhase}.title`;
|
|
12060
12072
|
return "detection.title";
|
|
12061
12073
|
}
|
|
12062
12074
|
/**
|
|
@@ -24856,6 +24868,7 @@ var NotificationCenter = class NotificationCenter {
|
|
|
24856
24868
|
zones: renderedZones,
|
|
24857
24869
|
timestamp: subject.timestamp,
|
|
24858
24870
|
...subject.occupancy !== void 0 ? { occupancy: frozenOccupancy(rule, subject.occupancy) } : {},
|
|
24871
|
+
...subject.packagePhase !== void 0 ? { packagePhase: subject.packagePhase } : {},
|
|
24859
24872
|
...subject.systemEvent !== void 0 ? { systemEvent: subject.systemEvent } : {}
|
|
24860
24873
|
}
|
|
24861
24874
|
};
|
|
@@ -28620,6 +28633,37 @@ var MediaStore = class {
|
|
|
28620
28633
|
return newKey;
|
|
28621
28634
|
}
|
|
28622
28635
|
/**
|
|
28636
|
+
* Exchange the bytes+instant held in the `firstFrame` and `lastFrame` slots
|
|
28637
|
+
* (close-time ordering repair, see `pipeline/first-last-frame-order.ts`).
|
|
28638
|
+
*
|
|
28639
|
+
* Both are single-instance kinds, so each `putReplacing` overwrites the slot
|
|
28640
|
+
* it names — the two rows end up holding each other's blob and timestamp.
|
|
28641
|
+
* Nothing is deleted and nothing is re-encoded: both blobs are already the
|
|
28642
|
+
* track's own stored pixels and the only thing wrong with them is which slot
|
|
28643
|
+
* they sit in. The `lastFrame` write lands first so the "ultimo" tile is
|
|
28644
|
+
* never momentarily the older view a viewer just refuted.
|
|
28645
|
+
*/
|
|
28646
|
+
async swapFirstAndLastFrame(input) {
|
|
28647
|
+
await this.gated(async () => {
|
|
28648
|
+
await this.putReplacingUnchecked({
|
|
28649
|
+
deviceId: input.deviceId,
|
|
28650
|
+
ownerKind: "track",
|
|
28651
|
+
ownerId: input.trackId,
|
|
28652
|
+
kind: "lastFrame",
|
|
28653
|
+
timestamp: input.firstFrame.timestamp,
|
|
28654
|
+
data: Buffer.from(input.firstFrame.base64, "base64")
|
|
28655
|
+
});
|
|
28656
|
+
await this.putReplacingUnchecked({
|
|
28657
|
+
deviceId: input.deviceId,
|
|
28658
|
+
ownerKind: "track",
|
|
28659
|
+
ownerId: input.trackId,
|
|
28660
|
+
kind: "firstFrame",
|
|
28661
|
+
timestamp: input.lastFrame.timestamp,
|
|
28662
|
+
data: Buffer.from(input.lastFrame.base64, "base64")
|
|
28663
|
+
});
|
|
28664
|
+
});
|
|
28665
|
+
}
|
|
28666
|
+
/**
|
|
28623
28667
|
* Fetch one media entry by its key (id). Returns null if the key is not
|
|
28624
28668
|
* found in the index or if the blob is missing from storage.
|
|
28625
28669
|
*/
|
|
@@ -30492,6 +30536,17 @@ function trackDurationMs(facts) {
|
|
|
30492
30536
|
function hasKind(media, kind) {
|
|
30493
30537
|
return media.some((m) => m.kind === kind);
|
|
30494
30538
|
}
|
|
30539
|
+
/** Newest instant stamped on a row of `kind`, or `null` when the track owns
|
|
30540
|
+
* none. Single-instance kinds hold at most one row; the max guards a legacy
|
|
30541
|
+
* duplicate, mirroring `decideFirstLastFrameOrder`. */
|
|
30542
|
+
function newestOfKind(media, kind) {
|
|
30543
|
+
let newest = null;
|
|
30544
|
+
for (const m of media) {
|
|
30545
|
+
if (m.kind !== kind) continue;
|
|
30546
|
+
if (newest === null || m.timestamp > newest) newest = m.timestamp;
|
|
30547
|
+
}
|
|
30548
|
+
return newest;
|
|
30549
|
+
}
|
|
30495
30550
|
function latestMediaAt(media) {
|
|
30496
30551
|
let latest = null;
|
|
30497
30552
|
for (const m of media) if (latest === null || m.timestamp > latest) latest = m.timestamp;
|
|
@@ -30539,6 +30594,12 @@ function analyseDebugTrack(inventory) {
|
|
|
30539
30594
|
const lag = facts.lastSeen - lastFrameAt;
|
|
30540
30595
|
if (lag > 2e3) out.push(finding("LASTFRAME_STALE", "warn", `lastFrame is ${String(lag)} ms older than the track's end.`));
|
|
30541
30596
|
}
|
|
30597
|
+
const firstFrameAt = newestOfKind(media, "firstFrame");
|
|
30598
|
+
if (firstFrameAt !== null) {
|
|
30599
|
+
const lastFrameAt = newestOfKind(media, "lastFrame");
|
|
30600
|
+
if (lastFrameAt !== null && firstFrameAt > lastFrameAt) out.push(finding("FIRSTFRAME_AFTER_LASTFRAME", "error", `firstFrame is ${String(firstFrameAt - lastFrameAt)} ms NEWER than lastFrame — the "primo" tile post-dates the "ultimo" one. The first view is captured at the CONFIRMED birth, which the confirmation gate can defer by up to its maxDeferralMs, while keyFrame/thumbnail/lastFrame are stamped at the earlier frame they were cut from.`));
|
|
30601
|
+
if (closed && firstFrameAt > facts.lastSeen) out.push(finding("FIRSTFRAME_AFTER_TRACK_END", "error", `firstFrame was captured ${String(firstFrameAt - facts.lastSeen)} ms AFTER the track's last observation — the tile shows a scene the subject had already left (a birth confirmed on a coasted frame).`));
|
|
30602
|
+
}
|
|
30542
30603
|
if (closed && durationMs >= 1e4 && media.length > 0) {
|
|
30543
30604
|
const latest = latestMediaAt(media);
|
|
30544
30605
|
if (latest !== null) {
|
|
@@ -32769,9 +32830,14 @@ var FaceStore = class {
|
|
|
32769
32830
|
* with that person named. High means "this looks like someone we can name",
|
|
32770
32831
|
* which is what a mis-enrolment actually looks like.
|
|
32771
32832
|
*
|
|
32772
|
-
* `suspicion`
|
|
32773
|
-
* one
|
|
32774
|
-
*
|
|
32833
|
+
* `suspicion` is the EXCESS — `bestForeign − selfMean` — because that is the
|
|
32834
|
+
* only one of the two that discriminates on real data. Measured on Roberta's
|
|
32835
|
+
* 106 samples, 2026-09-08: `selfMean` has a median of 0.188 and a p90 of 0.235,
|
|
32836
|
+
* so ANY absolute reading of "does not look like itself" fires on the whole
|
|
32837
|
+
* gallery. The first version of this module scored `(1 − selfMean) / 2` and
|
|
32838
|
+
* flagged 56 of 106 — a finding that fires on healthy samples means nothing.
|
|
32839
|
+
* `bestForeign` on the same gallery: median 0.283, p95 0.376, and one single
|
|
32840
|
+
* outlier at 0.612, which is the mis-enrolment.
|
|
32775
32841
|
*
|
|
32776
32842
|
* ## What it refuses to do
|
|
32777
32843
|
*
|
|
@@ -32786,8 +32852,20 @@ var FaceStore = class {
|
|
|
32786
32852
|
*
|
|
32787
32853
|
* Pure — the caller loads the vectors and performs the removal.
|
|
32788
32854
|
*/
|
|
32789
|
-
/**
|
|
32790
|
-
|
|
32855
|
+
/**
|
|
32856
|
+
* A row is FLAGGED when its best foreign match would auto-assign the face to
|
|
32857
|
+
* that other person — i.e. it reaches the recogniser's own
|
|
32858
|
+
* `similarityThreshold`.
|
|
32859
|
+
*
|
|
32860
|
+
* Tied to the live threshold rather than to a constant of its own, because
|
|
32861
|
+
* that is what the flag MEANS: this sample can steal a match. A second number
|
|
32862
|
+
* invented here would drift from the one the recogniser actually uses, and the
|
|
32863
|
+
* panel would accuse samples that cannot hurt anyone while staying quiet about
|
|
32864
|
+
* ones that can. On the live gallery (threshold 0.40) it flags 2 of 106.
|
|
32865
|
+
*/
|
|
32866
|
+
function flaggedCount(rows, autoAssignThreshold) {
|
|
32867
|
+
return rows.filter((row) => (row.bestForeign?.score ?? 0) >= autoAssignThreshold).length;
|
|
32868
|
+
}
|
|
32791
32869
|
/**
|
|
32792
32870
|
* How far a foreign match has to stand OUT to count as looking like someone
|
|
32793
32871
|
* else: `bestForeign - selfMean`. A sample that scores 0.6 against a stranger
|
|
@@ -32842,8 +32920,7 @@ function auditIdentitySamples(input) {
|
|
|
32842
32920
|
};
|
|
32843
32921
|
const selfMean = meanCosine(sample, comparable);
|
|
32844
32922
|
const foreign = bestForeignMatch(sample, input.others, input.modelId);
|
|
32845
|
-
const
|
|
32846
|
-
const suspicion = selfMean === null ? 0 : (1 - selfMean) / 2 + excess;
|
|
32923
|
+
const suspicion = foreignExcess(selfMean, foreign);
|
|
32847
32924
|
return {
|
|
32848
32925
|
sampleId: sample.sampleId,
|
|
32849
32926
|
enrolledAt: sample.enrolledAt,
|
|
@@ -32854,12 +32931,15 @@ function auditIdentitySamples(input) {
|
|
|
32854
32931
|
suspicion
|
|
32855
32932
|
};
|
|
32856
32933
|
});
|
|
32857
|
-
rows.sort((a, b) =>
|
|
32934
|
+
rows.sort((a, b) => {
|
|
32935
|
+
if (b.suspicion !== a.suspicion) return b.suspicion - a.suspicion;
|
|
32936
|
+
return (a.selfMean ?? Number.POSITIVE_INFINITY) - (b.selfMean ?? Number.POSITIVE_INFINITY);
|
|
32937
|
+
});
|
|
32858
32938
|
return {
|
|
32859
32939
|
identityId: input.identityId,
|
|
32860
32940
|
totalSamples: input.samples.length,
|
|
32861
32941
|
comparableSamples: comparable.length,
|
|
32862
|
-
flagged: rows
|
|
32942
|
+
flagged: flaggedCount(rows, input.autoAssignThreshold),
|
|
32863
32943
|
rows
|
|
32864
32944
|
};
|
|
32865
32945
|
}
|
|
@@ -32916,6 +32996,7 @@ var FaceGalleryProvider = class {
|
|
|
32916
32996
|
logger;
|
|
32917
32997
|
refreshGallery;
|
|
32918
32998
|
resolveClusterModelId;
|
|
32999
|
+
resolveAutoAssignThreshold;
|
|
32919
33000
|
eventMediaBaseUrl;
|
|
32920
33001
|
eventMediaPathPrefix;
|
|
32921
33002
|
emitFaceGalleryChanged;
|
|
@@ -32928,6 +33009,7 @@ var FaceGalleryProvider = class {
|
|
|
32928
33009
|
this.logger = deps.logger;
|
|
32929
33010
|
this.refreshGallery = deps.refreshGallery;
|
|
32930
33011
|
this.resolveClusterModelId = deps.resolveClusterModelId;
|
|
33012
|
+
this.resolveAutoAssignThreshold = deps.resolveAutoAssignThreshold;
|
|
32931
33013
|
this.eventMediaBaseUrl = deps.eventMediaBaseUrl;
|
|
32932
33014
|
this.eventMediaPathPrefix = deps.eventMediaPathPrefix;
|
|
32933
33015
|
this.emitFaceGalleryChanged = deps.emitFaceGalleryChanged;
|
|
@@ -33019,6 +33101,7 @@ var FaceGalleryProvider = class {
|
|
|
33019
33101
|
async auditIdentitySamples(input) {
|
|
33020
33102
|
const modelId = await this.resolveClusterModelId();
|
|
33021
33103
|
if (modelId === null) throw new Error("auditIdentitySamples: the cluster face-embedding model could not be resolved");
|
|
33104
|
+
const autoAssignThreshold = await this.resolveAutoAssignThreshold();
|
|
33022
33105
|
const identities = await this.identityStore.listIdentities();
|
|
33023
33106
|
const target = identities.find((i) => i.id === input.identityId);
|
|
33024
33107
|
if (target === void 0) throw new Error(`auditIdentitySamples: no identity ${input.identityId}`);
|
|
@@ -33044,7 +33127,8 @@ var FaceGalleryProvider = class {
|
|
|
33044
33127
|
name: identity.name,
|
|
33045
33128
|
samples: inputsFor(identity.id)
|
|
33046
33129
|
})),
|
|
33047
|
-
modelId
|
|
33130
|
+
modelId,
|
|
33131
|
+
autoAssignThreshold
|
|
33048
33132
|
});
|
|
33049
33133
|
const meta = new Map((samplesByIdentity.get(input.identityId) ?? []).map((sample) => [sample.id, sample]));
|
|
33050
33134
|
return {
|
|
@@ -33054,7 +33138,7 @@ var FaceGalleryProvider = class {
|
|
|
33054
33138
|
totalSamples: report.totalSamples,
|
|
33055
33139
|
comparableSamples: report.comparableSamples,
|
|
33056
33140
|
flagged: report.flagged,
|
|
33057
|
-
flagThreshold:
|
|
33141
|
+
flagThreshold: autoAssignThreshold,
|
|
33058
33142
|
rows: report.rows.map((row) => {
|
|
33059
33143
|
const sample = meta.get(row.sampleId);
|
|
33060
33144
|
return {
|
|
@@ -34115,6 +34199,29 @@ function firstFrameRetryBbox(current, birth) {
|
|
|
34115
34199
|
return { ...current };
|
|
34116
34200
|
}
|
|
34117
34201
|
//#endregion
|
|
34202
|
+
//#region src/pipeline-analytics/pipeline/birth-first-frame-gate.ts
|
|
34203
|
+
/**
|
|
34204
|
+
* Decide whether the confirmed-birth frame may be dispatched as this track's
|
|
34205
|
+
* `firstFrame` capture. Pure; the caller logs the refusal and keeps the
|
|
34206
|
+
* `firstFramePending` mark so a later frame retries.
|
|
34207
|
+
*
|
|
34208
|
+
* `coasted-frame` is reported ahead of `no-frame-handle` when both hold: a
|
|
34209
|
+
* handle-less coasted frame would have been refused for BEING coasted whatever
|
|
34210
|
+
* the decode plane did, and naming the decode is what would send the next
|
|
34211
|
+
* investigation at the wrong subsystem.
|
|
34212
|
+
*/
|
|
34213
|
+
function decideBirthFirstFrameCapture(input) {
|
|
34214
|
+
if (input.matchedThisFrame === false) return {
|
|
34215
|
+
capture: false,
|
|
34216
|
+
refusedBecause: "coasted-frame"
|
|
34217
|
+
};
|
|
34218
|
+
if (!input.hasFrameHandle) return {
|
|
34219
|
+
capture: false,
|
|
34220
|
+
refusedBecause: "no-frame-handle"
|
|
34221
|
+
};
|
|
34222
|
+
return { capture: true };
|
|
34223
|
+
}
|
|
34224
|
+
//#endregion
|
|
34118
34225
|
//#region src/pipeline-analytics/pipeline/events/event-filter.ts
|
|
34119
34226
|
var DEFAULT_EVENT_EMITTER_CONFIG = {
|
|
34120
34227
|
minTrackAge: 3,
|
|
@@ -34723,7 +34830,7 @@ var DEFAULT_TRACKER_CONFIG = {
|
|
|
34723
34830
|
var PERSON_CLASS = "person";
|
|
34724
34831
|
var ANIMAL_CLASS = "animal";
|
|
34725
34832
|
var MAX_PATH_LENGTH = 300;
|
|
34726
|
-
function clamp(value, min, max) {
|
|
34833
|
+
function clamp$1(value, min, max) {
|
|
34727
34834
|
return Math.max(min, Math.min(max, value));
|
|
34728
34835
|
}
|
|
34729
34836
|
function iou$3(a, b) {
|
|
@@ -35163,8 +35270,8 @@ var SortTracker = class SortTracker {
|
|
|
35163
35270
|
const perFrameX = t.velocity.dx * t.emitIntervalMs;
|
|
35164
35271
|
const perFrameY = t.velocity.dy * t.emitIntervalMs;
|
|
35165
35272
|
return {
|
|
35166
|
-
x: t.bbox.x + clamp(perFrameX * f, -t.bbox.w, t.bbox.w),
|
|
35167
|
-
y: t.bbox.y + clamp(perFrameY * f, -t.bbox.h, t.bbox.h),
|
|
35273
|
+
x: t.bbox.x + clamp$1(perFrameX * f, -t.bbox.w, t.bbox.w),
|
|
35274
|
+
y: t.bbox.y + clamp$1(perFrameY * f, -t.bbox.h, t.bbox.h),
|
|
35168
35275
|
w: t.bbox.w,
|
|
35169
35276
|
h: t.bbox.h
|
|
35170
35277
|
};
|
|
@@ -35173,8 +35280,8 @@ var SortTracker = class SortTracker {
|
|
|
35173
35280
|
const allowX = driftAllowancePx(t.bbox.w, elapsedMs);
|
|
35174
35281
|
const allowY = driftAllowancePx(t.bbox.h, elapsedMs);
|
|
35175
35282
|
return {
|
|
35176
|
-
x: t.bbox.x + clamp(t.velocity.dx * elapsedMs, -allowX, allowX),
|
|
35177
|
-
y: t.bbox.y + clamp(t.velocity.dy * elapsedMs, -allowY, allowY),
|
|
35283
|
+
x: t.bbox.x + clamp$1(t.velocity.dx * elapsedMs, -allowX, allowX),
|
|
35284
|
+
y: t.bbox.y + clamp$1(t.velocity.dy * elapsedMs, -allowY, allowY),
|
|
35178
35285
|
w: t.bbox.w,
|
|
35179
35286
|
h: t.bbox.h
|
|
35180
35287
|
};
|
|
@@ -37251,6 +37358,44 @@ function decideLastFramePromotion(media) {
|
|
|
37251
37358
|
};
|
|
37252
37359
|
}
|
|
37253
37360
|
//#endregion
|
|
37361
|
+
//#region src/pipeline-analytics/pipeline/first-last-frame-order.ts
|
|
37362
|
+
/**
|
|
37363
|
+
* Decide whether a closing track's `firstFrame` and `lastFrame` slots hold each
|
|
37364
|
+
* other's bytes. Pure — see the module header for the contract.
|
|
37365
|
+
*
|
|
37366
|
+
* Swap when BOTH slots are filled and the `firstFrame` is STRICTLY newer than
|
|
37367
|
+
* the `lastFrame`. Equal timestamps are not an inversion.
|
|
37368
|
+
*/
|
|
37369
|
+
function decideFirstLastFrameOrder(media) {
|
|
37370
|
+
let firstFrame;
|
|
37371
|
+
let lastFrame;
|
|
37372
|
+
for (const m of media) if (m.kind === "firstFrame") {
|
|
37373
|
+
if (firstFrame === void 0 || m.timestamp > firstFrame.timestamp) firstFrame = m;
|
|
37374
|
+
} else if (m.kind === "lastFrame") {
|
|
37375
|
+
if (lastFrame === void 0 || m.timestamp > lastFrame.timestamp) lastFrame = m;
|
|
37376
|
+
}
|
|
37377
|
+
if (firstFrame === void 0) return {
|
|
37378
|
+
swap: false,
|
|
37379
|
+
skippedBecause: "no-first-frame"
|
|
37380
|
+
};
|
|
37381
|
+
if (lastFrame === void 0) return {
|
|
37382
|
+
swap: false,
|
|
37383
|
+
skippedBecause: "no-last-frame"
|
|
37384
|
+
};
|
|
37385
|
+
if (firstFrame.timestamp <= lastFrame.timestamp) return {
|
|
37386
|
+
swap: false,
|
|
37387
|
+
skippedBecause: "ordered"
|
|
37388
|
+
};
|
|
37389
|
+
return {
|
|
37390
|
+
swap: true,
|
|
37391
|
+
firstFrameKey: firstFrame.key,
|
|
37392
|
+
lastFrameKey: lastFrame.key,
|
|
37393
|
+
firstFrameTimestamp: firstFrame.timestamp,
|
|
37394
|
+
lastFrameTimestamp: lastFrame.timestamp,
|
|
37395
|
+
inversionMs: firstFrame.timestamp - lastFrame.timestamp
|
|
37396
|
+
};
|
|
37397
|
+
}
|
|
37398
|
+
//#endregion
|
|
37254
37399
|
//#region src/pipeline-analytics/pipeline/static-track-gate.ts
|
|
37255
37400
|
/**
|
|
37256
37401
|
* Net displacement + path span for a track's centroid path, normalized to
|
|
@@ -37393,6 +37538,7 @@ var TrackCloser = class {
|
|
|
37393
37538
|
tile = "raster";
|
|
37394
37539
|
}
|
|
37395
37540
|
await this.maybePromoteLastFrame(t, ownedMedia);
|
|
37541
|
+
await this.repairFirstLastFrameOrder(t);
|
|
37396
37542
|
const hasRasterFallback = closure?.rasterFallback !== void 0;
|
|
37397
37543
|
let deriveMiss;
|
|
37398
37544
|
if (!thumbnailLanded) {
|
|
@@ -37736,6 +37882,56 @@ var TrackCloser = class {
|
|
|
37736
37882
|
}
|
|
37737
37883
|
}
|
|
37738
37884
|
/**
|
|
37885
|
+
* A track's "primo" tile is never newer than its "ultimo" tile
|
|
37886
|
+
* (`first-last-frame-order.ts`). Filed by hand three times on device 3829,
|
|
37887
|
+
* 2026-09-08 — `d035f251`, `448678bb`, `7628b19d`.
|
|
37888
|
+
*
|
|
37889
|
+
* The media list is RE-READ rather than reusing the caller's `ownedMedia`:
|
|
37890
|
+
* `maybePromoteLastFrame` has just run and is precisely what may have put the
|
|
37891
|
+
* older bytes in the `lastFrame` slot, so the caller's copy predates the pair
|
|
37892
|
+
* being ordered here. Best-effort — a failure never blocks the close.
|
|
37893
|
+
*/
|
|
37894
|
+
async repairFirstLastFrameOrder(t) {
|
|
37895
|
+
const store = this.deps.mediaStore();
|
|
37896
|
+
if (!store) return;
|
|
37897
|
+
const media = await store.listByOwner("track", t.trackId);
|
|
37898
|
+
const decision = decideFirstLastFrameOrder(media);
|
|
37899
|
+
if (!decision.swap) return;
|
|
37900
|
+
const firstFrame = media.find((m) => m.key === decision.firstFrameKey);
|
|
37901
|
+
const lastFrame = media.find((m) => m.key === decision.lastFrameKey);
|
|
37902
|
+
if (!firstFrame || !lastFrame) return;
|
|
37903
|
+
try {
|
|
37904
|
+
await store.swapFirstAndLastFrame({
|
|
37905
|
+
deviceId: t.deviceId,
|
|
37906
|
+
trackId: t.trackId,
|
|
37907
|
+
firstFrame,
|
|
37908
|
+
lastFrame
|
|
37909
|
+
});
|
|
37910
|
+
this.deps.logger.warn("firstFrame/lastFrame were out of order — slots swapped", {
|
|
37911
|
+
tags: { deviceId: t.deviceId },
|
|
37912
|
+
meta: {
|
|
37913
|
+
trackId: t.trackId,
|
|
37914
|
+
className: t.className,
|
|
37915
|
+
inversionMs: decision.inversionMs,
|
|
37916
|
+
firstFrameAt: decision.firstFrameTimestamp,
|
|
37917
|
+
lastFrameAt: decision.lastFrameTimestamp,
|
|
37918
|
+
/** How far the late capture ran past the track itself. Positive means
|
|
37919
|
+
* it was cut from a frame the subject was no longer observed in. */
|
|
37920
|
+
pastTrackEndMs: decision.firstFrameTimestamp - t.lastSeen,
|
|
37921
|
+
durationMs: t.lastSeen - t.firstSeen
|
|
37922
|
+
}
|
|
37923
|
+
});
|
|
37924
|
+
} catch (err) {
|
|
37925
|
+
this.deps.logger.warn("firstFrame/lastFrame order repair failed", {
|
|
37926
|
+
tags: { deviceId: t.deviceId },
|
|
37927
|
+
meta: {
|
|
37928
|
+
trackId: t.trackId,
|
|
37929
|
+
error: String(err)
|
|
37930
|
+
}
|
|
37931
|
+
});
|
|
37932
|
+
}
|
|
37933
|
+
}
|
|
37934
|
+
/**
|
|
37739
37935
|
* Key-event importance: score the just-expired track and persist. Peak
|
|
37740
37936
|
* confidence comes from the shared best-detection tracker; peak bbox area
|
|
37741
37937
|
* + the winning object-event id from one indexed queryObject(trackId) —
|
|
@@ -41551,7 +41747,10 @@ var TrackStore = class {
|
|
|
41551
41747
|
retrainStatus: active.retrainStatus ?? "none",
|
|
41552
41748
|
debug: active.debug === true,
|
|
41553
41749
|
debugNote: active.debugNote ?? "",
|
|
41554
|
-
favourited: active.favourited === true
|
|
41750
|
+
favourited: active.favourited === true,
|
|
41751
|
+
startedAt: active.firstSeen,
|
|
41752
|
+
className: active.className,
|
|
41753
|
+
...displayLabel(active) !== void 0 ? { label: displayLabel(active) } : {}
|
|
41555
41754
|
};
|
|
41556
41755
|
const persisted = await this.getPersistedByTrackId(trackId);
|
|
41557
41756
|
if (!persisted) return null;
|
|
@@ -41559,7 +41758,10 @@ var TrackStore = class {
|
|
|
41559
41758
|
retrainStatus: persisted.retrainStatus ?? "none",
|
|
41560
41759
|
debug: persisted.debug === true,
|
|
41561
41760
|
debugNote: persisted.debugNote ?? "",
|
|
41562
|
-
favourited: persisted.favourited === true
|
|
41761
|
+
favourited: persisted.favourited === true,
|
|
41762
|
+
startedAt: persisted.firstSeen,
|
|
41763
|
+
className: persisted.className,
|
|
41764
|
+
...displayLabel(persisted) !== void 0 ? { label: displayLabel(persisted) } : {}
|
|
41563
41765
|
};
|
|
41564
41766
|
}
|
|
41565
41767
|
/**
|
|
@@ -54987,7 +55189,7 @@ var PackageDropDetector = class {
|
|
|
54987
55189
|
async onStationaryChange(change) {
|
|
54988
55190
|
try {
|
|
54989
55191
|
if (change.phase === "appeared") await this.onAppeared(change.entry, change.timestamp);
|
|
54990
|
-
else await this.onDeparted(change
|
|
55192
|
+
else await this.onDeparted(change);
|
|
54991
55193
|
} catch (err) {
|
|
54992
55194
|
this.deps.onError?.("onStationaryChange", err);
|
|
54993
55195
|
this.deps.logger.warn("package-drop detector failed on change", {
|
|
@@ -55165,7 +55367,9 @@ var PackageDropDetector = class {
|
|
|
55165
55367
|
}
|
|
55166
55368
|
});
|
|
55167
55369
|
}
|
|
55168
|
-
async onDeparted(
|
|
55370
|
+
async onDeparted(change) {
|
|
55371
|
+
const entry = change.entry;
|
|
55372
|
+
const timestamp = change.timestamp;
|
|
55169
55373
|
if (!(await this.deps.resolveSettings(entry.deviceId)).packageDropPickupEnabled) {
|
|
55170
55374
|
this.deps.logger.debug("package pick-up skipped — packageDropPickupEnabled is false", {
|
|
55171
55375
|
tags: { deviceId: entry.deviceId },
|
|
@@ -55200,13 +55404,25 @@ var PackageDropDetector = class {
|
|
|
55200
55404
|
});
|
|
55201
55405
|
return;
|
|
55202
55406
|
}
|
|
55407
|
+
const collector = change.collector;
|
|
55408
|
+
const packageTrackId = entry.sourceTrackId ?? entry.id;
|
|
55409
|
+
const mediaTrackId = collector?.trackId ?? packageTrackId;
|
|
55410
|
+
if (collector === void 0) this.deps.logger.info("package pick-up has no collector track — the notification carries the parcel frame", {
|
|
55411
|
+
tags: { deviceId: entry.deviceId },
|
|
55412
|
+
meta: {
|
|
55413
|
+
entryId: entry.id,
|
|
55414
|
+
eventId: pickedUpId,
|
|
55415
|
+
packageTrackId,
|
|
55416
|
+
pickupAt: entry.lastConfirmedAt
|
|
55417
|
+
}
|
|
55418
|
+
});
|
|
55203
55419
|
const ev = {
|
|
55204
55420
|
id: pickedUpId,
|
|
55205
55421
|
kind: "object",
|
|
55206
55422
|
deviceId: entry.deviceId,
|
|
55207
55423
|
timestamp,
|
|
55208
55424
|
source: "pipeline",
|
|
55209
|
-
trackId:
|
|
55425
|
+
trackId: mediaTrackId,
|
|
55210
55426
|
className: PACKAGE_EVENT_CLASS,
|
|
55211
55427
|
...entry.label !== void 0 ? { label: entry.label } : {},
|
|
55212
55428
|
confidence: PACKAGE_IMPORTANCE,
|
|
@@ -55220,6 +55436,7 @@ var PackageDropDetector = class {
|
|
|
55220
55436
|
state: PICKED_UP_STATE,
|
|
55221
55437
|
frameWidth: entry.frameWidth,
|
|
55222
55438
|
frameHeight: entry.frameHeight,
|
|
55439
|
+
...collector?.keyFrameMediaKey !== void 0 ? { mediaKey: collector.keyFrameMediaKey } : {},
|
|
55223
55440
|
importance: PACKAGE_IMPORTANCE
|
|
55224
55441
|
};
|
|
55225
55442
|
await this.deps.events.insertObject(ev);
|
|
@@ -55229,6 +55446,7 @@ var PackageDropDetector = class {
|
|
|
55229
55446
|
entryId: entry.id,
|
|
55230
55447
|
deliveredEventId: deliveredId,
|
|
55231
55448
|
className: entry.className,
|
|
55449
|
+
...collector !== void 0 ? { collectorTrackId: collector.trackId } : {},
|
|
55232
55450
|
timestamp
|
|
55233
55451
|
});
|
|
55234
55452
|
this.deps.logger.info("package picked up", {
|
|
@@ -55236,11 +55454,139 @@ var PackageDropDetector = class {
|
|
|
55236
55454
|
meta: {
|
|
55237
55455
|
entryId: entry.id,
|
|
55238
55456
|
eventId: pickedUpId,
|
|
55239
|
-
deliveredEventId: deliveredId
|
|
55457
|
+
deliveredEventId: deliveredId,
|
|
55458
|
+
mediaTrackId,
|
|
55459
|
+
...collector !== void 0 ? {
|
|
55460
|
+
collectorTrackId: collector.trackId,
|
|
55461
|
+
collectorClassName: collector.className,
|
|
55462
|
+
collectorSkewMs: collector.skewMs
|
|
55463
|
+
} : { collectorTrackId: null }
|
|
55240
55464
|
}
|
|
55241
55465
|
});
|
|
55242
55466
|
}
|
|
55243
55467
|
};
|
|
55468
|
+
/**
|
|
55469
|
+
* How long a presence survives in the mirror, in FRAME time.
|
|
55470
|
+
*
|
|
55471
|
+
* It must outlive the whole detection latency: a pick-up at T is concluded no
|
|
55472
|
+
* earlier than T + `PACKAGE_STILLNESS_TTL_MS` (5 min of observed time), and the
|
|
55473
|
+
* pick is made at conclusion time. 10 minutes is double that, so a collector
|
|
55474
|
+
* noted at the true instant is still there to be named — and a mirror that
|
|
55475
|
+
* expires the answer before the question is asked would be the same silent
|
|
55476
|
+
* hole in a different place.
|
|
55477
|
+
*/
|
|
55478
|
+
var PACKAGE_COLLECTOR_RETENTION_MS = 10 * 6e4;
|
|
55479
|
+
/** The pick-up instant a stationary entry implies. One derivation, named. */
|
|
55480
|
+
function pickupInstantOf(entry) {
|
|
55481
|
+
return entry.lastConfirmedAt;
|
|
55482
|
+
}
|
|
55483
|
+
var PackageCollectorMirror = class {
|
|
55484
|
+
/** Per device, per track. Every write replaces the record (immutable). */
|
|
55485
|
+
byDevice = /* @__PURE__ */ new Map();
|
|
55486
|
+
/**
|
|
55487
|
+
* Record one sighting. Extends the track's presence span and refreshes its
|
|
55488
|
+
* key-frame handle (a re-shot key frame is a better one, and the newest is
|
|
55489
|
+
* also the nearest to the pick-up — the same trade as above).
|
|
55490
|
+
*/
|
|
55491
|
+
note(obs) {
|
|
55492
|
+
const current = this.byDevice.get(obs.deviceId);
|
|
55493
|
+
const next = /* @__PURE__ */ new Map();
|
|
55494
|
+
const horizon = obs.at - PACKAGE_COLLECTOR_RETENTION_MS;
|
|
55495
|
+
if (current !== void 0) for (const [id, p] of current) {
|
|
55496
|
+
if (p.lastAt < horizon) continue;
|
|
55497
|
+
next.set(id, p);
|
|
55498
|
+
}
|
|
55499
|
+
const prev = next.get(obs.trackId);
|
|
55500
|
+
next.set(obs.trackId, {
|
|
55501
|
+
trackId: obs.trackId,
|
|
55502
|
+
className: obs.className,
|
|
55503
|
+
firstAt: prev === void 0 ? obs.at : Math.min(prev.firstAt, obs.at),
|
|
55504
|
+
lastAt: prev === void 0 ? obs.at : Math.max(prev.lastAt, obs.at),
|
|
55505
|
+
...obs.keyFrameMediaKey !== void 0 ? { keyFrameMediaKey: obs.keyFrameMediaKey } : prev?.keyFrameMediaKey !== void 0 ? { keyFrameMediaKey: prev.keyFrameMediaKey } : {}
|
|
55506
|
+
});
|
|
55507
|
+
this.byDevice.set(obs.deviceId, evictOldest(next));
|
|
55508
|
+
}
|
|
55509
|
+
/**
|
|
55510
|
+
* The track nearest the pick-up instant, or `null` when nobody qualifies.
|
|
55511
|
+
*
|
|
55512
|
+
* `null` is a real answer and not an error: a parcel can stop being seen
|
|
55513
|
+
* because the light changed or the detector lost it, and naming a person who
|
|
55514
|
+
* was not there would be worse than naming nobody. The caller degrades to the
|
|
55515
|
+
* parcel's own owner and logs it.
|
|
55516
|
+
*/
|
|
55517
|
+
pickCollector(input) {
|
|
55518
|
+
const presences = this.byDevice.get(input.deviceId);
|
|
55519
|
+
if (presences === void 0 || presences.size === 0) return null;
|
|
55520
|
+
const maxSkewMs = input.maxSkewMs ?? 3e4;
|
|
55521
|
+
let best = null;
|
|
55522
|
+
let bestLastAt = Number.NEGATIVE_INFINITY;
|
|
55523
|
+
for (const p of presences.values()) {
|
|
55524
|
+
const observedAt = clamp(input.pickupAt, p.firstAt, p.lastAt);
|
|
55525
|
+
const skewMs = Math.abs(observedAt - input.pickupAt);
|
|
55526
|
+
if (skewMs > maxSkewMs) continue;
|
|
55527
|
+
if (best !== null && (skewMs > best.skewMs || skewMs === best.skewMs && p.lastAt <= bestLastAt)) continue;
|
|
55528
|
+
best = {
|
|
55529
|
+
trackId: p.trackId,
|
|
55530
|
+
className: p.className,
|
|
55531
|
+
observedAt,
|
|
55532
|
+
skewMs,
|
|
55533
|
+
...p.keyFrameMediaKey !== void 0 ? { keyFrameMediaKey: p.keyFrameMediaKey } : {}
|
|
55534
|
+
};
|
|
55535
|
+
bestLastAt = p.lastAt;
|
|
55536
|
+
}
|
|
55537
|
+
return best;
|
|
55538
|
+
}
|
|
55539
|
+
/**
|
|
55540
|
+
* Fold one pipeline frame in. Returns how many presences it touched, so the
|
|
55541
|
+
* caller can say "nobody was ever in the drop-off zone" with a number.
|
|
55542
|
+
*
|
|
55543
|
+
* This is the ONLY place the frame-shaped input is interpreted, so the
|
|
55544
|
+
* production path and the test path read the same rules: a non-package class,
|
|
55545
|
+
* inside a configured drop-off zone, on this frame.
|
|
55546
|
+
*/
|
|
55547
|
+
noteFrame(input) {
|
|
55548
|
+
if (input.packageZoneIds.size === 0) return 0;
|
|
55549
|
+
let noted = 0;
|
|
55550
|
+
for (const t of input.tracked) {
|
|
55551
|
+
if (input.packageClasses.has(t.className)) continue;
|
|
55552
|
+
if (!isDelivererInPackageZone(t.zones, input.packageZoneIds)) continue;
|
|
55553
|
+
const keyFrameMediaKey = input.keyFrameMediaKeyFor(t.trackId);
|
|
55554
|
+
this.note({
|
|
55555
|
+
deviceId: input.deviceId,
|
|
55556
|
+
trackId: t.trackId,
|
|
55557
|
+
className: t.className,
|
|
55558
|
+
at: input.timestamp,
|
|
55559
|
+
...keyFrameMediaKey !== void 0 ? { keyFrameMediaKey } : {}
|
|
55560
|
+
});
|
|
55561
|
+
noted += 1;
|
|
55562
|
+
}
|
|
55563
|
+
return noted;
|
|
55564
|
+
}
|
|
55565
|
+
/** Drop a camera's memory (unbind / step disabled / device removal). */
|
|
55566
|
+
forgetDevice(deviceId) {
|
|
55567
|
+
this.byDevice.delete(deviceId);
|
|
55568
|
+
}
|
|
55569
|
+
reset() {
|
|
55570
|
+
this.byDevice.clear();
|
|
55571
|
+
}
|
|
55572
|
+
/** Retained presences for a camera — diagnostics and tests only. */
|
|
55573
|
+
debugPresenceCount(deviceId) {
|
|
55574
|
+
return this.byDevice.get(deviceId)?.size ?? 0;
|
|
55575
|
+
}
|
|
55576
|
+
};
|
|
55577
|
+
function clamp(value, low, high) {
|
|
55578
|
+
if (value < low) return low;
|
|
55579
|
+
if (value > high) return high;
|
|
55580
|
+
return value;
|
|
55581
|
+
}
|
|
55582
|
+
/** Keep the newest {@link PACKAGE_COLLECTOR_MAX_PRESENCES} by `lastAt`. */
|
|
55583
|
+
function evictOldest(presences) {
|
|
55584
|
+
if (presences.size <= 32) return presences;
|
|
55585
|
+
const ordered = [...presences.values()].sort((a, b) => b.lastAt - a.lastAt);
|
|
55586
|
+
const kept = /* @__PURE__ */ new Map();
|
|
55587
|
+
for (const p of ordered.slice(0, 32)) kept.set(p.trackId, p);
|
|
55588
|
+
return kept;
|
|
55589
|
+
}
|
|
55244
55590
|
//#endregion
|
|
55245
55591
|
//#region src/pipeline-analytics/pipeline/person-over-vehicle.ts
|
|
55246
55592
|
/**
|
|
@@ -56716,6 +57062,61 @@ function classifyTrackAppearance(input) {
|
|
|
56716
57062
|
if (input.inPrevActive) return "continuing";
|
|
56717
57063
|
return input.positionsCount > 1 ? "resurrection" : "birth";
|
|
56718
57064
|
}
|
|
57065
|
+
//#endregion
|
|
57066
|
+
//#region src/pipeline-analytics/pipeline/archive-debug-note.ts
|
|
57067
|
+
/**
|
|
57068
|
+
* Archive the note a clear is about to destroy. Never throws.
|
|
57069
|
+
*
|
|
57070
|
+
* **Which way this fails, stated once.** The operator pressed a flag button;
|
|
57071
|
+
* the archive is this system's own bookkeeping. So a failing archive does NOT
|
|
57072
|
+
* block the flag write — the button must do what it says even when SQLite is
|
|
57073
|
+
* busy — and it does NOT retry, because the caller is a synchronous cap method
|
|
57074
|
+
* an operator is waiting on.
|
|
57075
|
+
*
|
|
57076
|
+
* What it must never do is lose the sentence quietly. The failure line carries
|
|
57077
|
+
* the NOTE TEXT verbatim, plus `tags: { deviceId }` and the trackId, so the
|
|
57078
|
+
* words are recoverable from the log by hand and the loss is attributable to
|
|
57079
|
+
* one camera. A branch that drops work silently reads as "never happened", and
|
|
57080
|
+
* the 3-hour media blackout produced not one line.
|
|
57081
|
+
*
|
|
57082
|
+
* An EMPTY note archives nothing: there is nothing to keep, and a row of `''`
|
|
57083
|
+
* would be a permanent entry in a corpus meant to be mined for sentences.
|
|
57084
|
+
*/
|
|
57085
|
+
async function archiveNoteBeforeClear(input) {
|
|
57086
|
+
const note = input.current.debugNote;
|
|
57087
|
+
if (note.trim() === "") return;
|
|
57088
|
+
if (input.archive === void 0) {
|
|
57089
|
+
input.logger.warn("debug note dropped — no archive is wired on this runner", {
|
|
57090
|
+
tags: { deviceId: input.deviceId },
|
|
57091
|
+
meta: {
|
|
57092
|
+
deviceId: input.deviceId,
|
|
57093
|
+
trackId: input.trackId,
|
|
57094
|
+
note
|
|
57095
|
+
}
|
|
57096
|
+
});
|
|
57097
|
+
return;
|
|
57098
|
+
}
|
|
57099
|
+
try {
|
|
57100
|
+
await input.archive.append({
|
|
57101
|
+
deviceId: input.deviceId,
|
|
57102
|
+
sourceTrackId: input.trackId,
|
|
57103
|
+
note,
|
|
57104
|
+
...input.current.startedAt !== void 0 ? { trackStartedAt: input.current.startedAt } : {},
|
|
57105
|
+
...input.current.className !== void 0 ? { trackClass: input.current.className } : {},
|
|
57106
|
+
...input.current.label !== void 0 ? { trackLabel: input.current.label } : {}
|
|
57107
|
+
});
|
|
57108
|
+
} catch (err) {
|
|
57109
|
+
input.logger.error("failed to archive a debug note — the text is in this line", {
|
|
57110
|
+
tags: { deviceId: input.deviceId },
|
|
57111
|
+
meta: {
|
|
57112
|
+
deviceId: input.deviceId,
|
|
57113
|
+
trackId: input.trackId,
|
|
57114
|
+
note,
|
|
57115
|
+
error: err instanceof Error ? err.message : String(err)
|
|
57116
|
+
}
|
|
57117
|
+
});
|
|
57118
|
+
}
|
|
57119
|
+
}
|
|
56719
57120
|
/** Thrown when the device is already holding its full staging budget. Distinct
|
|
56720
57121
|
* type so a surface can tell "you are out of room" from "that track is gone". */
|
|
56721
57122
|
var StagingBudgetExceededError = class extends Error {
|
|
@@ -56756,7 +57157,10 @@ var TrackAlreadyTrainedError = class extends Error {
|
|
|
56756
57157
|
* caller sent with it. A note surviving the flag is a leftover that reads
|
|
56757
57158
|
* as a live request, and the invariant belongs here rather than in the
|
|
56758
57159
|
* three surfaces that write this patch — one of which is a browser grid
|
|
56759
|
-
* that has no note UI at all and would never think to clear it.
|
|
57160
|
+
* that has no note UI at all and would never think to clear it. Since
|
|
57161
|
+
* D405 the clear is not a DELETE: `applyTrackFlags` ARCHIVES the note
|
|
57162
|
+
* first, into a table that outlives the track. The invariant above is
|
|
57163
|
+
* unchanged — only where the words go.
|
|
56760
57164
|
* 2. **No note in the patch → nothing is written.** This is what makes a
|
|
56761
57165
|
* cancelled prompt free: the app turns debug on, the operator declines to
|
|
56762
57166
|
* type, and the flag still lands. Refusing to write a note must never cost
|
|
@@ -56850,6 +57254,13 @@ async function applyTrackFlags(deps, input) {
|
|
|
56850
57254
|
...input.flags.favourited !== void 0 ? { favourited: input.flags.favourited } : {},
|
|
56851
57255
|
...resolveNotePatch(deps, input, current)
|
|
56852
57256
|
};
|
|
57257
|
+
if (patch.debugNote === "" && current.debugNote !== "") await archiveNoteBeforeClear({
|
|
57258
|
+
deviceId: input.deviceId,
|
|
57259
|
+
trackId: input.trackId,
|
|
57260
|
+
logger,
|
|
57261
|
+
archive: deps.noteArchive,
|
|
57262
|
+
current
|
|
57263
|
+
});
|
|
56853
57264
|
if (Object.keys(patch).length > 0) await store.setFlags(input.trackId, patch);
|
|
56854
57265
|
const retrainStatus = patch.markForTrain === void 0 ? current.retrainStatus : patch.markForTrain ? "staging" : "none";
|
|
56855
57266
|
return {
|
|
@@ -57932,6 +58343,9 @@ async function reconcileGallerySamples(deps, options) {
|
|
|
57932
58343
|
let keyless = 0;
|
|
57933
58344
|
let skippedGrace = 0;
|
|
57934
58345
|
let stoppedByCeiling = false;
|
|
58346
|
+
let missed = 0;
|
|
58347
|
+
/** Verdicts held until there are enough of them to tell rot from a move. */
|
|
58348
|
+
const held = [];
|
|
57935
58349
|
const identities = await deps.listIdentities();
|
|
57936
58350
|
for (const identity of identities) {
|
|
57937
58351
|
const samples = await deps.listSamples(identity.id);
|
|
@@ -57976,6 +58390,43 @@ async function reconcileGallerySamples(deps, options) {
|
|
|
57976
58390
|
}
|
|
57977
58391
|
checked += 1;
|
|
57978
58392
|
if (state === "exists") continue;
|
|
58393
|
+
missed += 1;
|
|
58394
|
+
const minProbes = options.massMissMinProbes ?? 8;
|
|
58395
|
+
const bar = options.massMissFraction ?? .5;
|
|
58396
|
+
if (checked < minProbes) {
|
|
58397
|
+
held.push({
|
|
58398
|
+
identityId: identity.id,
|
|
58399
|
+
sampleId: sample.id,
|
|
58400
|
+
mediaKey: sample.mediaKey
|
|
58401
|
+
});
|
|
58402
|
+
continue;
|
|
58403
|
+
}
|
|
58404
|
+
if (missed / checked >= bar) {
|
|
58405
|
+
const reason = `mass miss: ${String(missed)} of ${String(checked)} probes found no blob`;
|
|
58406
|
+
deps.logger.warn("gallery reconcile: nearly every blob is missing — that is the volume, not the samples. Aborting and keeping every row; check where the gallery location points.", { meta: {
|
|
58407
|
+
checked,
|
|
58408
|
+
missed,
|
|
58409
|
+
held: held.length,
|
|
58410
|
+
identity: identity.name
|
|
58411
|
+
} });
|
|
58412
|
+
return {
|
|
58413
|
+
checked,
|
|
58414
|
+
reclaimed,
|
|
58415
|
+
keyless,
|
|
58416
|
+
skippedGrace,
|
|
58417
|
+
stoppedByCeiling,
|
|
58418
|
+
aborted: reason
|
|
58419
|
+
};
|
|
58420
|
+
}
|
|
58421
|
+
for (const pending of held.splice(0, held.length)) {
|
|
58422
|
+
await deps.removeSample(pending.identityId, pending.sampleId);
|
|
58423
|
+
reclaimed += 1;
|
|
58424
|
+
deps.logger.warn("gallery reconcile: sample blob is gone from the filesystem — reference removed", { meta: {
|
|
58425
|
+
sampleId: pending.sampleId,
|
|
58426
|
+
mediaKey: pending.mediaKey,
|
|
58427
|
+
state: "missing-blob"
|
|
58428
|
+
} });
|
|
58429
|
+
}
|
|
57979
58430
|
await deps.removeSample(identity.id, sample.id);
|
|
57980
58431
|
reclaimed += 1;
|
|
57981
58432
|
deps.logger.warn("gallery reconcile: sample blob is gone from the filesystem — reference removed", { meta: {
|
|
@@ -57986,6 +58437,14 @@ async function reconcileGallerySamples(deps, options) {
|
|
|
57986
58437
|
} });
|
|
57987
58438
|
}
|
|
57988
58439
|
}
|
|
58440
|
+
for (const pending of held.splice(0, held.length)) {
|
|
58441
|
+
if (reclaimed >= options.maxReclaimPerRun) {
|
|
58442
|
+
stoppedByCeiling = true;
|
|
58443
|
+
break;
|
|
58444
|
+
}
|
|
58445
|
+
await deps.removeSample(pending.identityId, pending.sampleId);
|
|
58446
|
+
reclaimed += 1;
|
|
58447
|
+
}
|
|
57989
58448
|
if (reclaimed > 0) deps.logger.info("gallery reconcile: dead references removed", { meta: {
|
|
57990
58449
|
checked,
|
|
57991
58450
|
reclaimed,
|
|
@@ -59483,6 +59942,261 @@ var IdentityStore = class {
|
|
|
59483
59942
|
}
|
|
59484
59943
|
};
|
|
59485
59944
|
//#endregion
|
|
59945
|
+
//#region src/pipeline-analytics/store/debug-note-archive-store.ts
|
|
59946
|
+
/**
|
|
59947
|
+
* DebugNoteArchiveStore — the durable corpus of what operators asked to be
|
|
59948
|
+
* checked (D405).
|
|
59949
|
+
*
|
|
59950
|
+
* A declared SQL-backed collection, like the events ops-log next door, and for
|
|
59951
|
+
* the same reason: pipeline-analytics already owns SQLite collections, and an
|
|
59952
|
+
* undeclared collection crash-loops the runner. MUST be declared in
|
|
59953
|
+
* `onInitialize` before the first write.
|
|
59954
|
+
*
|
|
59955
|
+
* Collection name: pipeline-analytics:debug-note-archive
|
|
59956
|
+
*
|
|
59957
|
+
* Two properties separate it from every other table in this addon:
|
|
59958
|
+
*
|
|
59959
|
+
* - **It is not track-owned.** It names a track in `sourceTrackId` and that is
|
|
59960
|
+
* PROVENANCE, never ownership — the whole point is that the row outlives the
|
|
59961
|
+
* track, which `debug` deliberately never pinned (D353). It is classified
|
|
59962
|
+
* `never-orphaned` by hand in `collection-classification.ts` so the
|
|
59963
|
+
* ownership derivation cannot see the column and cascade it away.
|
|
59964
|
+
* - **It is bounded by COUNT, not by age.** An age sweep would delete the
|
|
59965
|
+
* corpus for being old, which is the failure this table exists to fix: a
|
|
59966
|
+
* six-month-old note is the most valuable row in it, because the pattern it
|
|
59967
|
+
* describes has had time to repeat. See {@link MAX_ARCHIVED_DEBUG_NOTES}.
|
|
59968
|
+
*/
|
|
59969
|
+
/**
|
|
59970
|
+
* @durable class=ledger owner=pipeline-analytics
|
|
59971
|
+
* write="one row per debug note RETIRED by a review — appended by
|
|
59972
|
+
* `applyTrackFlags` immediately before `debug: false` clears the note off
|
|
59973
|
+
* the track row (D405). Idempotent per (sourceTrackId, note): the row id is
|
|
59974
|
+
* derived from the pair, so reviewing the same track twice cannot double
|
|
59975
|
+
* the corpus. An empty note writes nothing."
|
|
59976
|
+
* retention="NOT track retention and NOT an age clock — deleting a note for
|
|
59977
|
+
* being old is the failure this table fixes. Bounded by ROW COUNT
|
|
59978
|
+
* (MAX_ARCHIVED_DEBUG_NOTES, 5000, fleet-wide), oldest `archivedAt` first,
|
|
59979
|
+
* trimmed on append. ~3 MB of text at the ceiling."
|
|
59980
|
+
*/
|
|
59981
|
+
var DEBUG_NOTE_ARCHIVE_COLLECTION = "pipeline-analytics:debug-note-archive";
|
|
59982
|
+
var DEBUG_NOTE_ARCHIVE_COLUMNS = [
|
|
59983
|
+
{
|
|
59984
|
+
name: "id",
|
|
59985
|
+
type: "TEXT",
|
|
59986
|
+
primaryKey: true,
|
|
59987
|
+
notNull: true
|
|
59988
|
+
},
|
|
59989
|
+
{
|
|
59990
|
+
name: "archivedAt",
|
|
59991
|
+
type: "INTEGER",
|
|
59992
|
+
notNull: true
|
|
59993
|
+
},
|
|
59994
|
+
{
|
|
59995
|
+
name: "deviceId",
|
|
59996
|
+
type: "INTEGER",
|
|
59997
|
+
notNull: true
|
|
59998
|
+
},
|
|
59999
|
+
{
|
|
60000
|
+
name: "sourceTrackId",
|
|
60001
|
+
type: "TEXT",
|
|
60002
|
+
notNull: true
|
|
60003
|
+
},
|
|
60004
|
+
{
|
|
60005
|
+
name: "note",
|
|
60006
|
+
type: "TEXT",
|
|
60007
|
+
notNull: true
|
|
60008
|
+
},
|
|
60009
|
+
{
|
|
60010
|
+
name: "trackStartedAt",
|
|
60011
|
+
type: "INTEGER"
|
|
60012
|
+
},
|
|
60013
|
+
{
|
|
60014
|
+
name: "trackClass",
|
|
60015
|
+
type: "TEXT"
|
|
60016
|
+
},
|
|
60017
|
+
{
|
|
60018
|
+
name: "trackLabel",
|
|
60019
|
+
type: "TEXT"
|
|
60020
|
+
}
|
|
60021
|
+
];
|
|
60022
|
+
var DEBUG_NOTE_ARCHIVE_INDEXES = [{
|
|
60023
|
+
name: "idx_debugnote_archived_at",
|
|
60024
|
+
columns: ["archivedAt"]
|
|
60025
|
+
}, {
|
|
60026
|
+
name: "idx_debugnote_device_at",
|
|
60027
|
+
columns: ["deviceId", "archivedAt"]
|
|
60028
|
+
}];
|
|
60029
|
+
/**
|
|
60030
|
+
* The row id for one `(track, note)` pair.
|
|
60031
|
+
*
|
|
60032
|
+
* Deterministic on purpose: the idempotency the operator needs is "reviewing
|
|
60033
|
+
* twice does not double the row", and a derived primary key gets it from the
|
|
60034
|
+
* database rather than from a read-modify-write that two concurrent reviews
|
|
60035
|
+
* could interleave through. Truncated to 32 hex characters — 128 bits over a
|
|
60036
|
+
* table capped at 5 000 rows.
|
|
60037
|
+
*/
|
|
60038
|
+
function archiveRowId(sourceTrackId, note) {
|
|
60039
|
+
return (0, node_crypto.createHash)("sha256").update(`${sourceTrackId}${note}`).digest("hex").slice(0, 32);
|
|
60040
|
+
}
|
|
60041
|
+
var DebugNoteArchiveStore = class {
|
|
60042
|
+
store;
|
|
60043
|
+
logger;
|
|
60044
|
+
now;
|
|
60045
|
+
maxRows;
|
|
60046
|
+
constructor(deps) {
|
|
60047
|
+
this.store = deps.store;
|
|
60048
|
+
this.logger = deps.logger;
|
|
60049
|
+
this.now = deps.now ?? (() => Date.now());
|
|
60050
|
+
this.maxRows = deps.maxRows ?? 5e3;
|
|
60051
|
+
}
|
|
60052
|
+
/** The ceiling a store built without an override enforces. */
|
|
60053
|
+
static defaultMaxRows() {
|
|
60054
|
+
return require_dist.MAX_ARCHIVED_DEBUG_NOTES;
|
|
60055
|
+
}
|
|
60056
|
+
static async declare(store) {
|
|
60057
|
+
await store.declareCollection.mutate({
|
|
60058
|
+
collection: DEBUG_NOTE_ARCHIVE_COLLECTION,
|
|
60059
|
+
columns: [...DEBUG_NOTE_ARCHIVE_COLUMNS],
|
|
60060
|
+
indexes: [...DEBUG_NOTE_ARCHIVE_INDEXES]
|
|
60061
|
+
});
|
|
60062
|
+
}
|
|
60063
|
+
/**
|
|
60064
|
+
* Append one retired note. THROWS if the row cannot be written.
|
|
60065
|
+
*
|
|
60066
|
+
* Deliberately not best-effort, unlike the ops-log's `append`: this is the
|
|
60067
|
+
* only copy of something a person typed, and the caller
|
|
60068
|
+
* (`archiveNoteBeforeClear`) has both the text and the camera and is the
|
|
60069
|
+
* right place to decide what a failure costs — it logs the note verbatim and
|
|
60070
|
+
* lets the operator's flag write proceed. Swallowing here would hide the
|
|
60071
|
+
* failure from the one caller able to preserve the words.
|
|
60072
|
+
*
|
|
60073
|
+
* The TRIM is the exception and is best-effort: the row has already landed,
|
|
60074
|
+
* and failing the append because housekeeping could not run would make the
|
|
60075
|
+
* caller report a note lost that is sitting in the table.
|
|
60076
|
+
*/
|
|
60077
|
+
async append(input) {
|
|
60078
|
+
const note = input.note.trim() === "" ? "" : input.note;
|
|
60079
|
+
if (note === "") return {
|
|
60080
|
+
archived: false,
|
|
60081
|
+
id: null
|
|
60082
|
+
};
|
|
60083
|
+
const id = archiveRowId(input.sourceTrackId, note);
|
|
60084
|
+
if ((await this.store.query.query({
|
|
60085
|
+
collection: "pipeline-analytics:debug-note-archive",
|
|
60086
|
+
filter: {
|
|
60087
|
+
where: { id },
|
|
60088
|
+
limit: 1
|
|
60089
|
+
}
|
|
60090
|
+
})).length > 0) return {
|
|
60091
|
+
archived: false,
|
|
60092
|
+
id
|
|
60093
|
+
};
|
|
60094
|
+
const row = require_dist.ArchivedDebugNoteSchema.parse({
|
|
60095
|
+
id,
|
|
60096
|
+
note,
|
|
60097
|
+
deviceId: input.deviceId,
|
|
60098
|
+
sourceTrackId: input.sourceTrackId,
|
|
60099
|
+
archivedAt: this.now(),
|
|
60100
|
+
trackStartedAt: input.trackStartedAt ?? null,
|
|
60101
|
+
trackClass: input.trackClass ?? null,
|
|
60102
|
+
trackLabel: input.trackLabel ?? null
|
|
60103
|
+
});
|
|
60104
|
+
await this.store.insert.mutate({
|
|
60105
|
+
collection: DEBUG_NOTE_ARCHIVE_COLLECTION,
|
|
60106
|
+
record: {
|
|
60107
|
+
id: row.id,
|
|
60108
|
+
data: {
|
|
60109
|
+
note: row.note,
|
|
60110
|
+
deviceId: row.deviceId,
|
|
60111
|
+
sourceTrackId: row.sourceTrackId,
|
|
60112
|
+
archivedAt: row.archivedAt,
|
|
60113
|
+
trackStartedAt: row.trackStartedAt,
|
|
60114
|
+
trackClass: row.trackClass,
|
|
60115
|
+
trackLabel: row.trackLabel
|
|
60116
|
+
}
|
|
60117
|
+
}
|
|
60118
|
+
});
|
|
60119
|
+
await this.trim();
|
|
60120
|
+
return {
|
|
60121
|
+
archived: true,
|
|
60122
|
+
id: row.id
|
|
60123
|
+
};
|
|
60124
|
+
}
|
|
60125
|
+
/**
|
|
60126
|
+
* Hold the table at its ceiling, oldest first. Best-effort by design — see
|
|
60127
|
+
* {@link append}.
|
|
60128
|
+
*
|
|
60129
|
+
* NOT an age-keyed sweep and must never become one: the boundary below is
|
|
60130
|
+
* computed from the POSITION of the excess rows, never from a clock, so a
|
|
60131
|
+
* quiet fleet keeps its whole corpus for ever and a busy one drops its oldest
|
|
60132
|
+
* questions. Rows sharing the boundary timestamp go together; at a
|
|
60133
|
+
* one-per-review write rate that is a rounding error, and the alternative —
|
|
60134
|
+
* deleting by id, one round trip per row — is the N+1 drain this store layer
|
|
60135
|
+
* spent a release removing.
|
|
60136
|
+
*/
|
|
60137
|
+
async trim() {
|
|
60138
|
+
try {
|
|
60139
|
+
const held = await this.store.count.query({ collection: DEBUG_NOTE_ARCHIVE_COLLECTION });
|
|
60140
|
+
const excess = held - this.maxRows;
|
|
60141
|
+
if (excess <= 0) return;
|
|
60142
|
+
const boundary = (await this.store.query.query({
|
|
60143
|
+
collection: DEBUG_NOTE_ARCHIVE_COLLECTION,
|
|
60144
|
+
filter: {
|
|
60145
|
+
orderBy: {
|
|
60146
|
+
field: "archivedAt",
|
|
60147
|
+
direction: "asc"
|
|
60148
|
+
},
|
|
60149
|
+
limit: excess
|
|
60150
|
+
},
|
|
60151
|
+
columns: ["archivedAt"]
|
|
60152
|
+
})).reduce((acc, r) => {
|
|
60153
|
+
const at = r.data["archivedAt"];
|
|
60154
|
+
return typeof at === "number" && at > acc ? at : acc;
|
|
60155
|
+
}, 0);
|
|
60156
|
+
if (boundary === 0) return;
|
|
60157
|
+
const { deleted } = await this.store.deleteWhere.mutate({
|
|
60158
|
+
collection: DEBUG_NOTE_ARCHIVE_COLLECTION,
|
|
60159
|
+
filter: { whereBetween: { archivedAt: [0, boundary] } }
|
|
60160
|
+
});
|
|
60161
|
+
this.logger.info("debug-note archive trimmed to its ceiling", { meta: {
|
|
60162
|
+
held,
|
|
60163
|
+
ceiling: this.maxRows,
|
|
60164
|
+
deleted
|
|
60165
|
+
} });
|
|
60166
|
+
} catch (err) {
|
|
60167
|
+
this.logger.warn("debug-note archive trim failed — the archive will keep growing", { meta: {
|
|
60168
|
+
ceiling: this.maxRows,
|
|
60169
|
+
error: err instanceof Error ? err.message : String(err)
|
|
60170
|
+
} });
|
|
60171
|
+
}
|
|
60172
|
+
}
|
|
60173
|
+
/** Archived notes, newest first, optionally scoped to one camera. */
|
|
60174
|
+
async list(query) {
|
|
60175
|
+
const filter = {
|
|
60176
|
+
orderBy: {
|
|
60177
|
+
field: "archivedAt",
|
|
60178
|
+
direction: "desc"
|
|
60179
|
+
},
|
|
60180
|
+
limit: query.limit ?? 200
|
|
60181
|
+
};
|
|
60182
|
+
if (query.deviceId !== void 0) filter["where"] = { deviceId: query.deviceId };
|
|
60183
|
+
const rows = await this.store.query.query({
|
|
60184
|
+
collection: DEBUG_NOTE_ARCHIVE_COLLECTION,
|
|
60185
|
+
filter
|
|
60186
|
+
});
|
|
60187
|
+
const out = [];
|
|
60188
|
+
for (const r of rows) {
|
|
60189
|
+
const parsed = require_dist.ArchivedDebugNoteSchema.safeParse({
|
|
60190
|
+
id: r.id,
|
|
60191
|
+
...r.data
|
|
60192
|
+
});
|
|
60193
|
+
if (parsed.success) out.push(parsed.data);
|
|
60194
|
+
else this.logger.debug("debug-note archive: skipped a malformed row", { meta: { id: r.id } });
|
|
60195
|
+
}
|
|
60196
|
+
return out;
|
|
60197
|
+
}
|
|
60198
|
+
};
|
|
60199
|
+
//#endregion
|
|
59486
60200
|
//#region src/pipeline-analytics/store/ops-log-store.ts
|
|
59487
60201
|
/**
|
|
59488
60202
|
* OpsLogStore — the EVENTS-domain operations audit for pipeline-analytics.
|
|
@@ -60846,6 +61560,13 @@ var ANALYTICS_COLLECTIONS = [
|
|
|
60846
61560
|
why: "the drained predecessor of the row above, declared only so its rows can be read once and moved (`viewer-settings-legacy-adoption.ts`). Retention must never touch it: its rows go one way only, to the successor, gated on a re-read that confirms they landed. Delete this entry together with that module."
|
|
60847
61561
|
}
|
|
60848
61562
|
},
|
|
61563
|
+
{
|
|
61564
|
+
collection: DEBUG_NOTE_ARCHIVE_COLLECTION,
|
|
61565
|
+
classification: {
|
|
61566
|
+
kind: "never-orphaned",
|
|
61567
|
+
why: "the archived operator notes (D405) — the ONE table here whose whole purpose is to outlive the track it names. `debug` never pinned a track against retention and D405 does not change that, so the track a note was written on IS expected to be evicted; deleting the note with it is the failure the archive was built to fix (25–27 notes from one sweep unreadable the same afternoon). Its column is called `sourceTrackId` and not `trackId` precisely so the ownership derivation cannot reach it, and its bound is a fleet-wide ROW COUNT trimmed on append — never an age sweep, which would delete the corpus for the one property that makes it valuable."
|
|
61568
|
+
}
|
|
61569
|
+
},
|
|
60849
61570
|
{
|
|
60850
61571
|
collection: STATIONARY_COLLECTION,
|
|
60851
61572
|
classification: {
|
|
@@ -68616,6 +69337,12 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
68616
69337
|
eventStore = null;
|
|
68617
69338
|
/** Events-domain ops-log (footprint/prune/manual-delete audit). Null until onInitialize. */
|
|
68618
69339
|
eventsOpsLog = null;
|
|
69340
|
+
/**
|
|
69341
|
+
* The archived operator debug notes (D405). Null until onInitialize — and
|
|
69342
|
+
* `applyTrackFlags` treats null as "no archive wired": the flag write the
|
|
69343
|
+
* operator asked for never depends on this addon's own bookkeeping.
|
|
69344
|
+
*/
|
|
69345
|
+
debugNoteArchive = null;
|
|
68619
69346
|
/** Event-media relocation engine (entity-routing Phase 4). */
|
|
68620
69347
|
mediaRelocate = null;
|
|
68621
69348
|
mediaLocationStorage = null;
|
|
@@ -69173,6 +69900,12 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
69173
69900
|
* which would let the streetlight wedge vouch for its own delivery.
|
|
69174
69901
|
*/
|
|
69175
69902
|
lastNonPackageActivityAt = /* @__PURE__ */ new Map();
|
|
69903
|
+
/**
|
|
69904
|
+
* WHO was in the drop-off zone, and when (D404). The pick-up is CONCLUDED
|
|
69905
|
+
* minutes after the fact, so the collector must be remembered while it is
|
|
69906
|
+
* still true — handles only, never pixels.
|
|
69907
|
+
*/
|
|
69908
|
+
packageCollectors = new PackageCollectorMirror();
|
|
69176
69909
|
/** Last time the oversize-sighting drop was logged per device — one line
|
|
69177
69910
|
* per 5 minutes, not one per governor round (the wedge re-sights all
|
|
69178
69911
|
* night; ~300 identical lines would bury the log it exists to serve). */
|
|
@@ -69598,6 +70331,10 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
69598
70331
|
logger: logger.child("ops-log"),
|
|
69599
70332
|
nodeId: ownNodeId
|
|
69600
70333
|
});
|
|
70334
|
+
this.debugNoteArchive = new DebugNoteArchiveStore({
|
|
70335
|
+
store: api.settingsStore,
|
|
70336
|
+
logger: logger.child("debug-note-archive")
|
|
70337
|
+
});
|
|
69601
70338
|
{
|
|
69602
70339
|
const designated = await step("postProcessingNodeState", () => this.postProcessingNodeState.get());
|
|
69603
70340
|
this.isPostProcessingNode = ownNodeId === designated;
|
|
@@ -69917,6 +70654,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
69917
70654
|
RetrainFrameStore.declare(api.settingsStore),
|
|
69918
70655
|
RetrainAnnotationStore.declare(api.settingsStore),
|
|
69919
70656
|
OpsLogStore.declare(api.settingsStore),
|
|
70657
|
+
DebugNoteArchiveStore.declare(api.settingsStore),
|
|
69920
70658
|
AnalyticsLts.declare(api.settingsStore),
|
|
69921
70659
|
SceneStore.declare(api.settingsStore),
|
|
69922
70660
|
NotificationCenter.declare(api.settingsStore),
|
|
@@ -71555,6 +72293,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
71555
72293
|
this.packageStillness.forgetDevice(data.deviceId);
|
|
71556
72294
|
this.packageDropDetector?.forgetDevice(data.deviceId);
|
|
71557
72295
|
this.lastNonPackageActivityAt.delete(data.deviceId);
|
|
72296
|
+
this.packageCollectors.forgetDevice(data.deviceId);
|
|
71558
72297
|
this.packageOversizeLogAt.delete(data.deviceId);
|
|
71559
72298
|
this.overlayState.clearDevice(data.deviceId);
|
|
71560
72299
|
this.overlaySynthesisWarnAt.delete(data.deviceId);
|
|
@@ -71574,6 +72313,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
71574
72313
|
this.packageStillness.forgetDevice(deviceId);
|
|
71575
72314
|
this.packageDropDetector?.forgetDevice(deviceId);
|
|
71576
72315
|
this.lastNonPackageActivityAt.delete(deviceId);
|
|
72316
|
+
this.packageCollectors.forgetDevice(deviceId);
|
|
71577
72317
|
this.packageOversizeLogAt.delete(deviceId);
|
|
71578
72318
|
this.overlayState.clearDevice(deviceId);
|
|
71579
72319
|
this.overlaySynthesisWarnAt.delete(deviceId);
|
|
@@ -71964,6 +72704,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
71964
72704
|
eventStore: stores.eventStore,
|
|
71965
72705
|
logger: this.ctx.logger,
|
|
71966
72706
|
refreshGallery: () => this.faceRecognizer?.refreshGallery(),
|
|
72707
|
+
resolveAutoAssignThreshold: async () => (await this.readDeviceSettings(0, resolveFaceSettings)).similarityThreshold,
|
|
71967
72708
|
resolveClusterModelId: async () => {
|
|
71968
72709
|
const api = this.ctx.api;
|
|
71969
72710
|
if (!api) return null;
|
|
@@ -72456,12 +73197,26 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
72456
73197
|
this.residents.markFirstFramePending(deviceId, id);
|
|
72457
73198
|
this.trackStore.seedSnapshotClock(id, result.timestamp, t.bbox);
|
|
72458
73199
|
this.residents.setLastFrameAt(deviceId, id, result.timestamp);
|
|
72459
|
-
|
|
73200
|
+
const birthCapture = decideBirthFirstFrameCapture({
|
|
73201
|
+
hasFrameHandle: frameHandle !== void 0,
|
|
73202
|
+
matchedThisFrame: t.matchedThisFrame
|
|
73203
|
+
});
|
|
73204
|
+
if (birthCapture.capture) firstFrameTargets.push({
|
|
72460
73205
|
trackId: id,
|
|
72461
73206
|
timestamp: result.timestamp,
|
|
72462
73207
|
bbox: { ...t.bbox },
|
|
72463
73208
|
...patchDisplayLabel(t.labelPatch) !== void 0 ? { label: patchDisplayLabel(t.labelPatch) } : {}
|
|
72464
73209
|
});
|
|
73210
|
+
else log.info("birth firstFrame not captured — retry armed", {
|
|
73211
|
+
tags: { deviceId },
|
|
73212
|
+
meta: {
|
|
73213
|
+
trackId: id,
|
|
73214
|
+
className: t.className,
|
|
73215
|
+
source,
|
|
73216
|
+
refusedBecause: birthCapture.refusedBecause,
|
|
73217
|
+
deferredMs: result.timestamp - (this.trackStore?.peekActive(id)?.firstSeen ?? result.timestamp)
|
|
73218
|
+
}
|
|
73219
|
+
});
|
|
72465
73220
|
}
|
|
72466
73221
|
this.ctx.eventBus.emit({
|
|
72467
73222
|
id: `pa-${(0, node_crypto.randomUUID)()}`,
|
|
@@ -75171,6 +75926,14 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
75171
75926
|
this.lastNonPackageActivityAt.set(input.deviceId, input.timestamp);
|
|
75172
75927
|
break;
|
|
75173
75928
|
}
|
|
75929
|
+
this.packageCollectors.noteFrame({
|
|
75930
|
+
deviceId: input.deviceId,
|
|
75931
|
+
tracked: input.tracked,
|
|
75932
|
+
packageClasses: input.packageClasses,
|
|
75933
|
+
packageZoneIds: input.packageZoneIds,
|
|
75934
|
+
timestamp: input.timestamp,
|
|
75935
|
+
keyFrameMediaKeyFor: (trackId) => this.residents.keyFrameKey(trackId)
|
|
75936
|
+
});
|
|
75174
75937
|
const sightings = [];
|
|
75175
75938
|
for (const t of input.tracked) {
|
|
75176
75939
|
if (!input.packageClasses.has(t.className)) continue;
|
|
@@ -75282,10 +76045,24 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
75282
76045
|
meta: { entryId: entry.id }
|
|
75283
76046
|
});
|
|
75284
76047
|
}).catch(() => void 0);
|
|
76048
|
+
const collector = this.packageCollectors.pickCollector({
|
|
76049
|
+
deviceId: input.deviceId,
|
|
76050
|
+
pickupAt: pickupInstantOf(entry)
|
|
76051
|
+
});
|
|
76052
|
+
if (collector === null) this.ctx.logger.info("package pick-up has no collector in the drop-off zone", {
|
|
76053
|
+
tags,
|
|
76054
|
+
meta: {
|
|
76055
|
+
entryId: entry.id,
|
|
76056
|
+
pickupAt: entry.lastConfirmedAt,
|
|
76057
|
+
departedAt: input.timestamp,
|
|
76058
|
+
presencesKnown: this.packageCollectors.debugPresenceCount(input.deviceId)
|
|
76059
|
+
}
|
|
76060
|
+
});
|
|
75285
76061
|
this.packageDropDetector.onStationaryChange({
|
|
75286
76062
|
phase: "departed",
|
|
75287
76063
|
entry,
|
|
75288
|
-
timestamp: input.timestamp
|
|
76064
|
+
timestamp: input.timestamp,
|
|
76065
|
+
...collector !== null ? { collector } : {}
|
|
75289
76066
|
});
|
|
75290
76067
|
}
|
|
75291
76068
|
for (const entry of outcome.departedWithoutActivity) {
|
|
@@ -77410,7 +78187,8 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
77410
78187
|
setFlags: (trackId, patch) => trackStore.setFlags(trackId, patch),
|
|
77411
78188
|
countStaging: (query) => trackStore.countStaging(query)
|
|
77412
78189
|
},
|
|
77413
|
-
logger: this.ctx.logger
|
|
78190
|
+
logger: this.ctx.logger,
|
|
78191
|
+
...this.debugNoteArchive !== null ? { noteArchive: this.debugNoteArchive } : {}
|
|
77414
78192
|
}, input);
|
|
77415
78193
|
this.ctx.logger.info("track operator flags set", {
|
|
77416
78194
|
tags: { deviceId: input.deviceId },
|
|
@@ -77541,6 +78319,18 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
77541
78319
|
return this.queryFacade.listOpsLog(input);
|
|
77542
78320
|
}
|
|
77543
78321
|
/**
|
|
78322
|
+
* The archived debug notes, newest first (D405).
|
|
78323
|
+
*
|
|
78324
|
+
* Answers from the archive alone — never from the track rows. The point of
|
|
78325
|
+
* the table is that the tracks these notes were written on are gone, so a
|
|
78326
|
+
* read that joined them would return exactly the rows the archive exists to
|
|
78327
|
+
* replace. An archive that has not been built yet answers EMPTY, which is
|
|
78328
|
+
* true: nothing has been archived on this runner.
|
|
78329
|
+
*/
|
|
78330
|
+
async listArchivedDebugNotes(input) {
|
|
78331
|
+
return await this.debugNoteArchive?.list(input) ?? [];
|
|
78332
|
+
}
|
|
78333
|
+
/**
|
|
77544
78334
|
* Track-centric time-based retention (design §5.1). Drains every persisted
|
|
77545
78335
|
* track for the device whose `lastSeen < cutoffMs`, page by page, through the
|
|
77546
78336
|
* widened cascade — enrolled faces/plates + identity media are exempt (design
|