@camstack/types 1.2.259 → 1.2.261

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/dist/addon/base-addon.d.ts +9 -1
  2. package/dist/addon.js +15 -7
  3. package/dist/addon.mjs +15 -7
  4. package/dist/capabilities/camera-streams.cap.d.ts +5 -5
  5. package/dist/capabilities/capability-definition.d.ts +9 -0
  6. package/dist/capabilities/custom-model-registry.cap.d.ts +18 -12
  7. package/dist/capabilities/decoder.cap.d.ts +12 -12
  8. package/dist/capabilities/device-extension.cap.d.ts +370 -0
  9. package/dist/capabilities/device-manager.cap.d.ts +45 -5
  10. package/dist/capabilities/doorbell.cap.d.ts +41 -0
  11. package/dist/capabilities/embedding-encoder.cap.d.ts +175 -0
  12. package/dist/capabilities/index.d.ts +17 -14
  13. package/dist/capabilities/llm-runtime.cap.d.ts +4 -4
  14. package/dist/capabilities/llm-shared.d.ts +6 -6
  15. package/dist/capabilities/llm.cap.d.ts +6 -6
  16. package/dist/capabilities/load-contribution.cap.d.ts +2 -2
  17. package/dist/capabilities/model-convert.cap.d.ts +9 -6
  18. package/dist/capabilities/model-distributor.cap.d.ts +18 -12
  19. package/dist/capabilities/motion-detection.cap.d.ts +4 -4
  20. package/dist/capabilities/notification-rules.cap.d.ts +603 -31
  21. package/dist/capabilities/osd-manager.cap.d.ts +7 -7
  22. package/dist/capabilities/pipeline-analytics.cap.d.ts +130 -129
  23. package/dist/capabilities/pipeline-executor.cap.d.ts +78 -120
  24. package/dist/capabilities/pipeline-orchestrator.cap.d.ts +78 -65
  25. package/dist/capabilities/pipeline-runner.cap.d.ts +30 -6
  26. package/dist/capabilities/platform-probe.cap.d.ts +4 -13
  27. package/dist/capabilities/recording-export.cap.d.ts +7 -7
  28. package/dist/capabilities/schemas/detection-shared.d.ts +4 -4
  29. package/dist/capabilities/schemas/streaming-shared.d.ts +8 -8
  30. package/dist/capabilities/stream-broker.cap.d.ts +9 -9
  31. package/dist/capabilities/system.cap.d.ts +2 -2
  32. package/dist/capabilities/vacuum-control.cap.d.ts +13 -13
  33. package/dist/capabilities/vector-store.cap.d.ts +14 -0
  34. package/dist/capabilities/videoclips.cap.d.ts +5 -5
  35. package/dist/capabilities/webrtc-session.cap.d.ts +2 -2
  36. package/dist/device-extension/index.d.ts +3 -0
  37. package/dist/device-extension/slot-descriptor.d.ts +62 -0
  38. package/dist/device-extension/slot-provider.d.ts +68 -0
  39. package/dist/generated/addon-api.d.ts +59 -9
  40. package/dist/generated/cap-input-defaults.d.ts +1 -1
  41. package/dist/generated/capability-router-map.d.ts +5 -2
  42. package/dist/generated/collection-array-methods.d.ts +1 -1
  43. package/dist/generated/device-proxy.d.ts +7 -7
  44. package/dist/generated/device-scoped-caps.d.ts +1 -1
  45. package/dist/generated/extension-slots.d.ts +39 -0
  46. package/dist/generated/method-access-map.d.ts +1 -1
  47. package/dist/generated/method-device-selectors.d.ts +1 -1
  48. package/dist/generated/system-proxy.d.ts +4 -2
  49. package/dist/index.d.ts +8 -3
  50. package/dist/index.js +3125 -1792
  51. package/dist/index.mjs +3049 -1788
  52. package/dist/interfaces/addon.d.ts +41 -0
  53. package/dist/interfaces/event-bus.d.ts +11 -1
  54. package/dist/interfaces/pipeline-executor-capability.d.ts +15 -6
  55. package/dist/interfaces/platform.d.ts +2 -2
  56. package/dist/interfaces/stream-broker.d.ts +2 -2
  57. package/dist/node.d.ts +1 -0
  58. package/dist/node.js +73 -7
  59. package/dist/node.mjs +73 -8
  60. package/dist/notification/system-event-summary.d.ts +83 -0
  61. package/dist/notification/template-vars.d.ts +15 -0
  62. package/dist/pipeline/cluster-model-scope.d.ts +59 -3
  63. package/dist/{sleep-DU7mu4wb.mjs → sleep-BXv0ZcRa.mjs} +49 -33
  64. package/dist/{sleep-DtomDrRW.js → sleep-QxyLiUId.js} +54 -32
  65. package/dist/storage/atomic-write.d.ts +25 -0
  66. package/dist/types/clip-model.d.ts +64 -0
  67. package/dist/types/detection.d.ts +9 -0
  68. package/dist/types/models.d.ts +18 -19
  69. package/dist/types/pipeline-step.d.ts +9 -1
  70. package/dist/types/pipeline.d.ts +2 -1
  71. package/dist/utils/background-run-registry.d.ts +69 -0
  72. package/dist/utils/runtime-mapping.d.ts +9 -54
  73. package/package.json +1 -1
@@ -489,6 +489,17 @@ export interface AddonContext<TConfig = Record<string, unknown>> {
489
489
  * allowlisted engine-cascade holdouts are the only callers.
490
490
  */
491
491
  readAddonStore(): Promise<Record<string, unknown>>;
492
+ /**
493
+ * The same read, THREE-WAY: a stored blob, no blob, or a read that FAILED.
494
+ * `readAddonStore` folds a failure into `{}`, which a caller cannot tell
495
+ * from "never written"; this one does not (D651). Optional: a settings view
496
+ * that predates it answers nothing, and the caller must treat that as
497
+ * unreadable, never as empty.
498
+ *
499
+ * @internal Same rule as `readAddonStore`: addons reach it only through
500
+ * `BaseAddon.readStoredGlobalKey`.
501
+ */
502
+ readAddonStoreResult?(): Promise<AddonStoreRead>;
492
503
  /**
493
504
  * Write a patch into the addon store. Keys not in the patch are
494
505
  * preserved. Keys explicitly set to `null` are preserved as-is (they
@@ -1409,3 +1420,33 @@ export interface InternalAddonContext extends AddonContext {
1409
1420
  /** Resolve the preferred provider for a capability. Kernel-only. */
1410
1421
  resolveProvider<T = unknown>(capabilityName: string): T | null;
1411
1422
  }
1423
+ /**
1424
+ * A three-way read of an addon's own store (D651). `missing` and `failed` are
1425
+ * distinct on purpose: folding them is how a failed read becomes "defaults".
1426
+ */
1427
+ export type AddonStoreRead = {
1428
+ readonly kind: 'value';
1429
+ readonly value: Readonly<Record<string, unknown>>;
1430
+ } | {
1431
+ readonly kind: 'missing';
1432
+ } | {
1433
+ readonly kind: 'failed';
1434
+ readonly error: string;
1435
+ };
1436
+ /** One stored key of an addon's own store, read three-way (D651). */
1437
+ export type StoredKeyRead =
1438
+ /** The key was written; `value` is what is stored. */
1439
+ {
1440
+ readonly kind: 'present';
1441
+ readonly value: unknown;
1442
+ }
1443
+ /** The store answered and the key was never written. */
1444
+ | {
1445
+ readonly kind: 'absent';
1446
+ }
1447
+ /** The store did not answer, or this node's settings view cannot say. */
1448
+ | {
1449
+ readonly kind: 'unreadable';
1450
+ readonly reason: 'store-read-failed' | 'three-way-read-unavailable';
1451
+ readonly error?: string;
1452
+ };
@@ -165,6 +165,16 @@ export interface PipelineAnalyticsFrameTrackedPayload {
165
165
  * a dropped frame just lets the session close slightly earlier.
166
166
  */
167
167
  readonly hasLiveTrack?: boolean;
168
+ /**
169
+ * How many objects the TRACKER holds on this frame — moving OR still,
170
+ * i.e. `result.tracked.length` (D652). Diagnostics only: it decides nothing.
171
+ * {@link hasLiveTrack} deliberately subtracts a still subject, so a session
172
+ * that closed on a parked car and one that closed on an empty street both
173
+ * saw `false` last; this count is what tells them apart on the orchestrator's
174
+ * close line (`liveTracks`). Absent ⇒ unknown (an older post-analysis),
175
+ * which the consumer reports as `null`, never as 0.
176
+ */
177
+ readonly liveTrackCount?: number;
168
178
  [key: string]: unknown;
169
179
  }
170
180
  /** Lifecycle phase of a tracked object: it appears (`start`), its best
@@ -1260,7 +1270,7 @@ export interface EventCatalog {
1260
1270
  readonly engine: PipelineEngineChoice;
1261
1271
  readonly modelsLoaded: readonly string[];
1262
1272
  readonly inUseByCameras: readonly number[];
1263
- readonly kind: 'runtime' | 'warm-override' | 'device-pool';
1273
+ readonly kind: 'runtime' | 'device-pool';
1264
1274
  readonly poolPid: number | null;
1265
1275
  readonly idleMs: number | null;
1266
1276
  readonly idleTtlMs: number | null;
@@ -33,14 +33,12 @@ export interface PipelineStepInput {
33
33
  * - `imageBase64`: one-shot benchmark (ImageTab "Run")
34
34
  * - `referenceImage`: named file from the reference-image store
35
35
  *
36
- * `engine` + `deviceId` are optional contextual hints — the executor uses
37
- * `deviceId` for per-device overrides and traces, and `engine` to select
38
- * the factory (when absent, falls back to the executor's persisted
39
- * selection during the migration window).
36
+ * `deviceId` is an optional contextual hint — the executor uses it for
37
+ * per-device overrides and traces. There is no `engine` (D646): the pool is
38
+ * the node's own engine, or the `deviceKey` pool when one is named.
40
39
  */
41
40
  export interface PipelineRunInput {
42
41
  readonly steps: readonly PipelineStepInput[];
43
- readonly engine?: PipelineEngineChoice;
44
42
  readonly frame?: FrameInput;
45
43
  /** Same-process lazy source; never forwarded over UDS/Moleculer. */
46
44
  readonly frameRef?: FrameRef;
@@ -93,6 +91,16 @@ export interface PipelineRunInput {
93
91
  * mutually-exclusive image inputs. Absent ⇒ tile-crop children.
94
92
  */
95
93
  readonly nativeCropRef?: NativeCropRef;
94
+ /**
95
+ * Offline REPLAY of stored media for `deviceId` (D56, D646). The call runs
96
+ * the camera's structure — its detection-stage zone gate and per-device
97
+ * overrides, exactly as a live frame — over a recorded image, and it is
98
+ * PINNED: every executing step runs the model it names on the `deviceKey`
99
+ * pool it names, or the call refuses (`model-not-servable` /
100
+ * `device-not-servable`) before any pool is touched. Nothing is emitted on
101
+ * the camera's live trace stream. Absent ⇒ unchanged behaviour.
102
+ */
103
+ readonly replay?: true;
96
104
  }
97
105
  /**
98
106
  * Singleton capability provided by the pipeline-executor addon.
@@ -121,6 +129,8 @@ export interface IPipelineExecutorProvider {
121
129
  */
122
130
  validatePipeline(input: {
123
131
  readonly steps: readonly PipelineStepInput[];
132
+ /** Validate against this device pool's format; absent ⇒ the node default. */
133
+ readonly deviceKey?: string;
124
134
  }): Promise<PipelineValidationResult>;
125
135
  /** Get the currently selected engine (bootstrap default on the node). */
126
136
  getSelectedEngine(): Promise<PipelineEngineChoice>;
@@ -172,7 +182,6 @@ export interface IPipelineExecutorProvider {
172
182
  */
173
183
  runPipelineBatch(input: {
174
184
  readonly steps: readonly PipelineStepInput[];
175
- readonly engine?: PipelineEngineChoice;
176
185
  readonly frames: readonly FrameInput[];
177
186
  readonly deviceId?: number;
178
187
  readonly sessionId?: string;
@@ -47,7 +47,7 @@ export interface CoralInfo {
47
47
  }
48
48
  /** A probed backend with availability and score */
49
49
  export interface PlatformScore {
50
- readonly runtime: 'node' | 'python';
50
+ readonly runtime: 'python';
51
51
  readonly backend: string;
52
52
  readonly format: 'onnx' | 'coreml' | 'openvino' | 'tflite';
53
53
  readonly score: number;
@@ -77,7 +77,7 @@ export interface ModelRequirement {
77
77
  /** Resolved config — the tripletta chosen by the scorer for an addon */
78
78
  export interface ResolvedInferenceConfig {
79
79
  readonly modelId: string;
80
- readonly runtime: 'node' | 'python';
80
+ readonly runtime: 'python';
81
81
  readonly backend: string;
82
82
  readonly format: import('../types/models.js').ModelFormat;
83
83
  readonly reason: string;
@@ -18,11 +18,11 @@ export declare const DecoderSessionConfigSchema: z.ZodObject<{
18
18
  codec: z.ZodString;
19
19
  maxFps: z.ZodDefault<z.ZodNumber>;
20
20
  outputFormat: z.ZodDefault<z.ZodEnum<{
21
- rgb: "rgb";
22
- gray: "gray";
23
21
  jpeg: "jpeg";
22
+ rgb: "rgb";
24
23
  bgr: "bgr";
25
24
  yuv420: "yuv420";
25
+ gray: "gray";
26
26
  }>>;
27
27
  scale: z.ZodDefault<z.ZodNumber>;
28
28
  width: z.ZodOptional<z.ZodNumber>;
package/dist/node.d.ts CHANGED
@@ -13,6 +13,7 @@ export { FfmpegProcess } from './ffmpeg/process.js';
13
13
  export type { ChildCostClaimHandle, ChildCostClaimInput, ProcStatReader, } from './process/child-cost-registry.js';
14
14
  export { ChildCostRegistry, NO_COST_CLAIM, nodeProcStatReader, parseProcCpuSeconds, parseProcRssBytes, readProcessCost, } from './process/child-cost-registry.js';
15
15
  export { FilesystemStorageProvider } from './storage/filesystem-storage-provider.js';
16
+ export { writeFileAtomic } from './storage/atomic-write.js';
16
17
  export type { PhysicalRoot, RealpathSync } from './storage/physical-root.js';
17
18
  export { containsOrEquals, physicalRootOf } from './storage/physical-root.js';
18
19
  export { canonicalHash } from './utils/canonical-hash.js';
package/dist/node.js CHANGED
@@ -1744,6 +1744,69 @@ var ChildCostRegistry = class {
1744
1744
  }
1745
1745
  };
1746
1746
  //#endregion
1747
+ //#region src/storage/atomic-write.ts
1748
+ /** Errors a Windows rename over a file another process has open can raise. */
1749
+ var WINDOWS_RENAME_BUSY = new Set([
1750
+ "EPERM",
1751
+ "EACCES",
1752
+ "EBUSY"
1753
+ ]);
1754
+ var WINDOWS_RENAME_RETRIES = 3;
1755
+ var WINDOWS_RENAME_RETRY_MS = 25;
1756
+ function errnoCode(err) {
1757
+ return err instanceof Error && "code" in err && typeof err.code === "string" ? err.code : void 0;
1758
+ }
1759
+ /**
1760
+ * Replace `filePath` with `data` so that a concurrent reader sees EITHER the
1761
+ * old complete file OR the new complete file — never a prefix (D654).
1762
+ *
1763
+ * A plain `writeFile` truncates the target and then writes it in chunks, so a
1764
+ * reader in between gets a torn file: a fixed-path single-instance media kind
1765
+ * (`lastFrame`, `lastSceneFrame`) is overwritten every roll while a
1766
+ * notification may be reading it. Here the bytes go to a temp file in the SAME
1767
+ * directory (same filesystem, so the rename cannot degrade to a copy), and a
1768
+ * `rename` swaps it in — atomic on POSIX. A reader that already opened the old
1769
+ * file keeps reading the old inode to the end.
1770
+ *
1771
+ * The temp name is DOT-prefixed so every directory scan that skips dot entries
1772
+ * (occupancy, D327) never counts it; it is removed on any failure. A process
1773
+ * killed between the write and the rename leaves one `.<name>.<uuid>.tmp`.
1774
+ *
1775
+ * Windows: a rename over a file another process holds open can fail with
1776
+ * EPERM/EACCES/EBUSY. It is retried briefly and then falls back to copying the
1777
+ * temp over the target — non-atomic, exactly the pre-D654 behaviour, rather
1778
+ * than losing the write.
1779
+ *
1780
+ * Cost: one extra `rename` (and, on failure, one `rm`) per write — a metadata
1781
+ * operation, independent of the payload size.
1782
+ */
1783
+ async function writeFileAtomic(filePath, data) {
1784
+ const tmp = node_path.join(node_path.dirname(filePath), `.${node_path.basename(filePath)}.${(0, node_crypto.randomUUID)()}.tmp`);
1785
+ try {
1786
+ await node_fs.promises.writeFile(tmp, data);
1787
+ await renameOver(tmp, filePath);
1788
+ } catch (err) {
1789
+ await node_fs.promises.rm(tmp, { force: true }).catch(() => void 0);
1790
+ throw err;
1791
+ }
1792
+ }
1793
+ async function renameOver(tmp, filePath) {
1794
+ for (let attempt = 0;; attempt += 1) try {
1795
+ await node_fs.promises.rename(tmp, filePath);
1796
+ return;
1797
+ } catch (err) {
1798
+ const code = errnoCode(err);
1799
+ if (!(process.platform === "win32" && code !== void 0 && WINDOWS_RENAME_BUSY.has(code))) throw err;
1800
+ if (attempt < WINDOWS_RENAME_RETRIES) {
1801
+ await new Promise((resolve) => setTimeout(resolve, WINDOWS_RENAME_RETRY_MS));
1802
+ continue;
1803
+ }
1804
+ await node_fs.promises.copyFile(tmp, filePath);
1805
+ await node_fs.promises.rm(tmp, { force: true });
1806
+ return;
1807
+ }
1808
+ }
1809
+ //#endregion
1747
1810
  //#region src/storage/filesystem-storage-provider.ts
1748
1811
  var STORAGE_LOCATION_TYPES = [
1749
1812
  "data",
@@ -1817,14 +1880,16 @@ var FilesystemStorageProvider = class {
1817
1880
  relativePath
1818
1881
  });
1819
1882
  await node_fs.promises.mkdir(node_path.dirname(filePath), { recursive: true });
1820
- if (Buffer.isBuffer(data)) await node_fs.promises.writeFile(filePath, data);
1883
+ if (Buffer.isBuffer(data)) await writeFileAtomic(filePath, data);
1821
1884
  else {
1822
- const writeStream = node_fs.createWriteStream(filePath);
1823
- await new Promise((resolve, reject) => {
1824
- data.pipe(writeStream);
1825
- writeStream.on("finish", resolve);
1826
- writeStream.on("error", reject);
1827
- });
1885
+ const tmp = node_path.join(node_path.dirname(filePath), `.${node_path.basename(filePath)}.${(0, node_crypto.randomUUID)()}.tmp`);
1886
+ try {
1887
+ await (0, node_stream_promises.pipeline)(data, node_fs.createWriteStream(tmp));
1888
+ await node_fs.promises.rename(tmp, filePath);
1889
+ } catch (err) {
1890
+ await node_fs.promises.rm(tmp, { force: true }).catch(() => void 0);
1891
+ throw err;
1892
+ }
1828
1893
  }
1829
1894
  }
1830
1895
  async read({ location, relativePath }) {
@@ -2090,3 +2155,4 @@ exports.readProcessCost = readProcessCost;
2090
2155
  exports.resolveExportFingerprint = resolveExportFingerprint;
2091
2156
  exports.signExpiringUrl = signExpiringUrl;
2092
2157
  exports.verifyExpiringUrl = verifyExpiringUrl;
2158
+ exports.writeFileAtomic = writeFileAtomic;
package/dist/node.mjs CHANGED
@@ -1721,6 +1721,69 @@ var ChildCostRegistry = class {
1721
1721
  }
1722
1722
  };
1723
1723
  //#endregion
1724
+ //#region src/storage/atomic-write.ts
1725
+ /** Errors a Windows rename over a file another process has open can raise. */
1726
+ var WINDOWS_RENAME_BUSY = new Set([
1727
+ "EPERM",
1728
+ "EACCES",
1729
+ "EBUSY"
1730
+ ]);
1731
+ var WINDOWS_RENAME_RETRIES = 3;
1732
+ var WINDOWS_RENAME_RETRY_MS = 25;
1733
+ function errnoCode(err) {
1734
+ return err instanceof Error && "code" in err && typeof err.code === "string" ? err.code : void 0;
1735
+ }
1736
+ /**
1737
+ * Replace `filePath` with `data` so that a concurrent reader sees EITHER the
1738
+ * old complete file OR the new complete file — never a prefix (D654).
1739
+ *
1740
+ * A plain `writeFile` truncates the target and then writes it in chunks, so a
1741
+ * reader in between gets a torn file: a fixed-path single-instance media kind
1742
+ * (`lastFrame`, `lastSceneFrame`) is overwritten every roll while a
1743
+ * notification may be reading it. Here the bytes go to a temp file in the SAME
1744
+ * directory (same filesystem, so the rename cannot degrade to a copy), and a
1745
+ * `rename` swaps it in — atomic on POSIX. A reader that already opened the old
1746
+ * file keeps reading the old inode to the end.
1747
+ *
1748
+ * The temp name is DOT-prefixed so every directory scan that skips dot entries
1749
+ * (occupancy, D327) never counts it; it is removed on any failure. A process
1750
+ * killed between the write and the rename leaves one `.<name>.<uuid>.tmp`.
1751
+ *
1752
+ * Windows: a rename over a file another process holds open can fail with
1753
+ * EPERM/EACCES/EBUSY. It is retried briefly and then falls back to copying the
1754
+ * temp over the target — non-atomic, exactly the pre-D654 behaviour, rather
1755
+ * than losing the write.
1756
+ *
1757
+ * Cost: one extra `rename` (and, on failure, one `rm`) per write — a metadata
1758
+ * operation, independent of the payload size.
1759
+ */
1760
+ async function writeFileAtomic(filePath, data) {
1761
+ const tmp = path$1.join(path$1.dirname(filePath), `.${path$1.basename(filePath)}.${randomUUID()}.tmp`);
1762
+ try {
1763
+ await fs.promises.writeFile(tmp, data);
1764
+ await renameOver(tmp, filePath);
1765
+ } catch (err) {
1766
+ await fs.promises.rm(tmp, { force: true }).catch(() => void 0);
1767
+ throw err;
1768
+ }
1769
+ }
1770
+ async function renameOver(tmp, filePath) {
1771
+ for (let attempt = 0;; attempt += 1) try {
1772
+ await fs.promises.rename(tmp, filePath);
1773
+ return;
1774
+ } catch (err) {
1775
+ const code = errnoCode(err);
1776
+ if (!(process.platform === "win32" && code !== void 0 && WINDOWS_RENAME_BUSY.has(code))) throw err;
1777
+ if (attempt < WINDOWS_RENAME_RETRIES) {
1778
+ await new Promise((resolve) => setTimeout(resolve, WINDOWS_RENAME_RETRY_MS));
1779
+ continue;
1780
+ }
1781
+ await fs.promises.copyFile(tmp, filePath);
1782
+ await fs.promises.rm(tmp, { force: true });
1783
+ return;
1784
+ }
1785
+ }
1786
+ //#endregion
1724
1787
  //#region src/storage/filesystem-storage-provider.ts
1725
1788
  var STORAGE_LOCATION_TYPES = [
1726
1789
  "data",
@@ -1794,14 +1857,16 @@ var FilesystemStorageProvider = class {
1794
1857
  relativePath
1795
1858
  });
1796
1859
  await fs.promises.mkdir(path$1.dirname(filePath), { recursive: true });
1797
- if (Buffer.isBuffer(data)) await fs.promises.writeFile(filePath, data);
1860
+ if (Buffer.isBuffer(data)) await writeFileAtomic(filePath, data);
1798
1861
  else {
1799
- const writeStream = fs.createWriteStream(filePath);
1800
- await new Promise((resolve, reject) => {
1801
- data.pipe(writeStream);
1802
- writeStream.on("finish", resolve);
1803
- writeStream.on("error", reject);
1804
- });
1862
+ const tmp = path$1.join(path$1.dirname(filePath), `.${path$1.basename(filePath)}.${randomUUID()}.tmp`);
1863
+ try {
1864
+ await pipeline(data, fs.createWriteStream(tmp));
1865
+ await fs.promises.rename(tmp, filePath);
1866
+ } catch (err) {
1867
+ await fs.promises.rm(tmp, { force: true }).catch(() => void 0);
1868
+ throw err;
1869
+ }
1805
1870
  }
1806
1871
  }
1807
1872
  async read({ location, relativePath }) {
@@ -2033,4 +2098,4 @@ function resolveExportFingerprint(input) {
2033
2098
  return input.persisted ?? input.fresh;
2034
2099
  }
2035
2100
  //#endregion
2036
- export { ChildCostRegistry, FFMPEG_RELEASE, FfmpegProcess, FilesystemStorageProvider, Fmp4FragmentChild, Fmp4FragmentPlane, NO_COST_CLAIM, PYTHON_VERSION, buildBinaryPath, canonicalDeviceFingerprint, canonicalHash, containsOrEquals, diffExportTargets, downloadBinary, ensureBinary, ensureFfmpeg, ensurePython, findInPath, getFfmpegArtifact, getFfmpegDownloadUrl, getPlatformInfo, getPythonDownloadUrl, installPythonPackages, installPythonRequirements, nodeProcStatReader, parseFfmpegCapabilities, parseProcCpuSeconds, parseProcRssBytes, physicalRootOf, probeFfmpegBinary, readProcessCost, resolveExportFingerprint, signExpiringUrl, verifyExpiringUrl };
2101
+ export { ChildCostRegistry, FFMPEG_RELEASE, FfmpegProcess, FilesystemStorageProvider, Fmp4FragmentChild, Fmp4FragmentPlane, NO_COST_CLAIM, PYTHON_VERSION, buildBinaryPath, canonicalDeviceFingerprint, canonicalHash, containsOrEquals, diffExportTargets, downloadBinary, ensureBinary, ensureFfmpeg, ensurePython, findInPath, getFfmpegArtifact, getFfmpegDownloadUrl, getPlatformInfo, getPythonDownloadUrl, installPythonPackages, installPythonRequirements, nodeProcStatReader, parseFfmpegCapabilities, parseProcCpuSeconds, parseProcRssBytes, physicalRootOf, probeFfmpegBinary, readProcessCost, resolveExportFingerprint, signExpiringUrl, verifyExpiringUrl, writeFileAtomic };
@@ -0,0 +1,83 @@
1
+ /**
2
+ * The STATE SUMMARY delivery of a system-event rule (D647): which rules may use
3
+ * it, and how an editor presents its schedule. One module, read by the hub's
4
+ * save guard, the notification centre's scheduler and both rule editors.
5
+ *
6
+ * A `device-events` or `system` rule may say `systemEventDelivery: { mode:
7
+ * 'summary', schedule }`: it then sends nothing per event, and at the END of
8
+ * each window of `schedule` it sends the current state of its kinds. The alarm
9
+ * stays instant (an alarm summarised at 08:00 is an alarm nobody heard), and a
10
+ * `mixed` rule is read-only (D637), so neither may carry it.
11
+ *
12
+ * Pure: no clock, no I/O.
13
+ */
14
+ import type { NcDelivery, NcRuleActions, NcSchedule, NcSystemEventCondition, NcSystemEventDelivery } from '../capabilities/notification-rules.cap.js';
15
+ import { type NcSystemEventFamily } from './system-event-families.js';
16
+ /** The families a rule may summarise. The alarm is not one of them. */
17
+ export declare const NC_SYSTEM_EVENT_SUMMARY_FAMILIES: readonly NcSystemEventFamily[];
18
+ /** The part of a rule the summary delivery is judged on. */
19
+ export interface NcSystemEventDeliverySubject {
20
+ readonly delivery: NcDelivery;
21
+ readonly conditions: {
22
+ readonly systemEvent?: NcSystemEventCondition;
23
+ };
24
+ readonly systemEventDelivery?: NcSystemEventDelivery;
25
+ readonly schedule?: NcSchedule;
26
+ readonly actions?: NcRuleActions;
27
+ }
28
+ export type NcSystemEventDeliveryProblem = {
29
+ readonly code: 'not-a-system-rule';
30
+ readonly delivery: NcDelivery;
31
+ } | {
32
+ readonly code: 'family-not-summarisable';
33
+ readonly family: NcSystemEventFamily | 'mixed';
34
+ } | {
35
+ readonly code: 'empty-schedule';
36
+ } | {
37
+ readonly code: 'degenerate-window';
38
+ readonly index: number;
39
+ } | {
40
+ readonly code: 'schedule-beside-summary';
41
+ } | {
42
+ readonly code: 'actions-beside-summary';
43
+ };
44
+ /** The summary's schedule, or `null` for any rule that is not a summary system rule. */
45
+ export declare function systemEventSummaryScheduleOf(rule: Pick<NcSystemEventDeliverySubject, 'delivery' | 'systemEventDelivery'>): NcSchedule | null;
46
+ /** Absent and `instant` are the same answer: a notification per event. */
47
+ export declare function isSystemEventSummaryRule(rule: Pick<NcSystemEventDeliverySubject, 'delivery' | 'systemEventDelivery'>): boolean;
48
+ /**
49
+ * Why the hub refuses this rule's delivery. Empty = accepted. An instant rule
50
+ * (absent or `{ mode: 'instant' }`) is never refused here.
51
+ *
52
+ * Beside a summary, two fields are refused because they would do nothing:
53
+ * the rule's own `schedule` (the summary's schedule is the send time; a second
54
+ * active-hours gate would silently empty it) and `actions` (a summary is not a
55
+ * match, so nothing would ever run them).
56
+ */
57
+ export declare function systemEventDeliveryProblems(rule: NcSystemEventDeliverySubject): readonly NcSystemEventDeliveryProblem[];
58
+ export declare function describeSystemEventDeliveryProblem(problem: NcSystemEventDeliveryProblem): string;
59
+ /** One send time as an editor presents it: the days it is sent on, and the minute. */
60
+ export interface NcSummarySendTime {
61
+ readonly days: readonly number[];
62
+ /** Minute of the day, 0–1439. */
63
+ readonly minute: number;
64
+ }
65
+ /**
66
+ * A send time is a ONE-MINUTE window that closes at that minute: the summary is
67
+ * sent when a window ends. Midnight starts the window at 23:59 of the day
68
+ * BEFORE, because a window's `days` are the days it STARTS on.
69
+ */
70
+ export declare function scheduleForSendTimes(times: readonly NcSummarySendTime[], timezone?: string): NcSchedule;
71
+ /**
72
+ * The send times a schedule stands for, or `null` when it is not a list of
73
+ * one-minute windows (an inverted or a longer window): the editor then shows
74
+ * the raw windows and says the summary is sent when each one ENDS.
75
+ */
76
+ export declare function sendTimesOfSchedule(schedule: NcSchedule): readonly NcSummarySendTime[] | null;
77
+ /**
78
+ * Does a custom BODY carry the summary's content (`{{summaryText}}` or a
79
+ * section variable)? A body that does not would send a summary with nothing
80
+ * in it; the hub then appends `{{summaryText}}` and the admin warns (review
81
+ * M3). An absent or blank body uses the hub's own wording, which does.
82
+ */
83
+ export declare function templateCarriesSummary(body: string | undefined): boolean;
@@ -36,6 +36,21 @@ export declare const NC_TEMPLATE_HELD_BACK_SYSTEM_EVENT_KINDS: readonly NcSystem
36
36
  * rule an installed viewer can author, which is exactly true.
37
37
  */
38
38
  export declare function servedTemplateVars(): NcTemplateVarDescriptor[];
39
+ /**
40
+ * Is this descriptor held back from the SERVED catalog as a whole (D647)?
41
+ * A summary-only variable is: an installed viewer does not read the delivery
42
+ * axis, and would offer it on every instant system rule. The admin, built
43
+ * from the workspace, restores it from its own `NC_TEMPLATE_VARS`.
44
+ */
45
+ export declare function isHeldBackTemplateVar(d: NcTemplateVarDescriptor): boolean;
46
+ /**
47
+ * Does a descriptor have a value on a system rule of this DELIVERY (D647)?
48
+ * Read only for a system rule kind. An absent axis is `instant` for a
49
+ * descriptor that names `systemEventKinds` (a per-event fact: a summary row
50
+ * has no one device, node or package) and BOTH for one that does not
51
+ * (`{{rule}}`, `{{time}}`).
52
+ */
53
+ export declare function templateVarDeliveryApplies(d: NcTemplateVarDescriptor, ctx: NcTemplateVarContext): boolean;
39
54
  /**
40
55
  * Does a descriptor declared for `declared` kinds have a value for a rule of
41
56
  * `kind`?
@@ -1,3 +1,8 @@
1
+ /**
2
+ * Why the orchestrator cannot say which model the operator chose for a
3
+ * cluster-scoped step (D651).
4
+ */
5
+ export declare const CLUSTER_MODEL_CHOICE_UNREADABLE_REASONS: readonly ["owner-not-ready", "store-read-failed", "three-way-read-unavailable", "invalid-stored", "not-cluster-scoped"];
1
6
  /**
2
7
  * How a step's model is chosen.
3
8
  *
@@ -66,13 +71,58 @@ export declare function isClusterScopedStep(stepId: string): boolean;
66
71
  export type ClusterStepModels = Readonly<Record<string, string>>;
67
72
  /** The cluster row when nobody has configured one — today's catalog defaults. */
68
73
  export declare const DEFAULT_CLUSTER_STEP_MODELS: ClusterStepModels;
74
+ /**
75
+ * What ONE stored `clusterModel:<stepId>` value means (D651 fix round 4).
76
+ *
77
+ * - `absent` — nobody chose. The key was never written, or the settings form
78
+ * cleared it: a select clears a field with a `null` patch, and `hydrateSchema`
79
+ * reads a stored `null` as `value ?? default` on every hydrated surface, so
80
+ * `null` and `''` are the same statement as "never written".
81
+ * - `chosen` — a non-empty string, taken VERBATIM. Not checked against the
82
+ * step's `options`: the runtime pins an id the catalog does not list on
83
+ * purpose (`cluster-model-resolution.ts`: a custom encoder from
84
+ * `custom-model-registry` is the operator's own compatibility problem), and
85
+ * the dropdown is guarded separately by `scripts/check-cluster-model-scope.ts`.
86
+ * An owner that called such an id invalid while the runner ran it would give
87
+ * the same stored key two meanings — the defect this function removes.
88
+ * - `invalid` — anything else (a number, an object). The form cannot write it;
89
+ * it is a hand edit or a corrupted row.
90
+ *
91
+ * Both readers of the key go through this function: the runner's
92
+ * {@link readClusterStepModels} and the owner's `decideClusterModelChoice`
93
+ * (addon-pipeline-orchestrator). They differ only in what they DO with
94
+ * `invalid`: a live row is total, so the runner falls to the catalog default
95
+ * and says so in its log (`ClusterModelSource`); a pass that would rewrite an
96
+ * index on it refuses (`invalid-stored`).
97
+ */
98
+ export type StoredClusterModelInterpretation = {
99
+ readonly kind: 'absent';
100
+ } | {
101
+ readonly kind: 'chosen';
102
+ readonly modelId: string;
103
+ } | {
104
+ readonly kind: 'invalid';
105
+ readonly value: unknown;
106
+ };
107
+ export declare function interpretStoredClusterModel(raw: unknown): StoredClusterModelInterpretation;
108
+ /** One stored cluster model value the interpretation rejected. */
109
+ export interface InvalidClusterStepModelEntry {
110
+ readonly stepId: string;
111
+ readonly value: unknown;
112
+ }
113
+ /**
114
+ * The cluster-scoped steps whose stored value is `invalid` under
115
+ * {@link interpretStoredClusterModel} — for the runtime's log line, so the
116
+ * default it runs on such a row is never run in silence.
117
+ */
118
+ export declare function invalidClusterStepModelEntries(config: Readonly<Record<string, unknown>>): readonly InvalidClusterStepModelEntry[];
69
119
  /**
70
120
  * Narrow a FLAT settings record to the cluster row.
71
121
  *
72
122
  * Per-FIELD fallback, deliberately (same rule as `readDetailCropConvention`): a
73
- * junk face model must not also discard a valid clip model. An absent, empty or
74
- * non-string value resolves to the step's catalog default — the historical
75
- * behaviour — never to a blank id, because a blank id downstream becomes
123
+ * junk face model must not also discard a valid clip model. An `absent` or
124
+ * `invalid` value ({@link interpretStoredClusterModel}) resolves to the step's
125
+ * catalog default — never to a blank id, because a blank id downstream becomes
76
126
  * "substitute the format default", which is precisely the substitution this
77
127
  * scope exists to forbid.
78
128
  */
@@ -98,6 +148,12 @@ export interface HydratedClusterView {
98
148
  * (addon mid-boot) is the defaults.
99
149
  */
100
150
  export declare function pickClusterStepModels(view: HydratedClusterView | null): ClusterStepModels;
151
+ /**
152
+ * The `invalid` stored cluster model values a hydrated view carries — what the
153
+ * runtime logs when it runs a default on a row it could not read as a choice.
154
+ * A `null` view (owner mid-boot) carries nothing to judge.
155
+ */
156
+ export declare function pickInvalidClusterStepModels(view: HydratedClusterView | null): readonly InvalidClusterStepModelEntry[];
101
157
  /**
102
158
  * The cluster model for `stepId`, or `null` when the step is node-scoped.
103
159
  *