@camstack/addon-post-analysis 1.2.307 → 1.2.309
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/{clip-model-registry-C67UM_q_.js → clip-model-registry-B3M4ZI17.js} +392 -264
- package/dist/{clip-model-registry-C9Aw4qkh.mjs → clip-model-registry-CrBn6Qvt.mjs} +387 -265
- 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-QE0Juzwb.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-Bkf0-pki.mjs} +3 -3
- package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-DWg2WU0a.mjs +26 -0
- package/dist/pipeline-analytics/{hostInit-DzNrV8y8.mjs → hostInit-Dj0aaMhv.mjs} +3 -3
- package/dist/pipeline-analytics/index.js +1350 -389
- package/dist/pipeline-analytics/index.mjs +1350 -389
- package/dist/pipeline-analytics/remoteEntry.js +1 -1
- package/package.json +2 -2
- package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-MwQBD526.mjs +0 -26
|
@@ -2,7 +2,7 @@ Object.defineProperties(exports, {
|
|
|
2
2
|
__esModule: { value: true },
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
|
-
const require_clip_model_registry = require("../clip-model-registry-
|
|
5
|
+
const require_clip_model_registry = require("../clip-model-registry-B3M4ZI17.js");
|
|
6
6
|
let node_fs = require("node:fs");
|
|
7
7
|
node_fs = require_clip_model_registry.__toESM(node_fs, 1);
|
|
8
8
|
let node_path = require("node:path");
|
|
@@ -13492,6 +13492,8 @@ var en_default = {
|
|
|
13492
13492
|
"summary.body.empty": "nothing was seen in this window",
|
|
13493
13493
|
"system.state-summary.title": "{{rule}}",
|
|
13494
13494
|
"system.state-summary.body": "{{summaryText}}",
|
|
13495
|
+
"system.state-summary.section.title": "{{rule}} — {{summarySection}}",
|
|
13496
|
+
"system.state-summary.section.body": "{{summaryText}}",
|
|
13495
13497
|
"summary.section.device-state": "Devices offline or switched off",
|
|
13496
13498
|
"summary.section.streams": "Streams offline",
|
|
13497
13499
|
"summary.section.battery": "Low batteries",
|
|
@@ -13820,6 +13822,8 @@ var it_default = {
|
|
|
13820
13822
|
"summary.body.empty": "nessun evento in questa finestra",
|
|
13821
13823
|
"system.state-summary.title": "{{rule}}",
|
|
13822
13824
|
"system.state-summary.body": "{{summaryText}}",
|
|
13825
|
+
"system.state-summary.section.title": "{{rule}} — {{summarySection}}",
|
|
13826
|
+
"system.state-summary.section.body": "{{summaryText}}",
|
|
13823
13827
|
"summary.section.device-state": "Dispositivi offline o spenti",
|
|
13824
13828
|
"summary.section.streams": "Stream offline",
|
|
13825
13829
|
"summary.section.battery": "Batterie scariche",
|
|
@@ -15765,6 +15769,38 @@ function planRuleTest(input) {
|
|
|
15765
15769
|
};
|
|
15766
15770
|
}
|
|
15767
15771
|
//#endregion
|
|
15772
|
+
//#region src/notification-center/state-summary/state-summary-test.ts
|
|
15773
|
+
/**
|
|
15774
|
+
* What TESTING a state-summary rule answers (D698, `nc.testRule`).
|
|
15775
|
+
*
|
|
15776
|
+
* A summary rule sends nothing per event (D647), so the per-event test the
|
|
15777
|
+
* other rules get — one synthetic event, injected — can never deliver for it.
|
|
15778
|
+
* Its test builds the summary NOW and enqueues it; this is the report: the
|
|
15779
|
+
* rows it enqueued (one, or one per section) and every section's read, so
|
|
15780
|
+
* "nothing to report" is an answer with its reason per section, never a
|
|
15781
|
+
* silence.
|
|
15782
|
+
*/
|
|
15783
|
+
var NcStateSummaryTestSchema = require_clip_model_registry.object({
|
|
15784
|
+
/** The test's record id — a per-section row appends `:<sectionId>`. */
|
|
15785
|
+
recordId: require_clip_model_registry.string(),
|
|
15786
|
+
split: require_clip_model_registry._enum(["rule", "section"]),
|
|
15787
|
+
/** Nothing open and every section read: nothing was enqueued. */
|
|
15788
|
+
nothingToReport: require_clip_model_registry.boolean(),
|
|
15789
|
+
sections: require_clip_model_registry.array(require_clip_model_registry.object({
|
|
15790
|
+
id: require_clip_model_registry.string(),
|
|
15791
|
+
status: require_clip_model_registry._enum(["read", "unavailable"]),
|
|
15792
|
+
items: require_clip_model_registry.number().int().nonnegative(),
|
|
15793
|
+
reason: require_clip_model_registry.string().optional()
|
|
15794
|
+
})),
|
|
15795
|
+
rows: require_clip_model_registry.array(require_clip_model_registry.object({
|
|
15796
|
+
recordId: require_clip_model_registry.string(),
|
|
15797
|
+
/** The section of a per-section row. */
|
|
15798
|
+
section: require_clip_model_registry.string().optional(),
|
|
15799
|
+
enqueued: require_clip_model_registry.number().int().nonnegative(),
|
|
15800
|
+
targetIds: require_clip_model_registry.array(require_clip_model_registry.string())
|
|
15801
|
+
}))
|
|
15802
|
+
});
|
|
15803
|
+
//#endregion
|
|
15768
15804
|
//#region src/notification-center/clock-text.ts
|
|
15769
15805
|
/**
|
|
15770
15806
|
* `{{time}}` — hours and minutes in the NOTIFICATION language.
|
|
@@ -17132,13 +17168,18 @@ var ncActions = require_clip_model_registry.defineCustomActions({
|
|
|
17132
17168
|
deviceId: require_clip_model_registry.number().int().optional()
|
|
17133
17169
|
}), NcTestEventResultSchema.extend({
|
|
17134
17170
|
/** The subject kind the rule was tested with — the answer to "why was my
|
|
17135
|
-
* rule not a candidate". */
|
|
17171
|
+
* rule not a candidate". `state-summary` for a summary rule (D698). */
|
|
17136
17172
|
trigger: require_clip_model_registry.string(),
|
|
17137
17173
|
/** What the plan could not reproduce (a device-state gate, a polygon). */
|
|
17138
17174
|
planNotes: require_clip_model_registry.array(require_clip_model_registry.object({
|
|
17139
17175
|
conditionId: require_clip_model_registry.string(),
|
|
17140
17176
|
note: require_clip_model_registry.string()
|
|
17141
|
-
}))
|
|
17177
|
+
})),
|
|
17178
|
+
/**
|
|
17179
|
+
* A STATE-SUMMARY rule's test (D698): the summary built now and the rows
|
|
17180
|
+
* it enqueued, or why nothing was — absent on every other rule.
|
|
17181
|
+
*/
|
|
17182
|
+
stateSummary: NcStateSummaryTestSchema.optional()
|
|
17142
17183
|
}), {
|
|
17143
17184
|
kind: "mutation",
|
|
17144
17185
|
auth: "admin",
|
|
@@ -17497,6 +17538,48 @@ function makeNcActionHandlers(deps) {
|
|
|
17497
17538
|
if (rule.ownerUserId !== void 0 && rule.ownerUserId !== caller.userId) throw new Error(`forbidden: rule not owned: ${ruleId}`);
|
|
17498
17539
|
return rule;
|
|
17499
17540
|
};
|
|
17541
|
+
/**
|
|
17542
|
+
* TEST a STATE-SUMMARY rule (D698). The per-event injection cannot deliver
|
|
17543
|
+
* for it — a summary rule sends nothing per event (D647), and that test used
|
|
17544
|
+
* to be dropped without a word — so the summary is built NOW and enqueued.
|
|
17545
|
+
* The answer is shaped like every rule test's, plus the summary's own report.
|
|
17546
|
+
*/
|
|
17547
|
+
const testSummaryRule = async (rule, by) => {
|
|
17548
|
+
const test = deps.testStateSummary;
|
|
17549
|
+
if (test === void 0) throw new Error("this node does not host the notification centre — test on the hub");
|
|
17550
|
+
deps.logger.info("rule test requested", { meta: {
|
|
17551
|
+
by,
|
|
17552
|
+
ruleId: rule.id,
|
|
17553
|
+
ruleName: rule.name,
|
|
17554
|
+
trigger: "state-summary",
|
|
17555
|
+
bypassMute: true
|
|
17556
|
+
} });
|
|
17557
|
+
const report = await test(rule);
|
|
17558
|
+
const enqueued = report.rows.reduce((sum, row) => sum + row.enqueued, 0);
|
|
17559
|
+
const notes = ["per-camera notification mute bypassed"];
|
|
17560
|
+
if (report.nothingToReport) notes.push(`nothing to report — ${report.sections.map((section) => `${section.id}: ${section.status === "read" ? `${String(section.items)} open` : "unavailable"}`).join(", ")} — nothing was sent, as the scheduled summary would send nothing`);
|
|
17561
|
+
else if (enqueued === 0) notes.push("the summary was built but no row was enqueued — the rule reaches no target");
|
|
17562
|
+
return {
|
|
17563
|
+
recordId: report.recordId,
|
|
17564
|
+
origin: "synthetic",
|
|
17565
|
+
delivery: rule.delivery,
|
|
17566
|
+
candidates: 1,
|
|
17567
|
+
outcomes: [{
|
|
17568
|
+
ruleId: rule.id,
|
|
17569
|
+
ruleName: rule.name,
|
|
17570
|
+
matched: !report.nothingToReport,
|
|
17571
|
+
cooldown: "bypassed",
|
|
17572
|
+
enqueued,
|
|
17573
|
+
targetIds: [...new Set(report.rows.flatMap((row) => row.targetIds))]
|
|
17574
|
+
}],
|
|
17575
|
+
still: "unattachable",
|
|
17576
|
+
notes,
|
|
17577
|
+
...report.nothingToReport ? { droppedBy: "nothing-to-report" } : {},
|
|
17578
|
+
trigger: "state-summary",
|
|
17579
|
+
planNotes: [],
|
|
17580
|
+
stateSummary: report
|
|
17581
|
+
};
|
|
17582
|
+
};
|
|
17500
17583
|
/** The producer half, or an honest refusal. */
|
|
17501
17584
|
const requireTimelapse = () => {
|
|
17502
17585
|
const tl = deps.timelapse;
|
|
@@ -17831,6 +17914,7 @@ function makeNcActionHandlers(deps) {
|
|
|
17831
17914
|
const inject = deps.injectTestEvent;
|
|
17832
17915
|
if (inject === void 0) throw new Error("this node does not host the notification centre — test on the hub");
|
|
17833
17916
|
const rule = assertRuleVisible(input.ruleId, c);
|
|
17917
|
+
if (require_clip_model_registry.isSystemEventSummaryRule(rule)) return testSummaryRule(rule, c.userId);
|
|
17834
17918
|
const plan = planRuleTest({
|
|
17835
17919
|
rule,
|
|
17836
17920
|
now: Date.now(),
|
|
@@ -19665,6 +19749,77 @@ var NcDeviceStateCache = class {
|
|
|
19665
19749
|
}
|
|
19666
19750
|
};
|
|
19667
19751
|
//#endregion
|
|
19752
|
+
//#region src/notification-center/state-summary/state-model.ts
|
|
19753
|
+
/**
|
|
19754
|
+
* The STATE SUMMARY's vocabulary (D647): its sections, and what one reported
|
|
19755
|
+
* item is.
|
|
19756
|
+
*
|
|
19757
|
+
* A section is one of the D637 kind GROUPS (`NC_SYSTEM_EVENT_KIND_GROUP_SPECS`)
|
|
19758
|
+
* — the same grouping both editors list kinds under — so "the rule selected a
|
|
19759
|
+
* battery kind" and "the summary has a battery section" are the same fact.
|
|
19760
|
+
* `backups-exports` is the one group with no section: a backup or an export is
|
|
19761
|
+
* a job that COMPLETED, and the state it leaves behind is not a condition that
|
|
19762
|
+
* is still true. The ADR records why it is excluded rather than approximated.
|
|
19763
|
+
*/
|
|
19764
|
+
var NC_STATE_SUMMARY_SECTION_IDS = [
|
|
19765
|
+
"device-state",
|
|
19766
|
+
"streams",
|
|
19767
|
+
"battery",
|
|
19768
|
+
"consumables",
|
|
19769
|
+
"tanks",
|
|
19770
|
+
"detection-blind",
|
|
19771
|
+
"nodes",
|
|
19772
|
+
"addons",
|
|
19773
|
+
"updates"
|
|
19774
|
+
];
|
|
19775
|
+
/** The kind groups that have no section, and why — said on the preview. */
|
|
19776
|
+
var NC_STATE_SUMMARY_EXCLUDED_GROUPS = { "backups-exports": "a backup or an export is a completed job, not a state that is still true" };
|
|
19777
|
+
function isStateSectionId(value) {
|
|
19778
|
+
return NC_STATE_SUMMARY_SECTION_IDS.some((id) => id === value);
|
|
19779
|
+
}
|
|
19780
|
+
/** The template variable that carries one section's lines (`template-vars.ts`). */
|
|
19781
|
+
var NC_STATE_SECTION_VAR = {
|
|
19782
|
+
"device-state": "summaryDevices",
|
|
19783
|
+
streams: "summaryStreams",
|
|
19784
|
+
battery: "summaryBattery",
|
|
19785
|
+
consumables: "summaryConsumables",
|
|
19786
|
+
tanks: "summaryTanks",
|
|
19787
|
+
"detection-blind": "summaryDetection",
|
|
19788
|
+
nodes: "summaryNodes",
|
|
19789
|
+
addons: "summaryAddons",
|
|
19790
|
+
updates: "summaryUpdates"
|
|
19791
|
+
};
|
|
19792
|
+
/** The kinds of a section — its kind group. */
|
|
19793
|
+
function sectionKinds(section) {
|
|
19794
|
+
return require_clip_model_registry.NC_SYSTEM_EVENT_KIND_GROUP_SPECS.find((group) => group.id === section)?.kinds ?? [];
|
|
19795
|
+
}
|
|
19796
|
+
/**
|
|
19797
|
+
* The sections a rule reports, in section order: every section whose kind
|
|
19798
|
+
* group shares a kind with the rule. A rule that selected only
|
|
19799
|
+
* `device-battery-low` still reports the battery STATE — the state has no
|
|
19800
|
+
* direction, only the event did.
|
|
19801
|
+
*/
|
|
19802
|
+
function sectionsOfRule(condition) {
|
|
19803
|
+
return NC_STATE_SUMMARY_SECTION_IDS.filter((section) => sectionKinds(section).some((kind) => condition.kinds.includes(kind)));
|
|
19804
|
+
}
|
|
19805
|
+
/** The kind groups the rule selected that have no section. */
|
|
19806
|
+
function excludedGroupsOfRule(condition) {
|
|
19807
|
+
return require_clip_model_registry.NC_SYSTEM_EVENT_KIND_GROUP_SPECS.filter((group) => NC_STATE_SUMMARY_EXCLUDED_GROUPS[group.id] !== void 0 && group.kinds.some((kind) => condition.kinds.includes(kind))).map((group) => group.id);
|
|
19808
|
+
}
|
|
19809
|
+
/**
|
|
19810
|
+
* The condition the summary judges its items with: the rule's own, with its
|
|
19811
|
+
* kinds widened to every kind of every section it reports — so a rule that
|
|
19812
|
+
* named the raise is not told "not my kind" about the state the raise left
|
|
19813
|
+
* behind. Every FILTER is the rule's, untouched.
|
|
19814
|
+
*/
|
|
19815
|
+
function summaryCondition(condition) {
|
|
19816
|
+
const kinds = sectionsOfRule(condition).flatMap((section) => sectionKinds(section));
|
|
19817
|
+
return {
|
|
19818
|
+
...condition,
|
|
19819
|
+
kinds: [...new Set([...condition.kinds, ...kinds])]
|
|
19820
|
+
};
|
|
19821
|
+
}
|
|
19822
|
+
//#endregion
|
|
19668
19823
|
//#region src/notification-center/state-summary/state-summary-delivery.ts
|
|
19669
19824
|
/**
|
|
19670
19825
|
* A state summary as an ORDINARY system-event subject (D647), so it reaches the
|
|
@@ -19673,7 +19828,9 @@ var NcDeviceStateCache = class {
|
|
|
19673
19828
|
* which renders the rule's own template with the summary's variables.
|
|
19674
19829
|
*
|
|
19675
19830
|
* - `recordId` is `state-summary:<ruleId>:<windowEndMs>` — the outbox dedup
|
|
19676
|
-
* key, so a restart can never enqueue one window twice.
|
|
19831
|
+
* key, so a restart can never enqueue one window twice. Split per SECTION
|
|
19832
|
+
* (D698), each row is `state-summary:<ruleId>:<windowEndMs>:<sectionId>`:
|
|
19833
|
+
* the same guarantee, per section.
|
|
19677
19834
|
* - The row files under the system device (0): a summary spans devices.
|
|
19678
19835
|
* `targetUsers` therefore reach admins only — a user whose grant is
|
|
19679
19836
|
* narrower than the whole installation must not be sent the names of
|
|
@@ -19687,20 +19844,37 @@ var NcDeviceStateCache = class {
|
|
|
19687
19844
|
*/
|
|
19688
19845
|
var NC_STATE_SUMMARY_TITLE_KEY = "system.state-summary.title";
|
|
19689
19846
|
var NC_STATE_SUMMARY_BODY_KEY = "system.state-summary.body";
|
|
19847
|
+
/** A per-section row's title and body (D698): the rule's name and the section's. */
|
|
19848
|
+
var NC_STATE_SUMMARY_SECTION_TITLE_KEY = "system.state-summary.section.title";
|
|
19849
|
+
var NC_STATE_SUMMARY_SECTION_BODY_KEY = "system.state-summary.section.body";
|
|
19690
19850
|
function stateSummaryRecordId(ruleId, windowEndMs) {
|
|
19691
19851
|
return `state-summary:${ruleId}:${String(windowEndMs)}`;
|
|
19692
19852
|
}
|
|
19853
|
+
/**
|
|
19854
|
+
* The record id of a TEST of the rule (D698, `nc.testRule`): its own prefix,
|
|
19855
|
+
* so a test can never take — or be swallowed by — a real window's dedup key.
|
|
19856
|
+
*/
|
|
19857
|
+
function stateSummaryTestRecordId(ruleId, nowMs) {
|
|
19858
|
+
return `state-summary-test:${ruleId}:${String(nowMs)}`;
|
|
19859
|
+
}
|
|
19860
|
+
/** One section's row: the record id of the whole, plus `:<sectionId>` — the per-section dedup key. */
|
|
19861
|
+
function sectionRecordId(base, sectionId) {
|
|
19862
|
+
return `${base}:${sectionId}`;
|
|
19863
|
+
}
|
|
19693
19864
|
/** Is this system-event data the data of a state-summary row? */
|
|
19694
19865
|
function isStateSummaryData(data) {
|
|
19695
19866
|
return data["stateSummary"] === true;
|
|
19696
19867
|
}
|
|
19868
|
+
function recordBaseOf(input) {
|
|
19869
|
+
return input.recordBase ?? stateSummaryRecordId(input.rule.id, input.window.endMs);
|
|
19870
|
+
}
|
|
19697
19871
|
/** `null` when the rule has no system-event condition (the guard refuses that). */
|
|
19698
19872
|
function stateSummarySubject(input) {
|
|
19699
19873
|
const kind = input.rule.conditions.systemEvent?.kinds[0];
|
|
19700
19874
|
if (kind === void 0) return null;
|
|
19701
19875
|
return {
|
|
19702
19876
|
kind: "system-event",
|
|
19703
|
-
recordId:
|
|
19877
|
+
recordId: recordBaseOf(input),
|
|
19704
19878
|
deviceId: NC_SYSTEM_DEVICE_ID$1,
|
|
19705
19879
|
timestamp: input.window.endMs,
|
|
19706
19880
|
classNames: [],
|
|
@@ -19713,6 +19887,7 @@ function stateSummarySubject(input) {
|
|
|
19713
19887
|
textParams: {
|
|
19714
19888
|
summaryText: input.text.text,
|
|
19715
19889
|
summaryCount: String(input.text.count),
|
|
19890
|
+
summarySection: input.text.sections.map((section) => section.title).join(", "),
|
|
19716
19891
|
...input.text.sectionVars
|
|
19717
19892
|
},
|
|
19718
19893
|
textCount: input.text.count,
|
|
@@ -19730,13 +19905,82 @@ function stateSummarySubject(input) {
|
|
|
19730
19905
|
};
|
|
19731
19906
|
}
|
|
19732
19907
|
/**
|
|
19908
|
+
* The rows one summary window sends. `rule`: the one D647 row. `section`: one
|
|
19909
|
+
* row per section with something to say — its items, or the sentence that it
|
|
19910
|
+
* is UNAVAILABLE, exactly as the combined text words it. A section with
|
|
19911
|
+
* nothing open sends nothing; neither does a summary with nothing at all.
|
|
19912
|
+
*/
|
|
19913
|
+
function stateSummarySubjects(input) {
|
|
19914
|
+
if (input.split === "rule") {
|
|
19915
|
+
const subject = stateSummarySubject(input);
|
|
19916
|
+
return subject === null ? [] : [subject];
|
|
19917
|
+
}
|
|
19918
|
+
const kind = input.rule.conditions.systemEvent?.kinds[0];
|
|
19919
|
+
if (kind === void 0) return [];
|
|
19920
|
+
return input.text.sections.map((section) => sectionSubject(input, kind, section));
|
|
19921
|
+
}
|
|
19922
|
+
function sectionSubject(input, kind, section) {
|
|
19923
|
+
const sectionVars = {};
|
|
19924
|
+
for (const name of Object.keys(input.text.sectionVars)) sectionVars[name] = "";
|
|
19925
|
+
sectionVars[NC_STATE_SECTION_VAR[section.id]] = section.text;
|
|
19926
|
+
return {
|
|
19927
|
+
kind: "system-event",
|
|
19928
|
+
recordId: sectionRecordId(recordBaseOf(input), section.id),
|
|
19929
|
+
deviceId: NC_SYSTEM_DEVICE_ID$1,
|
|
19930
|
+
timestamp: input.window.endMs,
|
|
19931
|
+
classNames: [],
|
|
19932
|
+
zones: [],
|
|
19933
|
+
systemEvent: {
|
|
19934
|
+
kind,
|
|
19935
|
+
subject: `state-summary:${input.rule.id}:${section.id}`,
|
|
19936
|
+
titleKey: NC_STATE_SUMMARY_SECTION_TITLE_KEY,
|
|
19937
|
+
bodyKey: NC_STATE_SUMMARY_SECTION_BODY_KEY,
|
|
19938
|
+
textParams: {
|
|
19939
|
+
summaryText: section.body,
|
|
19940
|
+
summaryCount: String(section.count),
|
|
19941
|
+
summarySection: section.title,
|
|
19942
|
+
...sectionVars
|
|
19943
|
+
},
|
|
19944
|
+
textCount: section.count,
|
|
19945
|
+
data: {
|
|
19946
|
+
stateSummary: true,
|
|
19947
|
+
section: section.id,
|
|
19948
|
+
windowStartMs: input.window.startMs,
|
|
19949
|
+
windowEndMs: input.window.endMs,
|
|
19950
|
+
sections: [{
|
|
19951
|
+
id: section.id,
|
|
19952
|
+
status: section.status,
|
|
19953
|
+
items: section.count
|
|
19954
|
+
}]
|
|
19955
|
+
}
|
|
19956
|
+
}
|
|
19957
|
+
};
|
|
19958
|
+
}
|
|
19959
|
+
/** The section a per-section row is about (`data.section`), or `undefined` on a combined row. */
|
|
19960
|
+
function stateSummarySectionOf(subject) {
|
|
19961
|
+
const section = subject.systemEvent?.data["section"];
|
|
19962
|
+
return typeof section === "string" && isStateSectionId(section) ? section : void 0;
|
|
19963
|
+
}
|
|
19964
|
+
/** Does a body name `{{summaryText}}` or this section's own variable? */
|
|
19965
|
+
function bodyCarriesSection(body, section) {
|
|
19966
|
+
return new RegExp(`\\{\\{\\s*(?:summaryText|${NC_STATE_SECTION_VAR[section]})\\s*\\}\\}`).test(body);
|
|
19967
|
+
}
|
|
19968
|
+
/**
|
|
19733
19969
|
* The rule as its summary is ENQUEUED: a custom body that carries none of the
|
|
19734
19970
|
* summary's content gets `{{summaryText}}` appended on its own line, so an
|
|
19735
19971
|
* operator's body never sends an empty summary (review M3). `null` = unchanged.
|
|
19972
|
+
*
|
|
19973
|
+
* On a per-SECTION row (D698 review M3) the content is THIS section's: a body
|
|
19974
|
+
* naming only `{{summaryBattery}}` carries the battery row and nothing on the
|
|
19975
|
+
* streams row, so every other section's row gets `{{summaryText}}` — its own
|
|
19976
|
+
* lines — appended.
|
|
19736
19977
|
*/
|
|
19737
|
-
function withSummaryBody(rule) {
|
|
19978
|
+
function withSummaryBody(rule, section) {
|
|
19738
19979
|
const body = rule.template?.body;
|
|
19739
|
-
if (require_clip_model_registry.templateCarriesSummary(body))
|
|
19980
|
+
if (require_clip_model_registry.templateCarriesSummary(body)) {
|
|
19981
|
+
if (section === void 0 || body === void 0 || body.trim().length === 0) return null;
|
|
19982
|
+
if (bodyCarriesSection(body, section)) return null;
|
|
19983
|
+
}
|
|
19740
19984
|
return {
|
|
19741
19985
|
...rule,
|
|
19742
19986
|
template: {
|
|
@@ -23785,6 +24029,120 @@ function withTimeout$3(promise, ms, what) {
|
|
|
23785
24029
|
});
|
|
23786
24030
|
}
|
|
23787
24031
|
//#endregion
|
|
24032
|
+
//#region src/notification-center/boot-window-rows.ts
|
|
24033
|
+
var BootWindowRows = class {
|
|
24034
|
+
deps;
|
|
24035
|
+
window = /* @__PURE__ */ new Map();
|
|
24036
|
+
/** Deferred closes, keyed so a repeat (one per detection) replaces rather than grows. */
|
|
24037
|
+
deferred = /* @__PURE__ */ new Map();
|
|
24038
|
+
settled = false;
|
|
24039
|
+
constructor(deps) {
|
|
24040
|
+
this.deps = deps;
|
|
24041
|
+
}
|
|
24042
|
+
/** Is the ledger still inside its boot window (not settled after an answered load)? */
|
|
24043
|
+
open() {
|
|
24044
|
+
return !this.settled;
|
|
24045
|
+
}
|
|
24046
|
+
/** Stage a row that would be CREATED inside the window — mirror only, never persisted. */
|
|
24047
|
+
stageProvisional(row) {
|
|
24048
|
+
this.deps.ledger.stage(row);
|
|
24049
|
+
this.window.set(this.deps.keyOf(row), {
|
|
24050
|
+
kind: "provisional",
|
|
24051
|
+
row
|
|
24052
|
+
});
|
|
24053
|
+
}
|
|
24054
|
+
/** A row the ledger persisted inside the window: the load must not regress it. */
|
|
24055
|
+
noteWritten(row) {
|
|
24056
|
+
this.window.set(this.deps.keyOf(row), {
|
|
24057
|
+
kind: "written",
|
|
24058
|
+
row
|
|
24059
|
+
});
|
|
24060
|
+
}
|
|
24061
|
+
/** A key the ledger removed inside the window: the load must not resurrect it. */
|
|
24062
|
+
noteRemoved(key) {
|
|
24063
|
+
this.window.set(key, { kind: "removed" });
|
|
24064
|
+
}
|
|
24065
|
+
/**
|
|
24066
|
+
* While the window is open, keep `replay` to run against the LOADED rows at
|
|
24067
|
+
* the settle; once settled, nothing — the caller's own pass already saw every
|
|
24068
|
+
* row (review LOW-5: no allocation per detection after boot). `key` names the
|
|
24069
|
+
* close: the same key deferred twice runs once, at the place it was first
|
|
24070
|
+
* deferred. `replay` answers how many rows it closed.
|
|
24071
|
+
*/
|
|
24072
|
+
deferWhileOpen(key, replay) {
|
|
24073
|
+
if (this.open()) this.deferred.set(key, replay);
|
|
24074
|
+
}
|
|
24075
|
+
/**
|
|
24076
|
+
* Close the window — call right after the ledger's `load`. A load that did
|
|
24077
|
+
* not answer leaves the window open and returns `null`.
|
|
24078
|
+
*/
|
|
24079
|
+
settle() {
|
|
24080
|
+
if (this.settled) return null;
|
|
24081
|
+
if (!this.deps.ledger.hasAnswered()) {
|
|
24082
|
+
this.deps.logger.warn("boot window NOT settled — the load did not answer", { meta: {
|
|
24083
|
+
collection: this.deps.ledger.collection,
|
|
24084
|
+
pendingCloses: this.deferred.size,
|
|
24085
|
+
provisionalRows: this.window.size
|
|
24086
|
+
} });
|
|
24087
|
+
return null;
|
|
24088
|
+
}
|
|
24089
|
+
this.settled = true;
|
|
24090
|
+
const ledger = this.deps.ledger;
|
|
24091
|
+
let replayed = 0;
|
|
24092
|
+
for (const replay of this.deferred.values()) replayed += replay();
|
|
24093
|
+
this.deferred.clear();
|
|
24094
|
+
let keptStored = 0;
|
|
24095
|
+
let written = 0;
|
|
24096
|
+
for (const [key, entry] of this.window) {
|
|
24097
|
+
if (entry.kind === "removed") {
|
|
24098
|
+
ledger.forget(key);
|
|
24099
|
+
continue;
|
|
24100
|
+
}
|
|
24101
|
+
if (entry.kind === "written") {
|
|
24102
|
+
ledger.stage(entry.row);
|
|
24103
|
+
continue;
|
|
24104
|
+
}
|
|
24105
|
+
const mirrored = ledger.get(key);
|
|
24106
|
+
const stored = mirrored === entry.row ? void 0 : mirrored;
|
|
24107
|
+
const held = this.deps.merge(stored, entry.row);
|
|
24108
|
+
const deviceId = this.deps.deviceIdOf(entry.row);
|
|
24109
|
+
const tags = deviceId !== void 0 ? { tags: { deviceId } } : {};
|
|
24110
|
+
if (stored !== void 0 && held === stored) {
|
|
24111
|
+
keptStored += 1;
|
|
24112
|
+
this.deps.logger.debug("boot-window row kept as stored — the restart learned nothing new", {
|
|
24113
|
+
...tags,
|
|
24114
|
+
meta: {
|
|
24115
|
+
collection: ledger.collection,
|
|
24116
|
+
key
|
|
24117
|
+
}
|
|
24118
|
+
});
|
|
24119
|
+
continue;
|
|
24120
|
+
}
|
|
24121
|
+
ledger.put(held);
|
|
24122
|
+
written += 1;
|
|
24123
|
+
this.deps.logger.debug("boot-window row written at load", {
|
|
24124
|
+
...tags,
|
|
24125
|
+
meta: {
|
|
24126
|
+
collection: ledger.collection,
|
|
24127
|
+
key,
|
|
24128
|
+
stored: stored !== void 0
|
|
24129
|
+
}
|
|
24130
|
+
});
|
|
24131
|
+
}
|
|
24132
|
+
this.window.clear();
|
|
24133
|
+
const merge = {
|
|
24134
|
+
keptStored,
|
|
24135
|
+
written,
|
|
24136
|
+
replayed
|
|
24137
|
+
};
|
|
24138
|
+
this.deps.logger.info("boot window settled", { meta: {
|
|
24139
|
+
collection: ledger.collection,
|
|
24140
|
+
...merge
|
|
24141
|
+
} });
|
|
24142
|
+
return merge;
|
|
24143
|
+
}
|
|
24144
|
+
};
|
|
24145
|
+
//#endregion
|
|
23788
24146
|
//#region src/notification-center/liveness-ledger.ts
|
|
23789
24147
|
/**
|
|
23790
24148
|
* @durable class=ledger owner=notification-center
|
|
@@ -23895,12 +24253,26 @@ var NC_LIVENESS_SPEC = {
|
|
|
23895
24253
|
};
|
|
23896
24254
|
var NcLivenessLedger = class {
|
|
23897
24255
|
ledger;
|
|
24256
|
+
/**
|
|
24257
|
+
* What was observed before the boot load answered (D698) — see
|
|
24258
|
+
* `boot-window-rows.ts`. A stored row holding the same STATE keeps its own
|
|
24259
|
+
* `updatedAt`: a restart that re-hears "offline" learned nothing about WHEN
|
|
24260
|
+
* the device went offline, and `updatedAt` is the state summary's "since".
|
|
24261
|
+
*/
|
|
24262
|
+
bootWindow;
|
|
23898
24263
|
constructor(deps) {
|
|
23899
24264
|
this.ledger = new DurableLedger({
|
|
23900
24265
|
spec: NC_LIVENESS_SPEC,
|
|
23901
24266
|
store: deps.store,
|
|
23902
24267
|
logger: deps.logger
|
|
23903
24268
|
});
|
|
24269
|
+
this.bootWindow = new BootWindowRows({
|
|
24270
|
+
ledger: this.ledger,
|
|
24271
|
+
keyOf: (row) => row.subject,
|
|
24272
|
+
deviceIdOf: (row) => row.deviceId,
|
|
24273
|
+
logger: deps.logger,
|
|
24274
|
+
merge: (stored, provisional) => stored !== void 0 && stored.state === provisional.state ? stored : provisional
|
|
24275
|
+
});
|
|
23904
24276
|
}
|
|
23905
24277
|
static declare(store) {
|
|
23906
24278
|
return DurableLedger.declare(store, NC_LIVENESS_SPEC);
|
|
@@ -23915,7 +24287,9 @@ var NcLivenessLedger = class {
|
|
|
23915
24287
|
* flood, so no error path may do it on purpose.
|
|
23916
24288
|
*/
|
|
23917
24289
|
async load() {
|
|
23918
|
-
|
|
24290
|
+
const loaded = (await this.ledger.load()).length;
|
|
24291
|
+
this.bootWindow.settle();
|
|
24292
|
+
return loaded;
|
|
23919
24293
|
}
|
|
23920
24294
|
/** Has a load READ the store once? Until then the mirror is not the truth (D647 reads it). */
|
|
23921
24295
|
hasAnswered() {
|
|
@@ -24008,13 +24382,21 @@ var NcLivenessLedger = class {
|
|
|
24008
24382
|
* spoke". That is the timestamp anyone reading the table wants.
|
|
24009
24383
|
*/
|
|
24010
24384
|
observe(observation, now) {
|
|
24011
|
-
|
|
24385
|
+
const row = {
|
|
24012
24386
|
subject: observation.subject,
|
|
24013
24387
|
state: observation.state,
|
|
24014
24388
|
kind: observation.kind,
|
|
24015
24389
|
...observation.deviceId !== void 0 ? { deviceId: observation.deviceId } : {},
|
|
24016
24390
|
updatedAt: now
|
|
24017
|
-
}
|
|
24391
|
+
};
|
|
24392
|
+
if (!this.bootWindow.open()) return this.ledger.observe(row);
|
|
24393
|
+
if (this.ledger.get(row.subject) === void 0) {
|
|
24394
|
+
this.bootWindow.stageProvisional(row);
|
|
24395
|
+
return "seeded";
|
|
24396
|
+
}
|
|
24397
|
+
const verdict = this.ledger.observe(row);
|
|
24398
|
+
if (verdict === "flip") this.bootWindow.noteWritten(row);
|
|
24399
|
+
return verdict;
|
|
24018
24400
|
}
|
|
24019
24401
|
};
|
|
24020
24402
|
/** The persisted column map (the `subject` PK is passed separately). */
|
|
@@ -30549,74 +30931,6 @@ function stateSources(ports) {
|
|
|
30549
30931
|
return { read: (section, nowMs) => readSection(section, ports, nowMs) };
|
|
30550
30932
|
}
|
|
30551
30933
|
//#endregion
|
|
30552
|
-
//#region src/notification-center/state-summary/state-model.ts
|
|
30553
|
-
/**
|
|
30554
|
-
* The STATE SUMMARY's vocabulary (D647): its sections, and what one reported
|
|
30555
|
-
* item is.
|
|
30556
|
-
*
|
|
30557
|
-
* A section is one of the D637 kind GROUPS (`NC_SYSTEM_EVENT_KIND_GROUP_SPECS`)
|
|
30558
|
-
* — the same grouping both editors list kinds under — so "the rule selected a
|
|
30559
|
-
* battery kind" and "the summary has a battery section" are the same fact.
|
|
30560
|
-
* `backups-exports` is the one group with no section: a backup or an export is
|
|
30561
|
-
* a job that COMPLETED, and the state it leaves behind is not a condition that
|
|
30562
|
-
* is still true. The ADR records why it is excluded rather than approximated.
|
|
30563
|
-
*/
|
|
30564
|
-
var NC_STATE_SUMMARY_SECTION_IDS = [
|
|
30565
|
-
"device-state",
|
|
30566
|
-
"streams",
|
|
30567
|
-
"battery",
|
|
30568
|
-
"consumables",
|
|
30569
|
-
"tanks",
|
|
30570
|
-
"detection-blind",
|
|
30571
|
-
"nodes",
|
|
30572
|
-
"addons",
|
|
30573
|
-
"updates"
|
|
30574
|
-
];
|
|
30575
|
-
/** The kind groups that have no section, and why — said on the preview. */
|
|
30576
|
-
var NC_STATE_SUMMARY_EXCLUDED_GROUPS = { "backups-exports": "a backup or an export is a completed job, not a state that is still true" };
|
|
30577
|
-
/** The template variable that carries one section's lines (`template-vars.ts`). */
|
|
30578
|
-
var NC_STATE_SECTION_VAR = {
|
|
30579
|
-
"device-state": "summaryDevices",
|
|
30580
|
-
streams: "summaryStreams",
|
|
30581
|
-
battery: "summaryBattery",
|
|
30582
|
-
consumables: "summaryConsumables",
|
|
30583
|
-
tanks: "summaryTanks",
|
|
30584
|
-
"detection-blind": "summaryDetection",
|
|
30585
|
-
nodes: "summaryNodes",
|
|
30586
|
-
addons: "summaryAddons",
|
|
30587
|
-
updates: "summaryUpdates"
|
|
30588
|
-
};
|
|
30589
|
-
/** The kinds of a section — its kind group. */
|
|
30590
|
-
function sectionKinds(section) {
|
|
30591
|
-
return require_clip_model_registry.NC_SYSTEM_EVENT_KIND_GROUP_SPECS.find((group) => group.id === section)?.kinds ?? [];
|
|
30592
|
-
}
|
|
30593
|
-
/**
|
|
30594
|
-
* The sections a rule reports, in section order: every section whose kind
|
|
30595
|
-
* group shares a kind with the rule. A rule that selected only
|
|
30596
|
-
* `device-battery-low` still reports the battery STATE — the state has no
|
|
30597
|
-
* direction, only the event did.
|
|
30598
|
-
*/
|
|
30599
|
-
function sectionsOfRule(condition) {
|
|
30600
|
-
return NC_STATE_SUMMARY_SECTION_IDS.filter((section) => sectionKinds(section).some((kind) => condition.kinds.includes(kind)));
|
|
30601
|
-
}
|
|
30602
|
-
/** The kind groups the rule selected that have no section. */
|
|
30603
|
-
function excludedGroupsOfRule(condition) {
|
|
30604
|
-
return require_clip_model_registry.NC_SYSTEM_EVENT_KIND_GROUP_SPECS.filter((group) => NC_STATE_SUMMARY_EXCLUDED_GROUPS[group.id] !== void 0 && group.kinds.some((kind) => condition.kinds.includes(kind))).map((group) => group.id);
|
|
30605
|
-
}
|
|
30606
|
-
/**
|
|
30607
|
-
* The condition the summary judges its items with: the rule's own, with its
|
|
30608
|
-
* kinds widened to every kind of every section it reports — so a rule that
|
|
30609
|
-
* named the raise is not told "not my kind" about the state the raise left
|
|
30610
|
-
* behind. Every FILTER is the rule's, untouched.
|
|
30611
|
-
*/
|
|
30612
|
-
function summaryCondition(condition) {
|
|
30613
|
-
const kinds = sectionsOfRule(condition).flatMap((section) => sectionKinds(section));
|
|
30614
|
-
return {
|
|
30615
|
-
...condition,
|
|
30616
|
-
kinds: [...new Set([...condition.kinds, ...kinds])]
|
|
30617
|
-
};
|
|
30618
|
-
}
|
|
30619
|
-
//#endregion
|
|
30620
30934
|
//#region src/notification-center/state-summary/state-summary.ts
|
|
30621
30935
|
/** Nothing to say: no item, and every section READ. Such a summary is not sent. */
|
|
30622
30936
|
function isEmptyStateSummary(summary) {
|
|
@@ -30717,28 +31031,50 @@ function lineOf(input, item) {
|
|
|
30717
31031
|
}
|
|
30718
31032
|
});
|
|
30719
31033
|
}
|
|
30720
|
-
function
|
|
30721
|
-
|
|
30722
|
-
|
|
30723
|
-
|
|
30724
|
-
|
|
30725
|
-
|
|
31034
|
+
function sectionTitle(input, section) {
|
|
31035
|
+
return input.texts.text({ key: `summary.section.${section.id}` });
|
|
31036
|
+
}
|
|
31037
|
+
/** A section's words: its block, and the body a per-section row sends. `null` = nothing to say. */
|
|
31038
|
+
function wordsOf(input, section) {
|
|
31039
|
+
const title = sectionTitle(input, section);
|
|
31040
|
+
if (section.status === "unavailable") {
|
|
31041
|
+
const sentence = input.texts.text({
|
|
31042
|
+
key: "summary.section.unavailable",
|
|
31043
|
+
vars: { section: title }
|
|
31044
|
+
});
|
|
31045
|
+
return {
|
|
31046
|
+
block: sentence,
|
|
31047
|
+
body: sentence
|
|
31048
|
+
};
|
|
31049
|
+
}
|
|
30726
31050
|
if (section.items.length === 0) return null;
|
|
30727
|
-
const lines = section.items.map((item) => `• ${lineOf(input, item)}`);
|
|
30728
|
-
return
|
|
31051
|
+
const lines = section.items.map((item) => `• ${lineOf(input, item)}`).join("\n");
|
|
31052
|
+
return {
|
|
31053
|
+
block: `${title}:\n${lines}`,
|
|
31054
|
+
body: lines
|
|
31055
|
+
};
|
|
30729
31056
|
}
|
|
30730
31057
|
function stateSummaryText(input) {
|
|
30731
31058
|
const sectionVars = {};
|
|
30732
|
-
const
|
|
31059
|
+
const sections = [];
|
|
30733
31060
|
for (const section of input.summary.sections) {
|
|
30734
|
-
const
|
|
30735
|
-
sectionVars[NC_STATE_SECTION_VAR[section.id]] = block ?? "";
|
|
30736
|
-
if (
|
|
31061
|
+
const words = wordsOf(input, section);
|
|
31062
|
+
sectionVars[NC_STATE_SECTION_VAR[section.id]] = words?.block ?? "";
|
|
31063
|
+
if (words === null) continue;
|
|
31064
|
+
sections.push({
|
|
31065
|
+
id: section.id,
|
|
31066
|
+
title: sectionTitle(input, section),
|
|
31067
|
+
text: words.block,
|
|
31068
|
+
body: words.body,
|
|
31069
|
+
count: section.items.length,
|
|
31070
|
+
status: section.status
|
|
31071
|
+
});
|
|
30737
31072
|
}
|
|
30738
31073
|
return {
|
|
30739
|
-
text:
|
|
31074
|
+
text: sections.map((section) => section.text).join("\n"),
|
|
30740
31075
|
count: input.summary.itemCount,
|
|
30741
|
-
sectionVars
|
|
31076
|
+
sectionVars,
|
|
31077
|
+
sections
|
|
30742
31078
|
};
|
|
30743
31079
|
}
|
|
30744
31080
|
//#endregion
|
|
@@ -31175,6 +31511,40 @@ var NcStateSummaryStampStore = class {
|
|
|
31175
31511
|
//#endregion
|
|
31176
31512
|
//#region src/notification-center/state-summary/system-state-ledger.ts
|
|
31177
31513
|
/**
|
|
31514
|
+
* NcSystemStateLedger — what is STILL TRUE among the event-driven system facts
|
|
31515
|
+
* no other ledger holds (D647).
|
|
31516
|
+
*
|
|
31517
|
+
* Battery, consumable and tank episodes already have a durable row
|
|
31518
|
+
* (`alert-store.ts`), and device/node liveness has its own
|
|
31519
|
+
* (`liveness-ledger.ts`). The rest of the summarisable kinds are EVENTS the
|
|
31520
|
+
* centre hears once — a stream going offline, a device switched off, a camera
|
|
31521
|
+
* detecting nothing, an update becoming available — and nothing remembered
|
|
31522
|
+
* that they were still true. A state summary is a claim about now, so this
|
|
31523
|
+
* ledger keeps one row per OPEN state, fed by the same intake the instant
|
|
31524
|
+
* rules read, and removes it when the closing event arrives:
|
|
31525
|
+
*
|
|
31526
|
+
* | opens | closes |
|
|
31527
|
+
* | --- | --- |
|
|
31528
|
+
* | `stream-offline` | `stream-online` (same subject) |
|
|
31529
|
+
* | `device-disabled` | `device-enabled` (same subject) |
|
|
31530
|
+
* | `detection-blind` | a DETECTION on that camera — the only evidence of recovery there is |
|
|
31531
|
+
* | `node-inference-unavailable`, `addon-crash-loop` | the node's next liveness edge, or a server update on it |
|
|
31532
|
+
* | `addon-update-available` (per package, per node) | `addon.updated` for that package on that node |
|
|
31533
|
+
* | `server-` / `wrapper-update-available` | `server-updated` on that node |
|
|
31534
|
+
*
|
|
31535
|
+
* Two kinds have no recovery event at all (`node-inference-unavailable`,
|
|
31536
|
+
* `addon-crash-loop`): the orchestrator drops such a node until it comes back,
|
|
31537
|
+
* and a crash-looped runner stays down until something respawns it — a node
|
|
31538
|
+
* restart or an update. The closing edges above are those respawns. A newer
|
|
31539
|
+
* `update-available` for the same package REPLACES the row.
|
|
31540
|
+
*
|
|
31541
|
+
* Durable (write-behind), so a restart does not forget an outage it heard
|
|
31542
|
+
* about. It is fed only on the evaluating node, like the liveness ledger. The
|
|
31543
|
+
* summary reads the RAM mirror only — never SQLite on its schedule (D447) —
|
|
31544
|
+
* and a mirror that has not loaded answers `null`: the sections it feeds say
|
|
31545
|
+
* "unavailable", never "none" (D49).
|
|
31546
|
+
*/
|
|
31547
|
+
/**
|
|
31178
31548
|
* @durable class=ledger owner=notification-center
|
|
31179
31549
|
* write="a system state the centre accepted OPENING (a stream offline, a device switched off, a camera detecting nothing, an update available, a node that cannot infer, a crash-looped runner); its closing event deletes the row"
|
|
31180
31550
|
* retention="none — a row lives exactly as long as its state is open. Bounded by the installation: streams, devices, nodes and installed packages."
|
|
@@ -31336,9 +31706,7 @@ var NcSystemStateLedger = class {
|
|
|
31336
31706
|
}
|
|
31337
31707
|
/** A detection on a camera: it is not blind. Called for every detection — a Map lookup. */
|
|
31338
31708
|
observeDetection(deviceId) {
|
|
31339
|
-
|
|
31340
|
-
if (this.deps.rows.get(key) === void 0) return;
|
|
31341
|
-
this.close(key, "detection");
|
|
31709
|
+
this.close(`detection-blind:${String(deviceId)}`, "detection");
|
|
31342
31710
|
}
|
|
31343
31711
|
/**
|
|
31344
31712
|
* Any `stream.online`, whatever its reason (D647 review I1). The intake gate
|
|
@@ -31353,7 +31721,7 @@ var NcSystemStateLedger = class {
|
|
|
31353
31721
|
const keep = new Set(list.rows.map((pkg) => updateStateKey(list.target, pkg.nodeId, pkg.packageName)));
|
|
31354
31722
|
for (const nodeId of list.nodeIds) {
|
|
31355
31723
|
const prefix = `update:${list.target}:${nodeId}:`;
|
|
31356
|
-
|
|
31724
|
+
this.closeWhere(`update-list:${list.target}:${nodeId}`, (row) => row.key.startsWith(prefix) && !keep.has(row.key), "update-list");
|
|
31357
31725
|
}
|
|
31358
31726
|
for (const pkg of list.rows) this.openUpdate(list.target, pkg, atMs);
|
|
31359
31727
|
}
|
|
@@ -31363,11 +31731,7 @@ var NcSystemStateLedger = class {
|
|
|
31363
31731
|
* on that node.
|
|
31364
31732
|
*/
|
|
31365
31733
|
observeAddonStarted(addonId, nodeId) {
|
|
31366
|
-
|
|
31367
|
-
if (row.kind !== "addon-crash-loop") continue;
|
|
31368
|
-
if (row.nodeId !== nodeId) continue;
|
|
31369
|
-
if ((row.params["addons"] ?? "").split(",").map((id) => id.trim()).includes(addonId)) this.close(row.key, "addon-started");
|
|
31370
|
-
}
|
|
31734
|
+
this.closeWhere(`addon-started:${nodeId}:${addonId}`, (row) => row.kind === "addon-crash-loop" && row.nodeId === nodeId && (row.params["addons"] ?? "").split(",").map((id) => id.trim()).includes(addonId), "addon-started");
|
|
31371
31735
|
}
|
|
31372
31736
|
/**
|
|
31373
31737
|
* The balancer placed a camera on a node (not pinned): placement skips a
|
|
@@ -31423,11 +31787,31 @@ var NcSystemStateLedger = class {
|
|
|
31423
31787
|
}
|
|
31424
31788
|
});
|
|
31425
31789
|
}
|
|
31790
|
+
/**
|
|
31791
|
+
* Close one state: the row held now (a loaded one, or one opened in the boot
|
|
31792
|
+
* window), and — deferred while the boot window is open — the STORED row
|
|
31793
|
+
* the window cannot see yet (D698 review H1).
|
|
31794
|
+
*/
|
|
31426
31795
|
close(key, by) {
|
|
31796
|
+
this.closeIfHeld(key, by, false);
|
|
31797
|
+
this.deps.rows.deferWhileOpen(`close:${key}`, () => this.closeIfHeld(key, by, true) ? 1 : 0);
|
|
31798
|
+
}
|
|
31799
|
+
/** {@link close} for every row a predicate names — now, and against the loaded rows. */
|
|
31800
|
+
closeWhere(key, names, by) {
|
|
31801
|
+
const run = (replayed) => {
|
|
31802
|
+
let closed = 0;
|
|
31803
|
+
for (const row of this.deps.rows.all()) if (names(row) && this.closeIfHeld(row.key, by, replayed)) closed += 1;
|
|
31804
|
+
return closed;
|
|
31805
|
+
};
|
|
31806
|
+
run(false);
|
|
31807
|
+
this.deps.rows.deferWhileOpen(`where:${key}`, () => run(true));
|
|
31808
|
+
}
|
|
31809
|
+
/** Close the row if one is held; `true` when it was. */
|
|
31810
|
+
closeIfHeld(key, by, replayed) {
|
|
31427
31811
|
const held = this.deps.rows.get(key);
|
|
31428
|
-
if (held === void 0) return;
|
|
31812
|
+
if (held === void 0) return false;
|
|
31429
31813
|
this.deps.rows.remove(key);
|
|
31430
|
-
|
|
31814
|
+
const line = {
|
|
31431
31815
|
...held.deviceId !== void 0 ? { tags: { deviceId: held.deviceId } } : {},
|
|
31432
31816
|
meta: {
|
|
31433
31817
|
key,
|
|
@@ -31435,49 +31819,78 @@ var NcSystemStateLedger = class {
|
|
|
31435
31819
|
by,
|
|
31436
31820
|
since: held.since
|
|
31437
31821
|
}
|
|
31438
|
-
}
|
|
31822
|
+
};
|
|
31823
|
+
if (replayed) {
|
|
31824
|
+
this.deps.logger?.info("system state closed by an event heard before the load", line);
|
|
31825
|
+
return true;
|
|
31826
|
+
}
|
|
31827
|
+
this.deps.logger?.debug("system state closed", line);
|
|
31828
|
+
return true;
|
|
31439
31829
|
}
|
|
31440
31830
|
closeNodeFaults(nodeId, by) {
|
|
31441
|
-
|
|
31442
|
-
if (row.nodeId !== nodeId) continue;
|
|
31443
|
-
if (row.kind === "node-inference-unavailable" || row.kind === "addon-crash-loop") this.close(row.key, by);
|
|
31444
|
-
}
|
|
31831
|
+
this.closeWhere(`node-faults:${nodeId}`, (row) => row.nodeId === nodeId && (row.kind === "node-inference-unavailable" || row.kind === "addon-crash-loop"), by);
|
|
31445
31832
|
}
|
|
31446
31833
|
closeServerUpdate(nodeId) {
|
|
31447
|
-
|
|
31448
|
-
if ((row.nodeId ?? HUB_NODE) !== nodeId) continue;
|
|
31449
|
-
if (row.kind === "server-update-available" || row.kind === "wrapper-update-available" || row.kind === "addon-crash-loop" || row.kind === "node-inference-unavailable") this.close(row.key, "server-updated");
|
|
31450
|
-
}
|
|
31834
|
+
this.closeWhere(`server-update:${nodeId}`, (row) => (row.nodeId ?? HUB_NODE) === nodeId && (row.kind === "server-update-available" || row.kind === "wrapper-update-available" || row.kind === "addon-crash-loop" || row.kind === "node-inference-unavailable"), "server-updated");
|
|
31451
31835
|
}
|
|
31452
31836
|
};
|
|
31453
31837
|
/** The durable home of the rows. */
|
|
31454
31838
|
var NcSystemStateStore = class {
|
|
31455
31839
|
ledger;
|
|
31840
|
+
/**
|
|
31841
|
+
* What the intake opened or closed before the boot load answered (D698,
|
|
31842
|
+
* `boot-window-rows.ts`). A stored row of the same KIND keeps its own
|
|
31843
|
+
* `since`: a stream re-announced offline at boot was not offline "since the
|
|
31844
|
+
* restart".
|
|
31845
|
+
*/
|
|
31846
|
+
bootWindow;
|
|
31456
31847
|
constructor(deps) {
|
|
31457
31848
|
this.ledger = new DurableLedger({
|
|
31458
31849
|
spec: NC_SYSTEM_STATE_SPEC,
|
|
31459
31850
|
store: deps.store,
|
|
31460
31851
|
logger: deps.logger
|
|
31461
31852
|
});
|
|
31853
|
+
this.bootWindow = new BootWindowRows({
|
|
31854
|
+
ledger: this.ledger,
|
|
31855
|
+
keyOf: (row) => row.key,
|
|
31856
|
+
deviceIdOf: (row) => row.deviceId,
|
|
31857
|
+
logger: deps.logger,
|
|
31858
|
+
merge: (stored, provisional) => {
|
|
31859
|
+
if (stored === void 0 || stored.kind !== provisional.kind) return provisional;
|
|
31860
|
+
const merged = {
|
|
31861
|
+
...provisional,
|
|
31862
|
+
since: stored.since
|
|
31863
|
+
};
|
|
31864
|
+
return (0, node_util.isDeepStrictEqual)(merged, stored) ? stored : merged;
|
|
31865
|
+
}
|
|
31866
|
+
});
|
|
31462
31867
|
}
|
|
31463
31868
|
static declare(store) {
|
|
31464
31869
|
return DurableLedger.declare(store, NC_SYSTEM_STATE_SPEC);
|
|
31465
31870
|
}
|
|
31466
31871
|
/** Boot reseed. A failed load keeps the mirror and leaves `answered` false. */
|
|
31467
31872
|
async load() {
|
|
31468
|
-
|
|
31873
|
+
const loaded = (await this.ledger.load()).length;
|
|
31874
|
+
this.bootWindow.settle();
|
|
31875
|
+
return loaded;
|
|
31469
31876
|
}
|
|
31470
31877
|
port() {
|
|
31471
31878
|
return {
|
|
31472
31879
|
get: (key) => this.ledger.get(key),
|
|
31473
31880
|
put: (row) => {
|
|
31881
|
+
if (this.bootWindow.open()) {
|
|
31882
|
+
this.bootWindow.stageProvisional(row);
|
|
31883
|
+
return;
|
|
31884
|
+
}
|
|
31474
31885
|
this.ledger.put(row).catch(() => void 0);
|
|
31475
31886
|
},
|
|
31476
31887
|
remove: (key) => {
|
|
31888
|
+
if (this.bootWindow.open()) this.bootWindow.noteRemoved(key);
|
|
31477
31889
|
this.ledger.forget(key);
|
|
31478
31890
|
},
|
|
31479
31891
|
all: () => this.ledger.snapshot(),
|
|
31480
|
-
answered: () => this.ledger.hasAnswered()
|
|
31892
|
+
answered: () => this.ledger.hasAnswered(),
|
|
31893
|
+
deferWhileOpen: (key, replay) => this.bootWindow.deferWhileOpen(key, replay)
|
|
31481
31894
|
};
|
|
31482
31895
|
}
|
|
31483
31896
|
};
|
|
@@ -31602,8 +32015,21 @@ var NcStateSummaryService = class {
|
|
|
31602
32015
|
const nowMs = this.deps.now();
|
|
31603
32016
|
const summary = await this.collect(rule, nowMs);
|
|
31604
32017
|
const text = this.word(summary, nowMs);
|
|
32018
|
+
const split = require_clip_model_registry.systemEventSummarySplitOf(rule);
|
|
32019
|
+
const empty = isEmptyStateSummary(summary);
|
|
31605
32020
|
return {
|
|
31606
|
-
empty
|
|
32021
|
+
empty,
|
|
32022
|
+
split,
|
|
32023
|
+
rows: empty ? [] : split === "section" ? text.sections.map((section) => ({
|
|
32024
|
+
section: section.id,
|
|
32025
|
+
title: section.title,
|
|
32026
|
+
text: section.body,
|
|
32027
|
+
count: section.count
|
|
32028
|
+
})) : [{
|
|
32029
|
+
title: text.sections.map((section) => section.title).join(", "),
|
|
32030
|
+
text: text.text,
|
|
32031
|
+
count: text.count
|
|
32032
|
+
}],
|
|
31607
32033
|
count: text.count,
|
|
31608
32034
|
text: text.text,
|
|
31609
32035
|
sections: summary.sections.map((section) => ({
|
|
@@ -31616,12 +32042,84 @@ var NcStateSummaryService = class {
|
|
|
31616
32042
|
excludedGroups: [...summary.excludedGroups]
|
|
31617
32043
|
};
|
|
31618
32044
|
}
|
|
31619
|
-
|
|
32045
|
+
/**
|
|
32046
|
+
* TEST the rule (D698, `nc.testRule`): the summary it would send NOW,
|
|
32047
|
+
* ENQUEUED through the ordinary outbox under a test record id — split as the
|
|
32048
|
+
* rule is. The per-camera mute is bypassed, as every rule test bypasses it,
|
|
32049
|
+
* and a summary has no cooldown to bypass. Nothing to report sends nothing
|
|
32050
|
+
* and SAYS so, per section.
|
|
32051
|
+
*/
|
|
32052
|
+
async test(rule) {
|
|
32053
|
+
const nowMs = this.deps.now();
|
|
32054
|
+
const summary = await this.collect(rule, nowMs, () => false);
|
|
32055
|
+
const split = require_clip_model_registry.systemEventSummarySplitOf(rule);
|
|
32056
|
+
const recordId = stateSummaryTestRecordId(rule.id, nowMs);
|
|
32057
|
+
const sections = summary.sections.map((section) => ({
|
|
32058
|
+
id: section.id,
|
|
32059
|
+
status: section.status,
|
|
32060
|
+
items: section.items.length,
|
|
32061
|
+
...section.reason !== void 0 ? { reason: section.reason } : {}
|
|
32062
|
+
}));
|
|
32063
|
+
const meta = {
|
|
32064
|
+
ruleId: rule.id,
|
|
32065
|
+
rule: rule.name,
|
|
32066
|
+
split,
|
|
32067
|
+
bypassMute: true
|
|
32068
|
+
};
|
|
32069
|
+
if (isEmptyStateSummary(summary)) {
|
|
32070
|
+
this.deps.logger.info("state summary TEST has nothing to report — not sent", { meta: {
|
|
32071
|
+
...meta,
|
|
32072
|
+
dropped: summary.dropped
|
|
32073
|
+
} });
|
|
32074
|
+
return {
|
|
32075
|
+
recordId,
|
|
32076
|
+
split,
|
|
32077
|
+
nothingToReport: true,
|
|
32078
|
+
sections,
|
|
32079
|
+
rows: []
|
|
32080
|
+
};
|
|
32081
|
+
}
|
|
32082
|
+
const subjects = stateSummarySubjects({
|
|
32083
|
+
rule,
|
|
32084
|
+
summary,
|
|
32085
|
+
text: this.word(summary, nowMs),
|
|
32086
|
+
window: {
|
|
32087
|
+
startMs: nowMs,
|
|
32088
|
+
endMs: nowMs
|
|
32089
|
+
},
|
|
32090
|
+
split,
|
|
32091
|
+
recordBase: recordId
|
|
32092
|
+
});
|
|
32093
|
+
const rows = [];
|
|
32094
|
+
for (const subject of subjects) {
|
|
32095
|
+
const enqueued = await this.deps.enqueue(rule, subject, "synthetic");
|
|
32096
|
+
const section = subject.systemEvent?.data["section"];
|
|
32097
|
+
rows.push({
|
|
32098
|
+
recordId: subject.recordId,
|
|
32099
|
+
...typeof section === "string" ? { section } : {},
|
|
32100
|
+
enqueued: enqueued.inserted,
|
|
32101
|
+
targetIds: [...enqueued.targetIds]
|
|
32102
|
+
});
|
|
32103
|
+
}
|
|
32104
|
+
this.deps.logger.info("state summary TEST enqueued", { meta: {
|
|
32105
|
+
...meta,
|
|
32106
|
+
items: summary.itemCount,
|
|
32107
|
+
rows: rows.map((row) => `${row.recordId}:${String(row.enqueued)}`)
|
|
32108
|
+
} });
|
|
32109
|
+
return {
|
|
32110
|
+
recordId,
|
|
32111
|
+
split,
|
|
32112
|
+
nothingToReport: false,
|
|
32113
|
+
sections,
|
|
32114
|
+
rows
|
|
32115
|
+
};
|
|
32116
|
+
}
|
|
32117
|
+
async collect(rule, nowMs, isMuted = this.deps.isMuted) {
|
|
31620
32118
|
const nodes = await this.nodeView();
|
|
31621
32119
|
return collectStateSummary({
|
|
31622
32120
|
rule,
|
|
31623
32121
|
nowMs,
|
|
31624
|
-
isMuted
|
|
32122
|
+
isMuted,
|
|
31625
32123
|
sources: stateSources({
|
|
31626
32124
|
alerts: this.deps.alerts,
|
|
31627
32125
|
liveness: this.deps.liveness,
|
|
@@ -31657,17 +32155,24 @@ var NcStateSummaryService = class {
|
|
|
31657
32155
|
});
|
|
31658
32156
|
}
|
|
31659
32157
|
async deliver(rule, summary, window) {
|
|
31660
|
-
const
|
|
32158
|
+
const text = this.word(summary, window.endMs);
|
|
32159
|
+
const subjects = stateSummarySubjects({
|
|
31661
32160
|
rule,
|
|
31662
32161
|
summary,
|
|
31663
|
-
text
|
|
31664
|
-
window
|
|
32162
|
+
text,
|
|
32163
|
+
window,
|
|
32164
|
+
split: require_clip_model_registry.systemEventSummarySplitOf(rule)
|
|
31665
32165
|
});
|
|
31666
|
-
if (
|
|
31667
|
-
this.deps.logger.warn("state summary
|
|
32166
|
+
if (subjects.length === 0) {
|
|
32167
|
+
this.deps.logger.warn("state summary produced no row — not sent", { meta: {
|
|
32168
|
+
ruleId: rule.id,
|
|
32169
|
+
sections: text.sections.length
|
|
32170
|
+
} });
|
|
31668
32171
|
return 0;
|
|
31669
32172
|
}
|
|
31670
|
-
|
|
32173
|
+
let inserted = 0;
|
|
32174
|
+
for (const subject of subjects) inserted += (await this.deps.enqueue(rule, subject, "system")).inserted;
|
|
32175
|
+
return inserted;
|
|
31671
32176
|
}
|
|
31672
32177
|
};
|
|
31673
32178
|
//#endregion
|
|
@@ -33393,7 +33898,7 @@ var NotificationCenter = class NotificationCenter {
|
|
|
33393
33898
|
livenessHoldoffMs: deps.livenessHoldoffMs ?? DEVICE_LIVENESS_HOLDOFF_MS,
|
|
33394
33899
|
isMuted: (deviceId) => this.deviceMutes.isMuted(deviceId),
|
|
33395
33900
|
texts: () => this.texts,
|
|
33396
|
-
enqueue: (rule, subject) => this.enqueueStateSummary(rule, subject)
|
|
33901
|
+
enqueue: (rule, subject, origin) => this.enqueueStateSummary(rule, subject, origin)
|
|
33397
33902
|
});
|
|
33398
33903
|
this.confirmGate = deps.generateVision !== void 0 ? new NcConfirmGate({
|
|
33399
33904
|
logger: this.logger.child("confirm"),
|
|
@@ -35934,30 +36439,42 @@ var NotificationCenter = class NotificationCenter {
|
|
|
35934
36439
|
await this.enqueuePickerTargets(event, reached, outcomes);
|
|
35935
36440
|
return report({ candidates: candidates.length });
|
|
35936
36441
|
}
|
|
36442
|
+
/** TEST a state-summary rule (D698, `nc.testRule`) — see `NcStateSummaryService.test`. */
|
|
36443
|
+
testStateSummary(rule) {
|
|
36444
|
+
return this.stateSummary.test(rule);
|
|
36445
|
+
}
|
|
35937
36446
|
/**
|
|
35938
|
-
* Enqueue ONE state summary (D647) through the rule's ordinary rows: its
|
|
36447
|
+
* Enqueue ONE state summary row (D647) through the rule's ordinary rows: its
|
|
35939
36448
|
* targets, its users (admins only — the row files under the system device),
|
|
35940
36449
|
* its template, and the drain's snooze gate and retries. The record id is
|
|
35941
|
-
* the dedup key, so a window
|
|
36450
|
+
* the dedup key, so a window — or each section of it (D698) — is enqueued
|
|
36451
|
+
* once whatever the restarts. A TEST's rows carry the `synthetic` origin, so
|
|
36452
|
+
* they are marked as a test exactly like every other rule test's.
|
|
35942
36453
|
*/
|
|
35943
|
-
async enqueueStateSummary(rule, subject) {
|
|
35944
|
-
const appended = withSummaryBody(rule);
|
|
36454
|
+
async enqueueStateSummary(rule, subject, origin) {
|
|
36455
|
+
const appended = withSummaryBody(rule, stateSummarySectionOf(subject));
|
|
35945
36456
|
if (appended !== null) this.logger.warn("state summary rule body names no summary content — {{summaryText}} appended", { meta: {
|
|
35946
36457
|
ruleId: rule.id,
|
|
35947
36458
|
rule: rule.name
|
|
35948
36459
|
} });
|
|
35949
36460
|
const sent = appended ?? rule;
|
|
35950
36461
|
const userTargets = await this.resolveUserTargets(sent, subject.deviceId);
|
|
35951
|
-
const entries = this.buildEntries(sent, subject, "system-event", ["systemEvent"], userTargets,
|
|
36462
|
+
const entries = this.buildEntries(sent, subject, "system-event", ["systemEvent"], userTargets, origin);
|
|
35952
36463
|
if (entries.length === 0) {
|
|
35953
36464
|
this.logger.warn("state summary reached NO target — its users are not admins, and it has no targets", { meta: {
|
|
35954
36465
|
ruleId: rule.id,
|
|
35955
36466
|
rule: rule.name,
|
|
35956
36467
|
targetUsers: rule.targetUsers ?? []
|
|
35957
36468
|
} });
|
|
35958
|
-
return
|
|
36469
|
+
return {
|
|
36470
|
+
inserted: 0,
|
|
36471
|
+
targetIds: []
|
|
36472
|
+
};
|
|
35959
36473
|
}
|
|
35960
|
-
return
|
|
36474
|
+
return {
|
|
36475
|
+
inserted: await this.outbox.enqueue(entries),
|
|
36476
|
+
targetIds: entries.map((entry) => entry.targetId)
|
|
36477
|
+
};
|
|
35961
36478
|
}
|
|
35962
36479
|
/**
|
|
35963
36480
|
* The rules a trigger may fire, which is `listEnabled(delivery)` plus and
|
|
@@ -36671,9 +37188,11 @@ var NotificationCenter = class NotificationCenter {
|
|
|
36671
37188
|
*
|
|
36672
37189
|
* A seed is INFO and a repeat is DEBUG on purpose: seeding happens once per
|
|
36673
37190
|
* subject per lifetime of the ledger, and it is exactly what an operator
|
|
36674
|
-
* reading the logs after a restart needs to see ("it
|
|
36675
|
-
*
|
|
36676
|
-
*
|
|
37191
|
+
* reading the logs after a restart needs to see ("it did not announce
|
|
37192
|
+
* them"). A seed heard BEFORE the boot load is provisional (D698): the load
|
|
37193
|
+
* keeps the stored row when it holds the same state, and its `boot window
|
|
37194
|
+
* settled` line says how many it kept and wrote. A no-flip is ordinary bus
|
|
37195
|
+
* chatter — two categories describing one edge — and would be noise at info.
|
|
36677
37196
|
*/
|
|
36678
37197
|
reportLivenessDrop(incoming, verdict) {
|
|
36679
37198
|
const dropped = discardedLivenessEvent(incoming, verdict);
|
|
@@ -42620,6 +43139,280 @@ function parseDebugDeviceIds(url) {
|
|
|
42620
43139
|
return out;
|
|
42621
43140
|
}
|
|
42622
43141
|
//#endregion
|
|
43142
|
+
//#region src/pipeline-analytics/store/tracking-tape-index-store.ts
|
|
43143
|
+
/**
|
|
43144
|
+
* @durable class=ledger owner=pipeline-analytics
|
|
43145
|
+
* write="one row per PROMOTED tracking-tape window (`debug.tape.promote`);
|
|
43146
|
+
* updated when the operator marks it reviewed or releases its pins;
|
|
43147
|
+
* deleted together with its file by `debug.tape.delete`."
|
|
43148
|
+
* retention="NOT an age clock and NOT track retention — a promoted window is
|
|
43149
|
+
* history being labelled (D681). Bounded by the promotion budget
|
|
43150
|
+
* (MAX_PROMOTED_TAPES = 60, 1 GiB): a promotion beyond it is REFUSED,
|
|
43151
|
+
* never satisfied by evicting another row."
|
|
43152
|
+
*/
|
|
43153
|
+
var TRACKING_TAPES_COLLECTION = "pipeline-analytics:tracking-tapes";
|
|
43154
|
+
var TRACKING_TAPES_COLUMNS = [
|
|
43155
|
+
{
|
|
43156
|
+
name: "id",
|
|
43157
|
+
type: "TEXT",
|
|
43158
|
+
primaryKey: true,
|
|
43159
|
+
notNull: true
|
|
43160
|
+
},
|
|
43161
|
+
{
|
|
43162
|
+
name: "deviceId",
|
|
43163
|
+
type: "INTEGER",
|
|
43164
|
+
notNull: true
|
|
43165
|
+
},
|
|
43166
|
+
{
|
|
43167
|
+
name: "scenario",
|
|
43168
|
+
type: "TEXT",
|
|
43169
|
+
notNull: true
|
|
43170
|
+
},
|
|
43171
|
+
{
|
|
43172
|
+
name: "note",
|
|
43173
|
+
type: "TEXT"
|
|
43174
|
+
},
|
|
43175
|
+
{
|
|
43176
|
+
name: "lighting",
|
|
43177
|
+
type: "TEXT",
|
|
43178
|
+
notNull: true
|
|
43179
|
+
},
|
|
43180
|
+
{
|
|
43181
|
+
name: "fromMs",
|
|
43182
|
+
type: "INTEGER",
|
|
43183
|
+
notNull: true
|
|
43184
|
+
},
|
|
43185
|
+
{
|
|
43186
|
+
name: "toMs",
|
|
43187
|
+
type: "INTEGER",
|
|
43188
|
+
notNull: true
|
|
43189
|
+
},
|
|
43190
|
+
{
|
|
43191
|
+
name: "state",
|
|
43192
|
+
type: "TEXT",
|
|
43193
|
+
notNull: true
|
|
43194
|
+
},
|
|
43195
|
+
{
|
|
43196
|
+
name: "frames",
|
|
43197
|
+
type: "INTEGER",
|
|
43198
|
+
notNull: true
|
|
43199
|
+
},
|
|
43200
|
+
{
|
|
43201
|
+
name: "skips",
|
|
43202
|
+
type: "INTEGER",
|
|
43203
|
+
notNull: true
|
|
43204
|
+
},
|
|
43205
|
+
{
|
|
43206
|
+
name: "gaps",
|
|
43207
|
+
type: "INTEGER",
|
|
43208
|
+
notNull: true
|
|
43209
|
+
},
|
|
43210
|
+
{
|
|
43211
|
+
name: "bytes",
|
|
43212
|
+
type: "INTEGER",
|
|
43213
|
+
notNull: true
|
|
43214
|
+
},
|
|
43215
|
+
{
|
|
43216
|
+
name: "reviewed",
|
|
43217
|
+
type: "BOOLEAN",
|
|
43218
|
+
notNull: true
|
|
43219
|
+
},
|
|
43220
|
+
{
|
|
43221
|
+
name: "trackIds",
|
|
43222
|
+
type: "TEXT",
|
|
43223
|
+
notNull: true
|
|
43224
|
+
},
|
|
43225
|
+
{
|
|
43226
|
+
name: "createdAt",
|
|
43227
|
+
type: "INTEGER",
|
|
43228
|
+
notNull: true
|
|
43229
|
+
},
|
|
43230
|
+
{
|
|
43231
|
+
name: "releasedAt",
|
|
43232
|
+
type: "INTEGER"
|
|
43233
|
+
},
|
|
43234
|
+
{
|
|
43235
|
+
name: "pinsSettled",
|
|
43236
|
+
type: "BOOLEAN",
|
|
43237
|
+
notNull: true
|
|
43238
|
+
}
|
|
43239
|
+
];
|
|
43240
|
+
var TrackingTapeRowSchema = require_clip_model_registry.object({
|
|
43241
|
+
id: require_clip_model_registry.string(),
|
|
43242
|
+
deviceId: require_clip_model_registry.number().int(),
|
|
43243
|
+
scenario: require_clip_model_registry.string(),
|
|
43244
|
+
note: require_clip_model_registry.string().nullable(),
|
|
43245
|
+
lighting: TapeLightingSchema,
|
|
43246
|
+
fromMs: require_clip_model_registry.number().int(),
|
|
43247
|
+
toMs: require_clip_model_registry.number().int(),
|
|
43248
|
+
state: require_clip_model_registry._enum(["complete", "incomplete"]),
|
|
43249
|
+
frames: require_clip_model_registry.number().int(),
|
|
43250
|
+
skips: require_clip_model_registry.number().int(),
|
|
43251
|
+
gaps: require_clip_model_registry.number().int(),
|
|
43252
|
+
bytes: require_clip_model_registry.number().int(),
|
|
43253
|
+
reviewed: require_clip_model_registry.boolean(),
|
|
43254
|
+
trackIds: require_clip_model_registry.array(require_clip_model_registry.string()),
|
|
43255
|
+
createdAt: require_clip_model_registry.number().int(),
|
|
43256
|
+
releasedAt: require_clip_model_registry.number().int().nullable(),
|
|
43257
|
+
pinsSettled: require_clip_model_registry.boolean()
|
|
43258
|
+
});
|
|
43259
|
+
var TrackIdsSchema = require_clip_model_registry.array(require_clip_model_registry.string());
|
|
43260
|
+
/**
|
|
43261
|
+
* True when the SQLite settings backend would hand `text` back DECODED: it
|
|
43262
|
+
* parses any TEXT that starts with `[` or `{` and is valid JSON
|
|
43263
|
+
* (`decodeColumnValue` decodes by content, not by declared type). Free text
|
|
43264
|
+
* like that cannot round-trip through a TEXT column (follow-up 617, review M2).
|
|
43265
|
+
*/
|
|
43266
|
+
function decodesAsJson(text) {
|
|
43267
|
+
if (!(text.startsWith("[") || text.startsWith("{"))) return false;
|
|
43268
|
+
try {
|
|
43269
|
+
JSON.parse(text);
|
|
43270
|
+
return true;
|
|
43271
|
+
} catch {
|
|
43272
|
+
return false;
|
|
43273
|
+
}
|
|
43274
|
+
}
|
|
43275
|
+
/**
|
|
43276
|
+
* Free text (`scenario`, `note`) as the column hands it back: a value the
|
|
43277
|
+
* backend decoded is re-encoded, so a row written before `debug.tape.promote`
|
|
43278
|
+
* refused such text never vanishes (its whitespace may differ). Anything else
|
|
43279
|
+
* goes to the schema as it came.
|
|
43280
|
+
*/
|
|
43281
|
+
function freeText(raw) {
|
|
43282
|
+
return raw !== null && typeof raw === "object" ? JSON.stringify(raw) : raw;
|
|
43283
|
+
}
|
|
43284
|
+
/**
|
|
43285
|
+
* `trackIds` as the column hands it back. The SQLite backend decodes a TEXT
|
|
43286
|
+
* that starts with `[` on read (`decodeColumnValue`), so the store sees the
|
|
43287
|
+
* ARRAY it wrote; a string is parsed here only for a backend (or an old row)
|
|
43288
|
+
* that hands the text back as written. `JSON.parse(String(array))` threw on the
|
|
43289
|
+
* decoded array and every promoted row was skipped as malformed (follow-up 617).
|
|
43290
|
+
* `null` = unreadable, absent included (the column is NOT NULL, so absence is
|
|
43291
|
+
* corruption) — the row is reported, never repaired.
|
|
43292
|
+
*/
|
|
43293
|
+
function decodeTrackIds(raw) {
|
|
43294
|
+
if (raw === void 0 || raw === null) return null;
|
|
43295
|
+
if (typeof raw !== "string") return raw;
|
|
43296
|
+
try {
|
|
43297
|
+
return JSON.parse(raw);
|
|
43298
|
+
} catch {
|
|
43299
|
+
return null;
|
|
43300
|
+
}
|
|
43301
|
+
}
|
|
43302
|
+
var TrackingTapeIndexStore = class {
|
|
43303
|
+
store;
|
|
43304
|
+
logger;
|
|
43305
|
+
constructor(deps) {
|
|
43306
|
+
this.store = deps.store;
|
|
43307
|
+
this.logger = deps.logger;
|
|
43308
|
+
}
|
|
43309
|
+
static async declare(store) {
|
|
43310
|
+
await store.declareCollection.mutate({
|
|
43311
|
+
collection: TRACKING_TAPES_COLLECTION,
|
|
43312
|
+
columns: [...TRACKING_TAPES_COLUMNS],
|
|
43313
|
+
indexes: [{
|
|
43314
|
+
name: "idx_trackingtapes_device_created",
|
|
43315
|
+
columns: ["deviceId", "createdAt"]
|
|
43316
|
+
}]
|
|
43317
|
+
});
|
|
43318
|
+
}
|
|
43319
|
+
async insert(row) {
|
|
43320
|
+
const { id, trackIds, ...rest } = TrackingTapeRowSchema.parse(row);
|
|
43321
|
+
await this.store.insert.mutate({
|
|
43322
|
+
collection: TRACKING_TAPES_COLLECTION,
|
|
43323
|
+
record: {
|
|
43324
|
+
id,
|
|
43325
|
+
data: {
|
|
43326
|
+
...rest,
|
|
43327
|
+
trackIds: JSON.stringify(trackIds)
|
|
43328
|
+
}
|
|
43329
|
+
}
|
|
43330
|
+
});
|
|
43331
|
+
}
|
|
43332
|
+
async get(id) {
|
|
43333
|
+
const first = (await this.store.query.query({
|
|
43334
|
+
collection: TRACKING_TAPES_COLLECTION,
|
|
43335
|
+
filter: {
|
|
43336
|
+
where: { id },
|
|
43337
|
+
limit: 1
|
|
43338
|
+
}
|
|
43339
|
+
}))[0];
|
|
43340
|
+
return first === void 0 ? null : this.parse(first.id, first.data);
|
|
43341
|
+
}
|
|
43342
|
+
async list(deviceId) {
|
|
43343
|
+
const rows = await this.store.query.query({
|
|
43344
|
+
collection: TRACKING_TAPES_COLLECTION,
|
|
43345
|
+
filter: {
|
|
43346
|
+
...deviceId !== void 0 ? { where: { deviceId } } : {},
|
|
43347
|
+
orderBy: {
|
|
43348
|
+
field: "createdAt",
|
|
43349
|
+
direction: "desc"
|
|
43350
|
+
},
|
|
43351
|
+
limit: 1e3
|
|
43352
|
+
}
|
|
43353
|
+
});
|
|
43354
|
+
const out = [];
|
|
43355
|
+
for (const r of rows) {
|
|
43356
|
+
const parsed = this.parse(r.id, r.data);
|
|
43357
|
+
if (parsed !== null) out.push(parsed);
|
|
43358
|
+
}
|
|
43359
|
+
return out;
|
|
43360
|
+
}
|
|
43361
|
+
async setReviewed(id, reviewed) {
|
|
43362
|
+
await this.store.update.mutate({
|
|
43363
|
+
collection: TRACKING_TAPES_COLLECTION,
|
|
43364
|
+
id,
|
|
43365
|
+
data: { reviewed }
|
|
43366
|
+
});
|
|
43367
|
+
}
|
|
43368
|
+
/** Release AND unsettle in one row write: the unpins the release implies
|
|
43369
|
+
* are owed from this moment, whether or not the caller lives to make them. */
|
|
43370
|
+
async setReleased(id, atMs) {
|
|
43371
|
+
await this.store.update.mutate({
|
|
43372
|
+
collection: TRACKING_TAPES_COLLECTION,
|
|
43373
|
+
id,
|
|
43374
|
+
data: {
|
|
43375
|
+
releasedAt: atMs,
|
|
43376
|
+
pinsSettled: false
|
|
43377
|
+
}
|
|
43378
|
+
});
|
|
43379
|
+
}
|
|
43380
|
+
async setPinsSettled(id, settled) {
|
|
43381
|
+
await this.store.update.mutate({
|
|
43382
|
+
collection: TRACKING_TAPES_COLLECTION,
|
|
43383
|
+
id,
|
|
43384
|
+
data: { pinsSettled: settled }
|
|
43385
|
+
});
|
|
43386
|
+
}
|
|
43387
|
+
async remove(id) {
|
|
43388
|
+
await this.store.delete.mutate({
|
|
43389
|
+
collection: TRACKING_TAPES_COLLECTION,
|
|
43390
|
+
key: id
|
|
43391
|
+
});
|
|
43392
|
+
}
|
|
43393
|
+
parse(id, data) {
|
|
43394
|
+
const ids = TrackIdsSchema.safeParse(decodeTrackIds(data["trackIds"]));
|
|
43395
|
+
const parsed = TrackingTapeRowSchema.safeParse({
|
|
43396
|
+
id,
|
|
43397
|
+
...data,
|
|
43398
|
+
scenario: freeText(data["scenario"]),
|
|
43399
|
+
note: freeText(data["note"] ?? null),
|
|
43400
|
+
releasedAt: data["releasedAt"] ?? null,
|
|
43401
|
+
trackIds: ids.success ? ids.data : null
|
|
43402
|
+
});
|
|
43403
|
+
if (parsed.success) return parsed.data;
|
|
43404
|
+
this.logger.warn("tracking tape index: skipped a malformed row", {
|
|
43405
|
+
meta: {
|
|
43406
|
+
id,
|
|
43407
|
+
issues: parsed.error.issues.map((i) => i.path.join(".")),
|
|
43408
|
+
trackIdsType: typeof data["trackIds"]
|
|
43409
|
+
},
|
|
43410
|
+
...typeof data["deviceId"] === "number" ? { tags: { deviceId: data["deviceId"] } } : {}
|
|
43411
|
+
});
|
|
43412
|
+
return null;
|
|
43413
|
+
}
|
|
43414
|
+
};
|
|
43415
|
+
//#endregion
|
|
42623
43416
|
//#region src/pipeline-analytics/debug/tape-actions.ts
|
|
42624
43417
|
/**
|
|
42625
43418
|
* `debug.tape.*` — the tracking-tape surface (D681, spec §3.3). Admin,
|
|
@@ -42650,12 +43443,19 @@ var TapeStatusRowSchema = require_clip_model_registry.object({
|
|
|
42650
43443
|
var TapeStatusInputSchema = require_clip_model_registry.object({ deviceIds: require_clip_model_registry.array(require_clip_model_registry.number().int()).optional() });
|
|
42651
43444
|
var TapeStatusOutputSchema = require_clip_model_registry.object({ cameras: require_clip_model_registry.array(TapeStatusRowSchema).readonly() });
|
|
42652
43445
|
var TapeLightingInputSchema = require_clip_model_registry._enum(["day", "ir"]);
|
|
43446
|
+
/**
|
|
43447
|
+
* Free text the settings store would hand back decoded is refused by name
|
|
43448
|
+
* (follow-up 617, review M2): whole JSON array/object text in a TEXT column
|
|
43449
|
+
* comes back as an array or object, and the index could not read the row.
|
|
43450
|
+
*/
|
|
43451
|
+
var NOT_JSON_TEXT = "must not be JSON array/object text — the settings store would hand it back decoded";
|
|
43452
|
+
var notJsonText = (s) => !decodesAsJson(s);
|
|
42653
43453
|
var PromoteInputSchema = require_clip_model_registry.object({
|
|
42654
43454
|
deviceId: require_clip_model_registry.number().int().positive(),
|
|
42655
43455
|
fromMs: require_clip_model_registry.number().int(),
|
|
42656
43456
|
toMs: require_clip_model_registry.number().int(),
|
|
42657
|
-
scenario: require_clip_model_registry.string(),
|
|
42658
|
-
note: require_clip_model_registry.string().max(500).optional(),
|
|
43457
|
+
scenario: require_clip_model_registry.string().refine(notJsonText, { message: NOT_JSON_TEXT }),
|
|
43458
|
+
note: require_clip_model_registry.string().max(500).refine(notJsonText, { message: NOT_JSON_TEXT }).optional(),
|
|
42659
43459
|
lighting: TapeLightingInputSchema.optional()
|
|
42660
43460
|
});
|
|
42661
43461
|
var PromoteOutputSchema = require_clip_model_registry.discriminatedUnion("ok", [require_clip_model_registry.object({
|
|
@@ -51118,6 +51918,17 @@ function pairRiders(detections) {
|
|
|
51118
51918
|
* (D438) no frame did, and a newcomer on the stale prediction took the id.
|
|
51119
51919
|
*/
|
|
51120
51920
|
var MAX_PREDICTION_MS = 1e3;
|
|
51921
|
+
/**
|
|
51922
|
+
* A frame more than `maxMissedMs` behind the latest is a CLOCK STEP only as
|
|
51923
|
+
* the K-th such frame in a row, each in order with the one before (follow-up
|
|
51924
|
+
* 617, fix round 3; D49: a read that would destroy work needs another to
|
|
51925
|
+
* agree). A step ends every track on the camera; one stray timestamp — a
|
|
51926
|
+
* backlog spike 4.5 s late — or two runner clocks delivering interleaved
|
|
51927
|
+
* (a camera migrating between runners) must not. Three is the least that a
|
|
51928
|
+
* single late frame plus an interleaved partner cannot reach by chance; the
|
|
51929
|
+
* cost of a real step is K−1 dropped frames (~260 ms at 7.7 fps).
|
|
51930
|
+
*/
|
|
51931
|
+
var CLOCK_STEP_CONFIRM_FRAMES = 3;
|
|
51121
51932
|
/** Confident boxes kept on a tracklet's path (the old tracker's bound). */
|
|
51122
51933
|
var MAX_PATH_LENGTH = 300;
|
|
51123
51934
|
/** Swap test: crossed pairs cheaper than this over the chosen pairs are doubt. */
|
|
@@ -51923,6 +52734,35 @@ function linkReach(from, gapMs) {
|
|
|
51923
52734
|
return diag * Math.min(LINK_REACH_MAX_DIAGONALS, Math.max(LINK_REACH_MIN_DIAGONALS, LINK_REACH_DIAGONALS_PER_SEC * gapMs / 1e3));
|
|
51924
52735
|
}
|
|
51925
52736
|
var arcKey = (from, to) => `${from}\u0000${to}`;
|
|
52737
|
+
/**
|
|
52738
|
+
* The rejections that mean "geometry or appearance said no to a plausible
|
|
52739
|
+
* continuation" of a confirmed predecessor — a track the operator sees break
|
|
52740
|
+
* (follow-up 617, F5). Every one of them is past `hardRejection`, so the
|
|
52741
|
+
* predecessor is confirmed. NOT here (fix round 1): `unconfirmed`;
|
|
52742
|
+
* `has-successor` (continued elsewhere); `concurrent` and `identity-conflict`
|
|
52743
|
+
* (two subjects — the normal case, and the most frequent); `dropped` (an
|
|
52744
|
+
* operator delete or a refused chain).
|
|
52745
|
+
*/
|
|
52746
|
+
var BREAK_REJECTIONS = new Set([
|
|
52747
|
+
"reach",
|
|
52748
|
+
"area",
|
|
52749
|
+
"appearance",
|
|
52750
|
+
"contested",
|
|
52751
|
+
"cost"
|
|
52752
|
+
]);
|
|
52753
|
+
/** The confirmed predecessors whose link a decision rejected as a break (F5). */
|
|
52754
|
+
function confirmedPredecessorRejections(candidates) {
|
|
52755
|
+
const out = [];
|
|
52756
|
+
for (const c of candidates ?? []) {
|
|
52757
|
+
if (c.rejected === void 0 || !BREAK_REJECTIONS.has(c.rejected)) continue;
|
|
52758
|
+
out.push({
|
|
52759
|
+
from: c.from,
|
|
52760
|
+
rejected: c.rejected,
|
|
52761
|
+
gapMs: c.gapMs
|
|
52762
|
+
});
|
|
52763
|
+
}
|
|
52764
|
+
return out;
|
|
52765
|
+
}
|
|
51926
52766
|
function arcFacts(a) {
|
|
51927
52767
|
return {
|
|
51928
52768
|
from: a.from.id,
|
|
@@ -52000,6 +52840,26 @@ var DeferredLinker = class {
|
|
|
52000
52840
|
if (seen === void 0) this.names.set(p.id, new Set([p.identity]));
|
|
52001
52841
|
else seen.add(p.identity);
|
|
52002
52842
|
}
|
|
52843
|
+
/**
|
|
52844
|
+
* A clock step (follow-up 617, fix round 2): every piece still undecided is
|
|
52845
|
+
* decided `new`, so nothing that existed before the step can be absorbed
|
|
52846
|
+
* across it — whatever was ready was committed just before. The facade
|
|
52847
|
+
* rebases its clock past `LINK_MAX_GAP_MS`, so no later piece can continue
|
|
52848
|
+
* one of these either. An extra id, never a mix-up.
|
|
52849
|
+
*/
|
|
52850
|
+
closeUndecided(now, pieces) {
|
|
52851
|
+
for (const p of pieces) this.accept(p, now);
|
|
52852
|
+
for (const p of this.pieces.values()) {
|
|
52853
|
+
if (this.outcomes.has(p.id)) continue;
|
|
52854
|
+
this.decide(p.id, "new");
|
|
52855
|
+
this.observer?.({
|
|
52856
|
+
t: now,
|
|
52857
|
+
kind: "new",
|
|
52858
|
+
to: p.id,
|
|
52859
|
+
reason: "clock-step"
|
|
52860
|
+
});
|
|
52861
|
+
}
|
|
52862
|
+
}
|
|
52003
52863
|
isTailFinal(id, now, isLive) {
|
|
52004
52864
|
const root = this.rootOf(id);
|
|
52005
52865
|
if (this.pieces.has(root) && !this.outcomes.has(root)) return false;
|
|
@@ -52750,7 +53610,15 @@ var Tracklet = class {
|
|
|
52750
53610
|
h: this.assocBox.h
|
|
52751
53611
|
};
|
|
52752
53612
|
}
|
|
53613
|
+
/**
|
|
53614
|
+
* A confident sighting. One OLDER than the last (`t < lastSeenAt` — ~1 % of
|
|
53615
|
+
* frames reach the tracker out of capture order, up to 1.2 s back) is
|
|
53616
|
+
* `stale`: it changes nothing. A rewound tracklet would be measured from a
|
|
53617
|
+
* box its subject left a second ago, and L2 refused a link by reach on
|
|
53618
|
+
* exactly that (follow-up 617, F1). The caller reports it.
|
|
53619
|
+
*/
|
|
52753
53620
|
observeConfident(det, t, faceBearing) {
|
|
53621
|
+
if (t < this.lastSeenAt) return "stale";
|
|
52754
53622
|
const dt = t - this.lastSeenAt;
|
|
52755
53623
|
if (dt > 0) {
|
|
52756
53624
|
const from = centreOf(this.lastBox);
|
|
@@ -52762,6 +53630,7 @@ var Tracklet = class {
|
|
|
52762
53630
|
this.emitIntervalMs = dt;
|
|
52763
53631
|
}
|
|
52764
53632
|
this.recordConfident(det, t, faceBearing);
|
|
53633
|
+
return "recorded";
|
|
52765
53634
|
}
|
|
52766
53635
|
observeLowScore(box, t) {
|
|
52767
53636
|
this.assocBox = box;
|
|
@@ -52863,6 +53732,15 @@ var TrackletTracker = class {
|
|
|
52863
53732
|
demoteSilent(t) {
|
|
52864
53733
|
for (const tr of this.live.values()) if (tr.mode === "active" && t - tr.lastSeenAt > MAX_PREDICTION_MS) tr.mode = "coasting";
|
|
52865
53734
|
}
|
|
53735
|
+
/** Ends every live tracklet at its own last sighting (a clock step); the ids it ended. */
|
|
53736
|
+
endAll(reason) {
|
|
53737
|
+
const ended = [];
|
|
53738
|
+
for (const tr of [...this.live.values()]) {
|
|
53739
|
+
ended.push(tr.id);
|
|
53740
|
+
this.finish(tr, tr.lastSeenAt, reason);
|
|
53741
|
+
}
|
|
53742
|
+
return ended;
|
|
53743
|
+
}
|
|
52866
53744
|
/** Silence: end what the coast budget no longer covers. */
|
|
52867
53745
|
expire(now) {
|
|
52868
53746
|
for (const tr of [...this.live.values()]) if (now - tr.lastSeenAt > this.config.maxMissedMs) this.finish(tr, tr.lastSeenAt, "missed");
|
|
@@ -52953,12 +53831,12 @@ var TrackletTracker = class {
|
|
|
52953
53831
|
const d = frame.detections[pair.detection];
|
|
52954
53832
|
if (tr === void 0 || d === void 0) return;
|
|
52955
53833
|
frame.used.add(pair.detection);
|
|
53834
|
+
if (tr.observeConfident(d, frame.t, frame.ctx.faceBearing?.has(d.bbox) === true) === "stale") return;
|
|
52956
53835
|
tr.mode = "active";
|
|
52957
53836
|
if (d.class !== tr.klass) this.refuse({
|
|
52958
53837
|
reason: "cross-class-absorbed",
|
|
52959
53838
|
against: tr
|
|
52960
53839
|
}, d);
|
|
52961
|
-
tr.observeConfident(d, frame.t, frame.ctx.faceBearing?.has(d.bbox) === true);
|
|
52962
53840
|
if (pair.contested) {
|
|
52963
53841
|
tr.contestedHits += 1;
|
|
52964
53842
|
tr.contestedSince ??= frame.t;
|
|
@@ -53147,27 +54025,151 @@ var TrackerV2 = class {
|
|
|
53147
54025
|
l1;
|
|
53148
54026
|
l2;
|
|
53149
54027
|
pending = [];
|
|
54028
|
+
/**
|
|
54029
|
+
* Internal time = the caller's frame time + `offset`. Zero until a clock
|
|
54030
|
+
* step; each step moves it so the internal clock never goes back and
|
|
54031
|
+
* resumes past `LINK_MAX_GAP_MS` of silence (fix round 2, C1). L1, L2, their
|
|
54032
|
+
* facts and prunes run on internal time; fusions and `frameClock()` are
|
|
54033
|
+
* handed back in the caller's clock.
|
|
54034
|
+
*/
|
|
54035
|
+
offset = 0;
|
|
53150
54036
|
lastT = 0;
|
|
54037
|
+
/** The latest FRAME time taken (internal) — never the sweep's extrapolated `now`. */
|
|
54038
|
+
lastFrameT = Number.NEGATIVE_INFINITY;
|
|
54039
|
+
observers;
|
|
54040
|
+
/** Caller times of the consecutive frames past the stale bound so far — the
|
|
54041
|
+
* run a clock step needs `CLOCK_STEP_CONFIRM_FRAMES` of (fix round 3). */
|
|
54042
|
+
stepRun = [];
|
|
53151
54043
|
constructor(config, deps) {
|
|
53152
54044
|
this.config = config;
|
|
53153
|
-
this.
|
|
54045
|
+
this.observers = deps.observers ?? {};
|
|
54046
|
+
const out = this.observers;
|
|
54047
|
+
const internal = {
|
|
54048
|
+
...out.refusal !== void 0 ? { refusal: out.refusal } : {},
|
|
54049
|
+
ended: (f) => out.ended?.({
|
|
54050
|
+
...f,
|
|
54051
|
+
at: f.at - this.offset
|
|
54052
|
+
}),
|
|
54053
|
+
split: (f) => out.split?.({
|
|
54054
|
+
...f,
|
|
54055
|
+
t: f.t - this.offset
|
|
54056
|
+
})
|
|
54057
|
+
};
|
|
54058
|
+
this.l1 = new TrackletTracker(config, deps.mintTrackId, internal);
|
|
53154
54059
|
this.l2 = new DeferredLinker({
|
|
53155
54060
|
maxMissedMs: config.maxMissedMs,
|
|
53156
54061
|
appearanceResolvesAmbiguity: config.appearanceResolvesAmbiguity
|
|
53157
|
-
},
|
|
54062
|
+
}, (f) => out.link?.({
|
|
54063
|
+
...f,
|
|
54064
|
+
t: f.t - this.offset
|
|
54065
|
+
}));
|
|
53158
54066
|
}
|
|
53159
|
-
update(detections,
|
|
54067
|
+
update(detections, frameT, ctx) {
|
|
54068
|
+
const order = this.classifyFrame(frameT);
|
|
54069
|
+
if (order === "out-of-order") return this.drop(frameT, detections.length);
|
|
54070
|
+
let t = frameT + this.offset;
|
|
54071
|
+
if (order === "clock-step") t = this.clockStep(frameT, this.lastFrameT - t);
|
|
54072
|
+
this.stepRun = [];
|
|
54073
|
+
this.lastFrameT = t;
|
|
53160
54074
|
this.lastT = Math.max(this.lastT, t);
|
|
53161
54075
|
const step = this.l1.step(detections, t, ctx);
|
|
53162
54076
|
this.link(this.lastT);
|
|
53163
54077
|
return step.emissions.map((e) => this.emit(e, t));
|
|
53164
54078
|
}
|
|
53165
54079
|
/**
|
|
54080
|
+
* A frame stamped before the latest one already taken (follow-up 617, F1;
|
|
54081
|
+
* ~1 % of frames, up to 1.2 s back). It is decided ONCE, for the whole
|
|
54082
|
+
* frame (fix round 1, H1): matched against the tracklets' NEWEST boxes, a
|
|
54083
|
+
* stale box can land on another subject — a walker's old box on the walker
|
|
54084
|
+
* behind — and be emitted under that id. So nothing binds, spawns, splits or
|
|
54085
|
+
* is demoted on it; the confirmed tracklets are emitted as on a frame that
|
|
54086
|
+
* matched nothing (no sighting), and one fact says the frame was dropped.
|
|
54087
|
+
*/
|
|
54088
|
+
/**
|
|
54089
|
+
* How `update` will take a frame at `frameT` — without taking it. The
|
|
54090
|
+
* processor asks first (fix round 3, MEDIUM-2), so a dropped frame never
|
|
54091
|
+
* reaches the stationary gate, the zones or the event path.
|
|
54092
|
+
* - `in-order`: not behind the latest frame taken;
|
|
54093
|
+
* - `out-of-order`: behind it — within `maxMissedMs` (disorder, ≤ 1.2 s
|
|
54094
|
+
* measured), or further back but not yet the `CLOCK_STEP_CONFIRM_FRAMES`-th
|
|
54095
|
+
* such frame in a row;
|
|
54096
|
+
* - `clock-step`: the K-th consecutive frame past the bound, each in order
|
|
54097
|
+
* with the one before (within `maxMissedMs`).
|
|
54098
|
+
*/
|
|
54099
|
+
classifyFrame(frameT) {
|
|
54100
|
+
const behindMs = this.lastFrameT - (frameT + this.offset);
|
|
54101
|
+
if (!(behindMs > 0)) return "in-order";
|
|
54102
|
+
if (behindMs <= this.config.maxMissedMs) return "out-of-order";
|
|
54103
|
+
return this.runLength(frameT) >= CLOCK_STEP_CONFIRM_FRAMES ? "clock-step" : "out-of-order";
|
|
54104
|
+
}
|
|
54105
|
+
/** The past-bound run this frame would make, itself included. */
|
|
54106
|
+
runLength(frameT) {
|
|
54107
|
+
const prev = this.stepRun.at(-1);
|
|
54108
|
+
return (prev !== void 0 && frameT >= prev && frameT - prev <= this.config.maxMissedMs ? this.stepRun.length : 0) + 1;
|
|
54109
|
+
}
|
|
54110
|
+
/**
|
|
54111
|
+
* Drops a frame `classifyFrame` called `out-of-order`. A frame past the
|
|
54112
|
+
* bound extends (or restarts) the run a clock step needs; any other breaks
|
|
54113
|
+
* it. One fact per frame.
|
|
54114
|
+
*/
|
|
54115
|
+
drop(frameT, detections) {
|
|
54116
|
+
const behindMs = this.lastFrameT - (frameT + this.offset);
|
|
54117
|
+
let pendingClockStep = 0;
|
|
54118
|
+
if (behindMs > this.config.maxMissedMs) {
|
|
54119
|
+
pendingClockStep = this.runLength(frameT);
|
|
54120
|
+
this.stepRun = pendingClockStep === 1 ? [frameT] : [...this.stepRun, frameT];
|
|
54121
|
+
} else this.stepRun = [];
|
|
54122
|
+
this.observers.outOfOrder?.({
|
|
54123
|
+
t: frameT,
|
|
54124
|
+
lastFrameT: this.frameClock(),
|
|
54125
|
+
behindMs,
|
|
54126
|
+
detections,
|
|
54127
|
+
pendingClockStep
|
|
54128
|
+
});
|
|
54129
|
+
return this.l1.liveTracklets().filter((tr) => tr.confirmed).map((tracklet) => this.emit({
|
|
54130
|
+
tracklet,
|
|
54131
|
+
detection: void 0,
|
|
54132
|
+
matched: false
|
|
54133
|
+
}, this.lastFrameT));
|
|
54134
|
+
}
|
|
54135
|
+
/**
|
|
54136
|
+
* The frame clock stepped back further than `maxMissedMs` (fix round 2, C1):
|
|
54137
|
+
* not disorder — an NTP correction, or a camera moved to a runner whose clock
|
|
54138
|
+
* is behind. An unbounded gate dropped every frame until the new clock passed
|
|
54139
|
+
* the old high mark (60 s back: 461 frames, a new subject never tracked, the
|
|
54140
|
+
* departed one coasting on). Instead: commit what L2 has decided, end every
|
|
54141
|
+
* live tracklet at its own last sighting, decide every piece still undecided
|
|
54142
|
+
* `new` (no fusion crosses the step), and resume the internal clock past
|
|
54143
|
+
* `LINK_MAX_GAP_MS` of silence, so nothing later can continue anything
|
|
54144
|
+
* earlier. Returns the frame's internal time.
|
|
54145
|
+
*/
|
|
54146
|
+
clockStep(frameT, behindMs) {
|
|
54147
|
+
const lastFrameT = this.frameClock();
|
|
54148
|
+
this.link(this.lastT);
|
|
54149
|
+
const ended = this.l1.endAll("clock-step");
|
|
54150
|
+
this.l2.closeUndecided(this.lastT, this.pieces());
|
|
54151
|
+
const resumeAt = Math.max(this.lastT, this.lastFrameT) + LINK_MAX_GAP_MS + 1;
|
|
54152
|
+
this.offset = resumeAt - frameT;
|
|
54153
|
+
this.lastT = resumeAt;
|
|
54154
|
+
this.observers.clockStep?.({
|
|
54155
|
+
t: frameT,
|
|
54156
|
+
lastFrameT,
|
|
54157
|
+
behindMs,
|
|
54158
|
+
ended
|
|
54159
|
+
});
|
|
54160
|
+
return resumeAt;
|
|
54161
|
+
}
|
|
54162
|
+
/** The latest frame time taken, in the caller's clock (rebased by a clock step). */
|
|
54163
|
+
frameClock() {
|
|
54164
|
+
return this.lastFrameT - this.offset;
|
|
54165
|
+
}
|
|
54166
|
+
/**
|
|
53166
54167
|
* The close sweep's entry on a camera whose frames stopped (D438): end the
|
|
53167
54168
|
* tracklets the coast budget no longer covers, THEN run L2 (review C2).
|
|
53168
54169
|
* `now` is FRAME time (the processor converts wall clock, Task 11).
|
|
53169
54170
|
*/
|
|
53170
|
-
advance(
|
|
54171
|
+
advance(frameNow) {
|
|
54172
|
+
const now = frameNow + this.offset;
|
|
53171
54173
|
this.lastT = Math.max(this.lastT, now);
|
|
53172
54174
|
this.l1.expire(now);
|
|
53173
54175
|
this.link(now);
|
|
@@ -53183,6 +54185,7 @@ var TrackerV2 = class {
|
|
|
53183
54185
|
markSuppressed(id) {
|
|
53184
54186
|
this.l1.markSuppressed(id);
|
|
53185
54187
|
}
|
|
54188
|
+
/** In the caller's clock (translated when each was committed, LOW-5). */
|
|
53186
54189
|
drainFusions() {
|
|
53187
54190
|
const out = this.pending;
|
|
53188
54191
|
this.pending = [];
|
|
@@ -53195,8 +54198,8 @@ var TrackerV2 = class {
|
|
|
53195
54198
|
isLive(id) {
|
|
53196
54199
|
return this.l2.membersOf(this.l2.rootOf(id)).some((m) => this.isLiveTracklet(m));
|
|
53197
54200
|
}
|
|
53198
|
-
isTailFinal(id,
|
|
53199
|
-
return this.l2.isTailFinal(id,
|
|
54201
|
+
isTailFinal(id, frameNow) {
|
|
54202
|
+
return this.l2.isTailFinal(id, frameNow + this.offset, (m) => this.isLiveTracklet(m));
|
|
53200
54203
|
}
|
|
53201
54204
|
/**
|
|
53202
54205
|
* Ends every live member of the chain. `stationary` (round 4): the
|
|
@@ -53212,9 +54215,15 @@ var TrackerV2 = class {
|
|
|
53212
54215
|
const tr = this.l1.get(id);
|
|
53213
54216
|
return tr !== void 0 && tr.endedAt === null;
|
|
53214
54217
|
}
|
|
54218
|
+
pieces() {
|
|
54219
|
+
return [...this.l1.liveTracklets(), ...this.l1.endedTracklets()].map(toPiece);
|
|
54220
|
+
}
|
|
53215
54221
|
link(now) {
|
|
53216
|
-
const
|
|
53217
|
-
|
|
54222
|
+
const offset = this.offset;
|
|
54223
|
+
for (const f of this.l2.step(now, this.pieces())) this.pending.push({
|
|
54224
|
+
...f,
|
|
54225
|
+
t: f.t - offset
|
|
54226
|
+
});
|
|
53218
54227
|
this.l1.prune(now);
|
|
53219
54228
|
this.l2.prune(now);
|
|
53220
54229
|
}
|
|
@@ -53277,7 +54286,8 @@ var NO_ENDS = {
|
|
|
53277
54286
|
missed: 0,
|
|
53278
54287
|
ambiguous: 0,
|
|
53279
54288
|
dropped: 0,
|
|
53280
|
-
stationary: 0
|
|
54289
|
+
stationary: 0,
|
|
54290
|
+
"clock-step": 0
|
|
53281
54291
|
};
|
|
53282
54292
|
function emptyStats() {
|
|
53283
54293
|
return {
|
|
@@ -53290,16 +54300,28 @@ function emptyStats() {
|
|
|
53290
54300
|
linksRefused: 0,
|
|
53291
54301
|
linksNew: 0,
|
|
53292
54302
|
linksDisagreed: 0,
|
|
53293
|
-
lowScoreHolds: 0
|
|
54303
|
+
lowScoreHolds: 0,
|
|
54304
|
+
outOfOrderFrames: 0,
|
|
54305
|
+
clockSteps: 0
|
|
53294
54306
|
};
|
|
53295
54307
|
}
|
|
53296
54308
|
/** Nothing happened in the window: no tracklet ended, no split, no L2
|
|
53297
54309
|
* decision, no low-score hold (review LOW-N1 — what the sweep stays quiet about). */
|
|
53298
54310
|
function isIdleTrackerStats(stats) {
|
|
53299
|
-
return [stats.tracklets.confirmed, stats.tracklets.unconfirmed].flatMap((c) => Object.values(c)).every((n) => n === 0) && stats.splits === 0 && stats.linksCommitted === 0 && stats.linksRefused === 0 && stats.linksNew === 0 && stats.linksDisagreed === 0 && stats.lowScoreHolds === 0;
|
|
54311
|
+
return [stats.tracklets.confirmed, stats.tracklets.unconfirmed].flatMap((c) => Object.values(c)).every((n) => n === 0) && stats.splits === 0 && stats.linksCommitted === 0 && stats.linksRefused === 0 && stats.linksNew === 0 && stats.linksDisagreed === 0 && stats.lowScoreHolds === 0 && stats.outOfOrderFrames === 0 && stats.clockSteps === 0;
|
|
53300
54312
|
}
|
|
53301
54313
|
/** Shared empty — a fold is rare and every other frame would allocate. */
|
|
53302
54314
|
var EMPTY_RIDER_FOLDS = [];
|
|
54315
|
+
/** The tape observation of a dropped frame: no gate verdict, nothing bound. */
|
|
54316
|
+
var DROPPED_FRAME_OBSERVATION = {
|
|
54317
|
+
gate: {
|
|
54318
|
+
strong: [],
|
|
54319
|
+
weak: [],
|
|
54320
|
+
confirmed: [],
|
|
54321
|
+
woken: []
|
|
54322
|
+
},
|
|
54323
|
+
live: []
|
|
54324
|
+
};
|
|
53303
54325
|
/** Mapping from StateAnalyzer's `ObjectState.state` values to the
|
|
53304
54326
|
* canonical TrackState enum used on tracks + events. */
|
|
53305
54327
|
function mapObjectStateToTrackState(s) {
|
|
@@ -53478,6 +54500,14 @@ var FrameProcessor = class {
|
|
|
53478
54500
|
else if (f.kind === "new") this.stats.linksNew += 1;
|
|
53479
54501
|
else if (f.kind === "disagreed") this.stats.linksDisagreed += 1;
|
|
53480
54502
|
this.trackerObservers.link?.(f);
|
|
54503
|
+
},
|
|
54504
|
+
outOfOrder: (f) => {
|
|
54505
|
+
this.stats.outOfOrderFrames += 1;
|
|
54506
|
+
this.trackerObservers.outOfOrder?.(f);
|
|
54507
|
+
},
|
|
54508
|
+
clockStep: (f) => {
|
|
54509
|
+
this.stats.clockSteps += 1;
|
|
54510
|
+
this.trackerObservers.clockStep?.(f);
|
|
53481
54511
|
}
|
|
53482
54512
|
}
|
|
53483
54513
|
});
|
|
@@ -53761,6 +54791,37 @@ var FrameProcessor = class {
|
|
|
53761
54791
|
* Zero is the expected reading; a rising one means two people stand close
|
|
53762
54792
|
* together on this camera, which is worth knowing per-camera rather than
|
|
53763
54793
|
* per-fleet. */
|
|
54794
|
+
/**
|
|
54795
|
+
* A frame `classifyFrame` called out of order: the tracker records the drop
|
|
54796
|
+
* (and any clock-step run it extends) and nothing else reads the frame. The
|
|
54797
|
+
* per-frame side tables a reader asks for afterwards are emptied, so it never
|
|
54798
|
+
* re-reads the previous frame's.
|
|
54799
|
+
*/
|
|
54800
|
+
droppedFrame(input) {
|
|
54801
|
+
this.tracker.drop(input.timestamp, input.frame.detections.length);
|
|
54802
|
+
this.lastZoneOverlaps = /* @__PURE__ */ new Map();
|
|
54803
|
+
this.lastZoneRejections = /* @__PURE__ */ new Map();
|
|
54804
|
+
this.lastLabelWriteRefusals = [];
|
|
54805
|
+
const fusions = this.tracker.drainFusions();
|
|
54806
|
+
this.applyFusions(fusions);
|
|
54807
|
+
return {
|
|
54808
|
+
deviceId: this.deviceId,
|
|
54809
|
+
timestamp: input.timestamp,
|
|
54810
|
+
frameWidth: input.frame.width,
|
|
54811
|
+
frameHeight: input.frame.height,
|
|
54812
|
+
tracked: [],
|
|
54813
|
+
objectEvents: [],
|
|
54814
|
+
appearanceEvents: [],
|
|
54815
|
+
rawTrackedDetections: [],
|
|
54816
|
+
stationaryConfirmed: [],
|
|
54817
|
+
stationaryWoken: [],
|
|
54818
|
+
stationaryContinued: 0,
|
|
54819
|
+
riderFolds: [],
|
|
54820
|
+
...input.observe === true ? { observation: DROPPED_FRAME_OBSERVATION } : {},
|
|
54821
|
+
fusions,
|
|
54822
|
+
droppedOutOfOrder: true
|
|
54823
|
+
};
|
|
54824
|
+
}
|
|
53764
54825
|
facesRefusedOutsideParent() {
|
|
53765
54826
|
return this.faceOutsideParentCount;
|
|
53766
54827
|
}
|
|
@@ -53783,6 +54844,7 @@ var FrameProcessor = class {
|
|
|
53783
54844
|
}
|
|
53784
54845
|
process(input) {
|
|
53785
54846
|
const { timestamp, frame } = input;
|
|
54847
|
+
if (this.tracker.classifyFrame(timestamp) === "out-of-order") return this.droppedFrame(input);
|
|
53786
54848
|
this.lastFrameT = timestamp;
|
|
53787
54849
|
this.lastFrameWallAt = this.wallClock();
|
|
53788
54850
|
const frameWidth = frame.width;
|
|
@@ -56534,239 +57596,6 @@ async function promoteTapeWindow(deps, input, signal) {
|
|
|
56534
57596
|
};
|
|
56535
57597
|
}
|
|
56536
57598
|
//#endregion
|
|
56537
|
-
//#region src/pipeline-analytics/store/tracking-tape-index-store.ts
|
|
56538
|
-
/**
|
|
56539
|
-
* @durable class=ledger owner=pipeline-analytics
|
|
56540
|
-
* write="one row per PROMOTED tracking-tape window (`debug.tape.promote`);
|
|
56541
|
-
* updated when the operator marks it reviewed or releases its pins;
|
|
56542
|
-
* deleted together with its file by `debug.tape.delete`."
|
|
56543
|
-
* retention="NOT an age clock and NOT track retention — a promoted window is
|
|
56544
|
-
* history being labelled (D681). Bounded by the promotion budget
|
|
56545
|
-
* (MAX_PROMOTED_TAPES = 60, 1 GiB): a promotion beyond it is REFUSED,
|
|
56546
|
-
* never satisfied by evicting another row."
|
|
56547
|
-
*/
|
|
56548
|
-
var TRACKING_TAPES_COLLECTION = "pipeline-analytics:tracking-tapes";
|
|
56549
|
-
var TRACKING_TAPES_COLUMNS = [
|
|
56550
|
-
{
|
|
56551
|
-
name: "id",
|
|
56552
|
-
type: "TEXT",
|
|
56553
|
-
primaryKey: true,
|
|
56554
|
-
notNull: true
|
|
56555
|
-
},
|
|
56556
|
-
{
|
|
56557
|
-
name: "deviceId",
|
|
56558
|
-
type: "INTEGER",
|
|
56559
|
-
notNull: true
|
|
56560
|
-
},
|
|
56561
|
-
{
|
|
56562
|
-
name: "scenario",
|
|
56563
|
-
type: "TEXT",
|
|
56564
|
-
notNull: true
|
|
56565
|
-
},
|
|
56566
|
-
{
|
|
56567
|
-
name: "note",
|
|
56568
|
-
type: "TEXT"
|
|
56569
|
-
},
|
|
56570
|
-
{
|
|
56571
|
-
name: "lighting",
|
|
56572
|
-
type: "TEXT",
|
|
56573
|
-
notNull: true
|
|
56574
|
-
},
|
|
56575
|
-
{
|
|
56576
|
-
name: "fromMs",
|
|
56577
|
-
type: "INTEGER",
|
|
56578
|
-
notNull: true
|
|
56579
|
-
},
|
|
56580
|
-
{
|
|
56581
|
-
name: "toMs",
|
|
56582
|
-
type: "INTEGER",
|
|
56583
|
-
notNull: true
|
|
56584
|
-
},
|
|
56585
|
-
{
|
|
56586
|
-
name: "state",
|
|
56587
|
-
type: "TEXT",
|
|
56588
|
-
notNull: true
|
|
56589
|
-
},
|
|
56590
|
-
{
|
|
56591
|
-
name: "frames",
|
|
56592
|
-
type: "INTEGER",
|
|
56593
|
-
notNull: true
|
|
56594
|
-
},
|
|
56595
|
-
{
|
|
56596
|
-
name: "skips",
|
|
56597
|
-
type: "INTEGER",
|
|
56598
|
-
notNull: true
|
|
56599
|
-
},
|
|
56600
|
-
{
|
|
56601
|
-
name: "gaps",
|
|
56602
|
-
type: "INTEGER",
|
|
56603
|
-
notNull: true
|
|
56604
|
-
},
|
|
56605
|
-
{
|
|
56606
|
-
name: "bytes",
|
|
56607
|
-
type: "INTEGER",
|
|
56608
|
-
notNull: true
|
|
56609
|
-
},
|
|
56610
|
-
{
|
|
56611
|
-
name: "reviewed",
|
|
56612
|
-
type: "BOOLEAN",
|
|
56613
|
-
notNull: true
|
|
56614
|
-
},
|
|
56615
|
-
{
|
|
56616
|
-
name: "trackIds",
|
|
56617
|
-
type: "TEXT",
|
|
56618
|
-
notNull: true
|
|
56619
|
-
},
|
|
56620
|
-
{
|
|
56621
|
-
name: "createdAt",
|
|
56622
|
-
type: "INTEGER",
|
|
56623
|
-
notNull: true
|
|
56624
|
-
},
|
|
56625
|
-
{
|
|
56626
|
-
name: "releasedAt",
|
|
56627
|
-
type: "INTEGER"
|
|
56628
|
-
},
|
|
56629
|
-
{
|
|
56630
|
-
name: "pinsSettled",
|
|
56631
|
-
type: "BOOLEAN",
|
|
56632
|
-
notNull: true
|
|
56633
|
-
}
|
|
56634
|
-
];
|
|
56635
|
-
var TrackingTapeRowSchema = require_clip_model_registry.object({
|
|
56636
|
-
id: require_clip_model_registry.string(),
|
|
56637
|
-
deviceId: require_clip_model_registry.number().int(),
|
|
56638
|
-
scenario: require_clip_model_registry.string(),
|
|
56639
|
-
note: require_clip_model_registry.string().nullable(),
|
|
56640
|
-
lighting: TapeLightingSchema,
|
|
56641
|
-
fromMs: require_clip_model_registry.number().int(),
|
|
56642
|
-
toMs: require_clip_model_registry.number().int(),
|
|
56643
|
-
state: require_clip_model_registry._enum(["complete", "incomplete"]),
|
|
56644
|
-
frames: require_clip_model_registry.number().int(),
|
|
56645
|
-
skips: require_clip_model_registry.number().int(),
|
|
56646
|
-
gaps: require_clip_model_registry.number().int(),
|
|
56647
|
-
bytes: require_clip_model_registry.number().int(),
|
|
56648
|
-
reviewed: require_clip_model_registry.boolean(),
|
|
56649
|
-
trackIds: require_clip_model_registry.array(require_clip_model_registry.string()),
|
|
56650
|
-
createdAt: require_clip_model_registry.number().int(),
|
|
56651
|
-
releasedAt: require_clip_model_registry.number().int().nullable(),
|
|
56652
|
-
pinsSettled: require_clip_model_registry.boolean()
|
|
56653
|
-
});
|
|
56654
|
-
var TrackIdsSchema = require_clip_model_registry.array(require_clip_model_registry.string());
|
|
56655
|
-
var TrackingTapeIndexStore = class {
|
|
56656
|
-
store;
|
|
56657
|
-
logger;
|
|
56658
|
-
constructor(deps) {
|
|
56659
|
-
this.store = deps.store;
|
|
56660
|
-
this.logger = deps.logger;
|
|
56661
|
-
}
|
|
56662
|
-
static async declare(store) {
|
|
56663
|
-
await store.declareCollection.mutate({
|
|
56664
|
-
collection: TRACKING_TAPES_COLLECTION,
|
|
56665
|
-
columns: [...TRACKING_TAPES_COLUMNS],
|
|
56666
|
-
indexes: [{
|
|
56667
|
-
name: "idx_trackingtapes_device_created",
|
|
56668
|
-
columns: ["deviceId", "createdAt"]
|
|
56669
|
-
}]
|
|
56670
|
-
});
|
|
56671
|
-
}
|
|
56672
|
-
async insert(row) {
|
|
56673
|
-
const { id, trackIds, ...rest } = TrackingTapeRowSchema.parse(row);
|
|
56674
|
-
await this.store.insert.mutate({
|
|
56675
|
-
collection: TRACKING_TAPES_COLLECTION,
|
|
56676
|
-
record: {
|
|
56677
|
-
id,
|
|
56678
|
-
data: {
|
|
56679
|
-
...rest,
|
|
56680
|
-
trackIds: JSON.stringify(trackIds)
|
|
56681
|
-
}
|
|
56682
|
-
}
|
|
56683
|
-
});
|
|
56684
|
-
}
|
|
56685
|
-
async get(id) {
|
|
56686
|
-
const first = (await this.store.query.query({
|
|
56687
|
-
collection: TRACKING_TAPES_COLLECTION,
|
|
56688
|
-
filter: {
|
|
56689
|
-
where: { id },
|
|
56690
|
-
limit: 1
|
|
56691
|
-
}
|
|
56692
|
-
}))[0];
|
|
56693
|
-
return first === void 0 ? null : this.parse(first.id, first.data);
|
|
56694
|
-
}
|
|
56695
|
-
async list(deviceId) {
|
|
56696
|
-
const rows = await this.store.query.query({
|
|
56697
|
-
collection: TRACKING_TAPES_COLLECTION,
|
|
56698
|
-
filter: {
|
|
56699
|
-
...deviceId !== void 0 ? { where: { deviceId } } : {},
|
|
56700
|
-
orderBy: {
|
|
56701
|
-
field: "createdAt",
|
|
56702
|
-
direction: "desc"
|
|
56703
|
-
},
|
|
56704
|
-
limit: 1e3
|
|
56705
|
-
}
|
|
56706
|
-
});
|
|
56707
|
-
const out = [];
|
|
56708
|
-
for (const r of rows) {
|
|
56709
|
-
const parsed = this.parse(r.id, r.data);
|
|
56710
|
-
if (parsed !== null) out.push(parsed);
|
|
56711
|
-
}
|
|
56712
|
-
return out;
|
|
56713
|
-
}
|
|
56714
|
-
async setReviewed(id, reviewed) {
|
|
56715
|
-
await this.store.update.mutate({
|
|
56716
|
-
collection: TRACKING_TAPES_COLLECTION,
|
|
56717
|
-
id,
|
|
56718
|
-
data: { reviewed }
|
|
56719
|
-
});
|
|
56720
|
-
}
|
|
56721
|
-
/** Release AND unsettle in one row write: the unpins the release implies
|
|
56722
|
-
* are owed from this moment, whether or not the caller lives to make them. */
|
|
56723
|
-
async setReleased(id, atMs) {
|
|
56724
|
-
await this.store.update.mutate({
|
|
56725
|
-
collection: TRACKING_TAPES_COLLECTION,
|
|
56726
|
-
id,
|
|
56727
|
-
data: {
|
|
56728
|
-
releasedAt: atMs,
|
|
56729
|
-
pinsSettled: false
|
|
56730
|
-
}
|
|
56731
|
-
});
|
|
56732
|
-
}
|
|
56733
|
-
async setPinsSettled(id, settled) {
|
|
56734
|
-
await this.store.update.mutate({
|
|
56735
|
-
collection: TRACKING_TAPES_COLLECTION,
|
|
56736
|
-
id,
|
|
56737
|
-
data: { pinsSettled: settled }
|
|
56738
|
-
});
|
|
56739
|
-
}
|
|
56740
|
-
async remove(id) {
|
|
56741
|
-
await this.store.delete.mutate({
|
|
56742
|
-
collection: TRACKING_TAPES_COLLECTION,
|
|
56743
|
-
key: id
|
|
56744
|
-
});
|
|
56745
|
-
}
|
|
56746
|
-
parse(id, data) {
|
|
56747
|
-
let trackIds = [];
|
|
56748
|
-
try {
|
|
56749
|
-
trackIds = JSON.parse(String(data["trackIds"] ?? "[]"));
|
|
56750
|
-
} catch {
|
|
56751
|
-
trackIds = null;
|
|
56752
|
-
}
|
|
56753
|
-
const ids = TrackIdsSchema.safeParse(trackIds);
|
|
56754
|
-
const parsed = TrackingTapeRowSchema.safeParse({
|
|
56755
|
-
id,
|
|
56756
|
-
...data,
|
|
56757
|
-
note: data["note"] ?? null,
|
|
56758
|
-
releasedAt: data["releasedAt"] ?? null,
|
|
56759
|
-
trackIds: ids.success ? ids.data : null
|
|
56760
|
-
});
|
|
56761
|
-
if (parsed.success) return parsed.data;
|
|
56762
|
-
this.logger.warn("tracking tape index: skipped a malformed row", {
|
|
56763
|
-
meta: { id },
|
|
56764
|
-
...typeof data["deviceId"] === "number" ? { tags: { deviceId: data["deviceId"] } } : {}
|
|
56765
|
-
});
|
|
56766
|
-
return null;
|
|
56767
|
-
}
|
|
56768
|
-
};
|
|
56769
|
-
//#endregion
|
|
56770
57599
|
//#region src/pipeline-analytics/tracking-bench/recorder/tape-status.ts
|
|
56771
57600
|
/** The one node that may hold tapes: sqlite and bulk state are hub-only, and
|
|
56772
57601
|
* an agent persists nothing (spec §3.1). */
|
|
@@ -87918,6 +88747,18 @@ function isRecord(x) {
|
|
|
87918
88747
|
}
|
|
87919
88748
|
//#endregion
|
|
87920
88749
|
//#region src/pipeline-analytics/store/track-alias-store.ts
|
|
88750
|
+
/**
|
|
88751
|
+
* The alias collection.
|
|
88752
|
+
*
|
|
88753
|
+
* @durable class=ledger owner=pipeline-analytics
|
|
88754
|
+
* write="one row per fused piece (D683): aliasId = the absorbed provisional
|
|
88755
|
+
* id, trackId = its survivor; rewritten when the survivor is itself
|
|
88756
|
+
* absorbed by a later fusion"
|
|
88757
|
+
* retention="TRACK-OWNED by its survivor through the declared `trackId`
|
|
88758
|
+
* column: aliases leave with the survivor in the retention cascade, and
|
|
88759
|
+
* widen that cascade (`expandLeafIds`) to rows still keyed by an
|
|
88760
|
+
* absorbed id"
|
|
88761
|
+
*/
|
|
87921
88762
|
var TRACK_ALIASES_COLLECTION = "pipeline-analytics:track-aliases";
|
|
87922
88763
|
var TRACK_ALIAS_COLUMNS = [
|
|
87923
88764
|
{
|
|
@@ -88100,6 +88941,20 @@ var TrackAliasStore = class {
|
|
|
88100
88941
|
dropped: 0
|
|
88101
88942
|
}];
|
|
88102
88943
|
}
|
|
88944
|
+
/**
|
|
88945
|
+
* The orphan audit's reclaim of ONE alias row: the row AND the `recent`
|
|
88946
|
+
* entry that would otherwise keep resolving it for up to an hour. `true`
|
|
88947
|
+
* only when a row was actually deleted. (`misses` needs nothing: a deleted
|
|
88948
|
+
* alias IS a non-alias, which is what a miss records.)
|
|
88949
|
+
*/
|
|
88950
|
+
async deleteAlias(aliasId) {
|
|
88951
|
+
const { deleted } = await this.store.deleteWhere.mutate({
|
|
88952
|
+
collection: TRACK_ALIASES_COLLECTION,
|
|
88953
|
+
filter: { where: { id: aliasId } }
|
|
88954
|
+
});
|
|
88955
|
+
this.recent.delete(aliasId);
|
|
88956
|
+
return deleted > 0;
|
|
88957
|
+
}
|
|
88103
88958
|
async deleteByTracks(trackIds) {
|
|
88104
88959
|
const { deleted } = await this.store.deleteWhere.mutate({
|
|
88105
88960
|
collection: TRACK_ALIASES_COLLECTION,
|
|
@@ -89026,7 +89881,8 @@ async function auditScope(scope, deps, limits, cursor) {
|
|
|
89026
89881
|
stoppedBy,
|
|
89027
89882
|
nextCursor: offset
|
|
89028
89883
|
} });
|
|
89029
|
-
|
|
89884
|
+
const freedNothing = scope.sizeField === null ? tally.reclaimed === 0 : tally.bytesReclaimed === 0;
|
|
89885
|
+
if (limits.mode === "reclaim" && tally.orphans > 0 && freedNothing) deps.logger.warn("orphan audit: reclaim pass freed no bytes against a standing backlog", { meta: {
|
|
89030
89886
|
collection: scope.collection,
|
|
89031
89887
|
shard: scope.shard?.value ?? null,
|
|
89032
89888
|
orphans: tally.orphans,
|
|
@@ -89140,6 +89996,40 @@ var ORPHAN_AUDIT_DEFAULTS = {
|
|
|
89140
89996
|
pageSize: OrphanAuditPageSizeSchema.parse(void 0)
|
|
89141
89997
|
};
|
|
89142
89998
|
var RAW_DELETE_COLLECTIONS = new Set(["pipeline-analytics:object-events", "pipeline-analytics:plates"]);
|
|
89999
|
+
/**
|
|
90000
|
+
* The `deleteRow` port: ONE row by its primary key `id` (what every
|
|
90001
|
+
* raw-delete collection above declares), answering whether a row went.
|
|
90002
|
+
*
|
|
90003
|
+
* Through `deleteWhere`, because it is the delete that SAYS: `delete`'s output
|
|
90004
|
+
* is `z.void()`, and reading it as `!== undefined` answered `false` for every
|
|
90005
|
+
* row it really deleted — the audit counted every plates/object-events reclaim
|
|
90006
|
+
* as "found nothing", and the per-scope reclaim ceiling never tripped.
|
|
90007
|
+
*/
|
|
90008
|
+
function settingsRowDeleter(store) {
|
|
90009
|
+
return async (collection, key) => {
|
|
90010
|
+
const { deleted } = await store.deleteWhere.mutate({
|
|
90011
|
+
collection,
|
|
90012
|
+
filter: { where: { id: key } }
|
|
90013
|
+
});
|
|
90014
|
+
return deleted > 0;
|
|
90015
|
+
};
|
|
90016
|
+
}
|
|
90017
|
+
/**
|
|
90018
|
+
* The `deleteRow` port AS PRODUCTION WIRES IT: {@link settingsRowDeleter} over
|
|
90019
|
+
* a store that may not be bound yet. Unbound THROWS — the engine counts that as
|
|
90020
|
+
* `reclaimFailed` and keeps the row — rather than answering "nothing deleted",
|
|
90021
|
+
* which is indistinguishable from a row already gone. A named factory so the
|
|
90022
|
+
* wiring itself is under test (D695 T7 review M1: the inline wiring could be
|
|
90023
|
+
* reverted to the original bug with every spec green);
|
|
90024
|
+
* `scripts/check-orphan-reclaimers.ts` checks `index.ts` uses it.
|
|
90025
|
+
*/
|
|
90026
|
+
function settingsRowDeleterOf(getStore) {
|
|
90027
|
+
return async (collection, key) => {
|
|
90028
|
+
const store = getStore();
|
|
90029
|
+
if (store === null) throw new Error("orphan audit: settings store not bound");
|
|
90030
|
+
return await settingsRowDeleter(store)(collection, key);
|
|
90031
|
+
};
|
|
90032
|
+
}
|
|
89143
90033
|
function buildOrphanReclaimer(ports) {
|
|
89144
90034
|
return async (collection, key) => {
|
|
89145
90035
|
if (collection === MEDIA_COLLECTION) {
|
|
@@ -89163,6 +90053,10 @@ function buildOrphanReclaimer(ports) {
|
|
|
89163
90053
|
bytes: null
|
|
89164
90054
|
};
|
|
89165
90055
|
}
|
|
90056
|
+
if (collection === TRACK_ALIASES_COLLECTION) return {
|
|
90057
|
+
rows: await ports.deleteTrackAlias(key) ? 1 : 0,
|
|
90058
|
+
bytes: null
|
|
90059
|
+
};
|
|
89166
90060
|
if (RAW_DELETE_COLLECTIONS.has(collection)) return {
|
|
89167
90061
|
rows: await ports.deleteRow(collection, key) ? 1 : 0,
|
|
89168
90062
|
bytes: null
|
|
@@ -97697,6 +98591,12 @@ var TRACKER_FACT_LOG_SAMPLE_EVERY = 10;
|
|
|
97697
98591
|
* positive spawns (review M1: ~1 750 `new` decisions per camera per hour) —
|
|
97698
98592
|
* are sampled harder; the per-minute summary carries their exact counts. */
|
|
97699
98593
|
var TRACKER_PIECE_LOG_SAMPLE_EVERY = 50;
|
|
98594
|
+
/** Out-of-order frames (follow-up 617, F1: ~1 % of frames) log the first N,
|
|
98595
|
+
* then one in this many — every line carries the count; the per-minute
|
|
98596
|
+
* summary carries the exact `outOfOrderFrames`. */
|
|
98597
|
+
var TRACKER_OUT_OF_ORDER_LOG_SAMPLE_EVERY = 50;
|
|
98598
|
+
/** Clock-step WARNs past the first ten (fix round 3, MEDIUM-1). */
|
|
98599
|
+
var TRACKER_CLOCK_STEP_LOG_SAMPLE_EVERY = 50;
|
|
97700
98600
|
/** First N, then one in `every` — the refusal stream's cadence (D391). */
|
|
97701
98601
|
function sampledTrackerFact(count, every = TRACKER_FACT_LOG_SAMPLE_EVERY) {
|
|
97702
98602
|
return count <= TRACKER_FACT_LOG_ALWAYS_FIRST || count % every === 0;
|
|
@@ -99526,6 +100426,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
|
|
|
99526
100426
|
},
|
|
99527
100427
|
listTrackGroups: (query) => center.listTrackGroups(query),
|
|
99528
100428
|
injectTestEvent: (input) => center.injectTestEvent(input, { getSnapshot: (deviceId) => api.snapshot.getSnapshot.query({ deviceId }) }),
|
|
100429
|
+
testStateSummary: (rule) => center.testStateSummary(rule),
|
|
99529
100430
|
timelapse: {
|
|
99530
100431
|
store: center.timelapseStore,
|
|
99531
100432
|
getRecordingConfig: (deviceId) => api.recordingArchive.getDeviceConfig.query({ deviceId }).catch(() => null),
|
|
@@ -102344,9 +103245,11 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
|
|
|
102344
103245
|
let splitCount = 0;
|
|
102345
103246
|
let ignoredCount = 0;
|
|
102346
103247
|
let newCount = 0;
|
|
103248
|
+
let outOfOrderCount = 0;
|
|
103249
|
+
let clockStepCount = 0;
|
|
102347
103250
|
const endedCounts = /* @__PURE__ */ new Map();
|
|
102348
103251
|
const logLink = (f) => {
|
|
102349
|
-
const
|
|
103252
|
+
const decision = {
|
|
102350
103253
|
source,
|
|
102351
103254
|
t: f.t,
|
|
102352
103255
|
kind: f.kind,
|
|
@@ -102355,7 +103258,10 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
|
|
|
102355
103258
|
cost: f.cost,
|
|
102356
103259
|
gapMs: f.gapMs,
|
|
102357
103260
|
reason: f.reason,
|
|
102358
|
-
recognition: f.recognition
|
|
103261
|
+
recognition: f.recognition
|
|
103262
|
+
};
|
|
103263
|
+
const meta = {
|
|
103264
|
+
...decision,
|
|
102359
103265
|
candidates: f.candidates
|
|
102360
103266
|
};
|
|
102361
103267
|
switch (f.kind) {
|
|
@@ -102393,7 +103299,14 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
|
|
|
102393
103299
|
meta
|
|
102394
103300
|
});
|
|
102395
103301
|
return;
|
|
102396
|
-
case "new":
|
|
103302
|
+
case "new": {
|
|
103303
|
+
if (f.reason === "clock-step") {
|
|
103304
|
+
trackerLog.debug("tracker: piece decided new — closed by a frame clock step", {
|
|
103305
|
+
tags: { deviceId },
|
|
103306
|
+
meta
|
|
103307
|
+
});
|
|
103308
|
+
return;
|
|
103309
|
+
}
|
|
102397
103310
|
if (f.reason === "unconfirmed") {
|
|
102398
103311
|
trackerLog.debug("tracker: piece decided new — never confirmed", {
|
|
102399
103312
|
tags: { deviceId },
|
|
@@ -102401,6 +103314,17 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
|
|
|
102401
103314
|
});
|
|
102402
103315
|
return;
|
|
102403
103316
|
}
|
|
103317
|
+
const rejectedPredecessors = confirmedPredecessorRejections(f.candidates);
|
|
103318
|
+
if (rejectedPredecessors.length > 0) {
|
|
103319
|
+
trackerLog.info("tracker: piece decided new — a confirmed predecessor was rejected", {
|
|
103320
|
+
tags: { deviceId },
|
|
103321
|
+
meta: {
|
|
103322
|
+
...decision,
|
|
103323
|
+
rejectedPredecessors
|
|
103324
|
+
}
|
|
103325
|
+
});
|
|
103326
|
+
return;
|
|
103327
|
+
}
|
|
102404
103328
|
newCount += 1;
|
|
102405
103329
|
if (!sampledTrackerFact(newCount, TRACKER_PIECE_LOG_SAMPLE_EVERY)) return;
|
|
102406
103330
|
trackerLog.info("tracker: piece decided new — no predecessor", {
|
|
@@ -102411,6 +103335,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
|
|
|
102411
103335
|
}
|
|
102412
103336
|
});
|
|
102413
103337
|
return;
|
|
103338
|
+
}
|
|
102414
103339
|
}
|
|
102415
103340
|
};
|
|
102416
103341
|
p.setTrackerObservers({
|
|
@@ -102431,6 +103356,39 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
|
|
|
102431
103356
|
});
|
|
102432
103357
|
},
|
|
102433
103358
|
link: logLink,
|
|
103359
|
+
clockStep: (f) => {
|
|
103360
|
+
clockStepCount += 1;
|
|
103361
|
+
if (!sampledTrackerFact(clockStepCount, TRACKER_CLOCK_STEP_LOG_SAMPLE_EVERY)) return;
|
|
103362
|
+
trackerLog.warn("tracker: frame clock stepped back — tracklets ended, clock rebased", {
|
|
103363
|
+
tags: { deviceId },
|
|
103364
|
+
meta: {
|
|
103365
|
+
source,
|
|
103366
|
+
t: f.t,
|
|
103367
|
+
lastFrameT: f.lastFrameT,
|
|
103368
|
+
behindMs: f.behindMs,
|
|
103369
|
+
ended: f.ended,
|
|
103370
|
+
clockStepCount,
|
|
103371
|
+
sampledEvery: TRACKER_CLOCK_STEP_LOG_SAMPLE_EVERY
|
|
103372
|
+
}
|
|
103373
|
+
});
|
|
103374
|
+
},
|
|
103375
|
+
outOfOrder: (f) => {
|
|
103376
|
+
outOfOrderCount += 1;
|
|
103377
|
+
if (!sampledTrackerFact(outOfOrderCount, TRACKER_OUT_OF_ORDER_LOG_SAMPLE_EVERY)) return;
|
|
103378
|
+
trackerLog.info("tracker: out-of-order frame dropped — older than the latest frame", {
|
|
103379
|
+
tags: { deviceId },
|
|
103380
|
+
meta: {
|
|
103381
|
+
source,
|
|
103382
|
+
t: f.t,
|
|
103383
|
+
lastFrameT: f.lastFrameT,
|
|
103384
|
+
behindMs: f.behindMs,
|
|
103385
|
+
detections: f.detections,
|
|
103386
|
+
pendingClockStep: f.pendingClockStep,
|
|
103387
|
+
outOfOrderCount,
|
|
103388
|
+
sampledEvery: TRACKER_OUT_OF_ORDER_LOG_SAMPLE_EVERY
|
|
103389
|
+
}
|
|
103390
|
+
});
|
|
103391
|
+
},
|
|
102434
103392
|
ended: (f) => {
|
|
102435
103393
|
const k = `${f.reason}:${String(f.confirmed)}`;
|
|
102436
103394
|
const n = (endedCounts.get(k) ?? 0) + 1;
|
|
@@ -102572,6 +103530,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
|
|
|
102572
103530
|
});
|
|
102573
103531
|
});
|
|
102574
103532
|
this.reportTrackerSummary(deviceId, key, processor, "frame");
|
|
103533
|
+
if (result.droppedOutOfOrder === true) return;
|
|
102575
103534
|
for (const r of processor.getLastLabelWriteRefusals()) {
|
|
102576
103535
|
const message = r.reason.kind === "macro-class" ? "label tier collapse refused" : "label write refused";
|
|
102577
103536
|
this.ctx.logger.info(message, {
|
|
@@ -107226,10 +108185,12 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
|
|
|
107226
108185
|
bytes: count.bytes
|
|
107227
108186
|
};
|
|
107228
108187
|
},
|
|
107229
|
-
|
|
107230
|
-
|
|
107231
|
-
|
|
107232
|
-
|
|
108188
|
+
deleteTrackAlias: async (aliasId) => {
|
|
108189
|
+
const aliasStore = this.trackAliasStore;
|
|
108190
|
+
if (aliasStore === null) throw new Error("track alias store not bound");
|
|
108191
|
+
return await aliasStore.deleteAlias(aliasId);
|
|
108192
|
+
},
|
|
108193
|
+
deleteRow: settingsRowDeleterOf(() => this.settingsStore),
|
|
107233
108194
|
logger: this.ctx.logger
|
|
107234
108195
|
}),
|
|
107235
108196
|
now: () => Date.now(),
|