@camstack/system 1.2.313 → 1.2.315

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 (111) hide show
  1. package/dist/addon-runner.js +1 -1
  2. package/dist/addon-runner.mjs +1 -1
  3. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
  4. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
  5. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
  6. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
  7. package/dist/builtins/alerts/alerts.addon.js +1 -1
  8. package/dist/builtins/alerts/alerts.addon.mjs +1 -1
  9. package/dist/builtins/autotrack/index.js +1 -1
  10. package/dist/builtins/autotrack/index.mjs +1 -1
  11. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +1 -1
  12. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +1 -1
  13. package/dist/builtins/camera-grid/index.js +1 -1
  14. package/dist/builtins/camera-grid/index.mjs +1 -1
  15. package/dist/builtins/composer/claim-gate.d.ts +32 -0
  16. package/dist/builtins/composer/composed-device-set.d.ts +38 -0
  17. package/dist/builtins/composer/composed-device.d.ts +4 -3
  18. package/dist/builtins/composer/composer-grafts.d.ts +54 -0
  19. package/dist/builtins/composer/composer-plan.d.ts +96 -0
  20. package/dist/builtins/composer/composer-verdict.d.ts +19 -0
  21. package/dist/builtins/composer/composer.addon.js +2717 -477
  22. package/dist/builtins/composer/composer.addon.mjs +2722 -482
  23. package/dist/builtins/composer/composer.d.ts +71 -52
  24. package/dist/builtins/composer/composition-runtime.d.ts +93 -9
  25. package/dist/builtins/composer/confirmed-seed.d.ts +32 -0
  26. package/dist/builtins/composer/existing-target.d.ts +96 -0
  27. package/dist/builtins/composer/field-record.d.ts +50 -0
  28. package/dist/builtins/composer/graft-host.d.ts +47 -0
  29. package/dist/builtins/composer/held-claims.d.ts +12 -0
  30. package/dist/builtins/composer/owned-fields-target.d.ts +55 -0
  31. package/dist/builtins/composer/slice-assembler.d.ts +10 -0
  32. package/dist/builtins/composer/source-readings.d.ts +122 -0
  33. package/dist/builtins/composer/source-tracker.d.ts +35 -42
  34. package/dist/builtins/console-logging/index.js +1 -1
  35. package/dist/builtins/console-logging/index.mjs +1 -1
  36. package/dist/builtins/core-blocks/composition-api.d.ts +7 -4
  37. package/dist/builtins/core-blocks/composition-peers.d.ts +9 -0
  38. package/dist/builtins/core-blocks/composition-sources.d.ts +16 -3
  39. package/dist/builtins/core-blocks/core-block-store.d.ts +8 -1
  40. package/dist/builtins/core-blocks/core-blocks.addon.d.ts +4 -3
  41. package/dist/builtins/core-blocks/core-blocks.addon.js +255 -79
  42. package/dist/builtins/core-blocks/core-blocks.addon.mjs +255 -79
  43. package/dist/builtins/device-manager/claimed-status-overlay.d.ts +12 -0
  44. package/dist/builtins/device-manager/device-manager.addon.d.ts +16 -0
  45. package/dist/builtins/device-manager/device-manager.addon.js +1825 -317
  46. package/dist/builtins/device-manager/device-manager.addon.mjs +1825 -317
  47. package/dist/builtins/device-manager/device-provider-context.d.ts +24 -2
  48. package/dist/builtins/device-manager/device-row-store.d.ts +2 -0
  49. package/dist/builtins/device-manager/device-state-claim-views.d.ts +77 -0
  50. package/dist/builtins/device-manager/device-state-claims.d.ts +43 -0
  51. package/dist/builtins/device-manager/device-state-mirror.d.ts +145 -61
  52. package/dist/builtins/device-manager/field-claims-index.d.ts +69 -0
  53. package/dist/builtins/device-manager/field-ownership.d.ts +53 -0
  54. package/dist/builtins/device-manager/migrate-device.d.ts +27 -0
  55. package/dist/builtins/device-manager/migrate-guard.d.ts +7 -0
  56. package/dist/builtins/device-manager/migrate-hardware-state.d.ts +55 -0
  57. package/dist/builtins/device-manager/migration-refused.d.ts +23 -0
  58. package/dist/builtins/device-manager/mirror-row-writer.d.ts +138 -0
  59. package/dist/builtins/device-manager/runtime-state-persist-gate.d.ts +4 -0
  60. package/dist/builtins/doorbell/binding-mirror.d.ts +1 -0
  61. package/dist/builtins/doorbell/doorbell-composition-migration.d.ts +59 -0
  62. package/dist/builtins/doorbell/virtual-doorbell.addon.d.ts +3 -0
  63. package/dist/builtins/doorbell/virtual-doorbell.addon.js +315 -10
  64. package/dist/builtins/doorbell/virtual-doorbell.addon.mjs +315 -10
  65. package/dist/builtins/hub-forwarder/index.js +1 -1
  66. package/dist/builtins/hub-forwarder/index.mjs +1 -1
  67. package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
  68. package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
  69. package/dist/builtins/local-auth/local-auth.addon.js +1 -1
  70. package/dist/builtins/local-auth/local-auth.addon.mjs +1 -1
  71. package/dist/builtins/local-network/local-network.addon.js +1 -1
  72. package/dist/builtins/local-network/local-network.addon.mjs +1 -1
  73. package/dist/builtins/loki-logging/index.js +1 -1
  74. package/dist/builtins/loki-logging/index.mjs +1 -1
  75. package/dist/builtins/native-metrics/native-metrics.addon.js +1 -1
  76. package/dist/builtins/native-metrics/native-metrics.addon.mjs +1 -1
  77. package/dist/builtins/platform-probe/index.js +1 -1
  78. package/dist/builtins/platform-probe/index.mjs +1 -1
  79. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
  80. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
  81. package/dist/builtins/snapshot/index.js +1 -1
  82. package/dist/builtins/snapshot/index.mjs +1 -1
  83. package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
  84. package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
  85. package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +0 -0
  86. package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +0 -0
  87. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +1 -1
  88. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +1 -1
  89. package/dist/builtins/system-config/system-config.addon.js +1 -1
  90. package/dist/builtins/system-config/system-config.addon.mjs +1 -1
  91. package/dist/builtins/winston-logging/index.js +1 -1
  92. package/dist/builtins/winston-logging/index.mjs +1 -1
  93. package/dist/{child-cap-dispatch-CHTiHbsv.mjs → child-cap-dispatch-AAltfBUH.mjs} +1484 -1417
  94. package/dist/{child-cap-dispatch-Dyn_7ezH.js → child-cap-dispatch-DzcUd_ct.js} +1503 -1424
  95. package/dist/composition-sources-DYqQLRzF.js +122 -0
  96. package/dist/composition-sources-yonsRneP.mjs +111 -0
  97. package/dist/{dist-CkRwnhbo.js → dist-DVKv5i-e.js} +5396 -4331
  98. package/dist/{dist-Cj7pJmXq.mjs → dist-MzYJVCLE.mjs} +5330 -4331
  99. package/dist/index.js +137 -8
  100. package/dist/index.mjs +133 -8
  101. package/dist/kernel/capability-registry.d.ts +43 -1
  102. package/dist/kernel/index.d.ts +3 -1
  103. package/dist/kernel/moleculer/device-cap-proxy.d.ts +0 -10
  104. package/dist/kernel/status-overlay.d.ts +18 -0
  105. package/dist/kernel/transport/claimed-get-status.d.ts +15 -0
  106. package/dist/kernel/transport/index.d.ts +2 -0
  107. package/dist/kernel/transport/local-child-registry.d.ts +23 -0
  108. package/dist/kernel/transport/parent-unowned-call.d.ts +25 -0
  109. package/dist/{retired-settings-keys-C1duPikd.mjs → retired-settings-keys-8yY2sTc-.mjs} +1 -1
  110. package/dist/{retired-settings-keys-CGSuXhP8.js → retired-settings-keys-iY5I0nKM.js} +1 -1
  111. package/package.json +1 -1
@@ -1,27 +1,25 @@
1
- import { CapabilityLookup, ComposerApplyInput, ComposerApplyResult, CompositionBlockState, DeviceDeclaration, DeviceType, IScopedLogger } from '@camstack/types';
1
+ import { CapabilityLookup, ComposerApplyInput, ComposerApplyResult, CompositionBlockState, IScopedLogger } from '@camstack/types';
2
2
  import { DeviceStateChangedData } from '../shared/device-state-changed.js';
3
- import { ComposedDeviceHandle } from './composed-device.js';
3
+ import { ComposerDeclare, ComposerDevicePorts } from './composed-device-set.js';
4
4
  import { PersistedComposedRow } from './composition-declarations.js';
5
- import { DeadlineTimer } from './composition-runtime.js';
5
+ import { CompositionEventEmitter, DeadlineTimer } from './composition-runtime.js';
6
+ import { ComposerClaimsApi, ComposerGraftPorts, DescribeTarget } from './existing-target.js';
6
7
  import { SourceBindingsChangeReason, SourceTracker } from './source-tracker.js';
8
+ export type { ComposerDerivedSpec } from './composer-plan.js';
9
+ export type { ComposerDeclareResult, ComposerDevicePorts } from './composed-device-set.js';
7
10
  /** What the composer reads of a `device.bindings-changed` payload; the typed `EventCatalog` payload is assignable to it. */
8
11
  export interface DeviceBindingsChangedData {
9
12
  readonly deviceId: number;
10
13
  readonly capName: string;
11
14
  readonly reason: SourceBindingsChangeReason;
15
+ /** The addon that registered (or unregistered) the provider: a foreign native vs the composer's own graft. */
16
+ readonly addonId: string;
12
17
  }
13
- export interface ComposerDerivedSpec {
14
- readonly type: DeviceType;
15
- readonly role?: string;
16
- }
17
- export interface ComposerDevicePorts {
18
- setName(deviceId: number, name: string): Promise<void>;
19
- decommission(deviceId: number): Promise<void>;
20
- applyDerivedSpec(deviceId: number, derived: ComposerDerivedSpec): Promise<void>;
21
- }
22
- export interface ComposerDeclareResult {
23
- /** Live composed devices by stableId. */
24
- readonly devices: ReadonlyMap<string, ComposedDeviceHandle>;
18
+ /** The owner-only `device.native-shadow-changed` payload (D224): the provider's slice under a claim. */
19
+ export interface NativeShadowChangedData {
20
+ readonly deviceId: number;
21
+ readonly capName: string;
22
+ readonly native: Readonly<Record<string, unknown>>;
25
23
  }
26
24
  export interface ComposerDeps {
27
25
  readonly logger: IScopedLogger;
@@ -29,75 +27,96 @@ export interface ComposerDeps {
29
27
  readonly lookupCap: CapabilityLookup;
30
28
  /** The composed rows that already exist (`listOwnDevices`), read once before the first withdrawal decision. */
31
29
  readonly readPersisted: () => Promise<readonly PersistedComposedRow[]>;
32
- readonly declare: (integrationId: string, devices: readonly DeviceDeclaration[]) => Promise<ComposerDeclareResult>;
30
+ readonly declare: ComposerDeclare;
33
31
  readonly devices: ComposerDevicePorts;
32
+ /** The hub's per-field claims (D663): the write target's methods and the boot seed. */
33
+ readonly claims: ComposerClaimsApi;
34
+ /** Typed state-derived events emitted by composed capability fields. */
35
+ readonly events: CompositionEventEmitter;
36
+ /** Effective target state, read before an existing-target runtime owns any field. */
37
+ readonly readExistingTargetCap: (deviceId: number, capName: string) => Promise<Readonly<Record<string, unknown>> | null>;
38
+ /** ONE `getBindings` per target description read (rule 1). */
39
+ readonly describeTarget: DescribeTarget;
40
+ /** `GraftHost` (Task 10): a method-serving native for each ADDED cap. Absent, nothing is grafted. */
41
+ readonly grafts?: ComposerGraftPorts;
34
42
  readonly now: () => number;
35
43
  readonly newTimer: () => DeadlineTimer;
36
44
  }
37
45
  export declare class Composer {
38
46
  private readonly deps;
39
47
  private live;
40
- private devices;
41
- /** StableIds whose row exists: the seeded persisted rows, then what every declare kept. */
42
- private rows;
43
- private seeded;
44
- /** The previous seed read answered `[]`: one more `[]` confirms it (D49). */
45
- private emptySeedSeen;
48
+ /** The composed devices of `new`-target blocks, and the rows that exist. */
49
+ private readonly composed;
46
50
  /** Consecutive applies refused because `core_blocks` had not loaded: warned once per run. */
47
51
  private storeWaitApplies;
48
52
  /** Consecutive applies refused because the Blocks integration had not reconciled: warned once per run. */
49
53
  private integrationWaitApplies;
50
- private declaredFingerprint;
51
54
  private readonly gate;
55
+ private readonly claimGate;
56
+ private readonly targets;
57
+ private readonly grafts;
58
+ /** The rows the composer owns, read ONCE before its first withdrawal decision. */
59
+ private readonly rowSeed;
60
+ /**
61
+ * The claims the hub holds, read ONCE before the first release decision (rule
62
+ * 3). Every owner in it is a composer block id — the Task 6 guard lets nothing
63
+ * else claim. `not-loaded` is UNKNOWN (D315), never empty. Until seeded, every
64
+ * existing-target block waits by name; new-target blocks are not held.
65
+ */
66
+ private readonly claimSeed;
52
67
  /** Applies never interleave: each one reads and replaces the whole live set. */
53
68
  private applyChain;
69
+ /**
70
+ * Runtimes built by an apply and not started yet: that apply's sync starts
71
+ * them, with their sources tracked. An event in between never does (N-5) —
72
+ * it would start one on sources "not tracked yet".
73
+ */
74
+ private unsynced;
54
75
  constructor(deps: ComposerDeps);
55
76
  apply(input: ComposerApplyInput): Promise<ComposerApplyResult>;
56
77
  getStates(blockIds: readonly string[]): readonly CompositionBlockState[];
57
78
  /** Runs on the tracker's one chain (R3): resolves once the change reached every runtime. */
58
79
  onStateChanged(data: DeviceStateChangedData): Promise<void>;
80
+ onNativeChanged(data: NativeShadowChangedData): Promise<void>;
59
81
  onDeviceUnregistered(deviceId: number): Promise<void>;
60
82
  onBindingsChanged(data: DeviceBindingsChangedData): Promise<void>;
61
83
  shutdown(): void;
84
+ /** Run `work` after every apply already queued, whether that settled or failed. */
85
+ private chained;
62
86
  private applyNow;
63
87
  private waiting;
64
- /**
65
- * Seed the withdrawal gate with the rows that exist, ONCE, before its first
66
- * `filter`; answers null once seeded, else why this apply must wait.
67
- *
68
- * An EMPTY answer is the destroying direction: a cold registry answers `[]`
69
- * too, and `DeclaredDevices.reconcile` sweeps from its OWN later read — so a
70
- * cold seed followed by a warm sweep would delete the row of a block that no
71
- * longer plans, with no placeholder to hold it. `[]` is therefore trusted
72
- * only when the next apply's read agrees (D49); that apply comes from the
73
- * supervisor's existing converge, never a timer here (D3). A failed read
74
- * changes nothing and breaks the agreement.
75
- */
76
- private ensureSeeded;
77
- /**
78
- * A row that exists but does not parse is held exactly like a composition
79
- * that stopped planning: its device (if it has a row) stays as an offline
80
- * placeholder, and the block fails naming the parse error. Absent, it would
81
- * be withdrawn on the second apply that omitted it (D49).
82
- */
83
- private planUnreadable;
84
88
  private plan;
85
- /** One line when a block starts or stops planning — never one per apply. */
86
- private logPlanTransitions;
87
- /**
88
- * A changed type/role/cap set (or a switch to or from a placeholder) needs a
89
- * NEW device object: decommission, and let the reconcile adopt it with the
90
- * new class. A failure keeps the old device and is retried next apply.
91
- */
92
- private rebuildShapes;
93
- private reconcileDevices;
94
89
  private rebuildRuntimes;
90
+ private ungraftGone;
91
+ /** Where a block's runtime writes: its composed device, or a claim-backed target on the existing device. */
92
+ private writerFor;
93
+ /** C-1/N-1: the tracker follows the block's OWN claim as the hub answers it; every answer re-reads the native once. */
94
+ private onClaimEvent;
95
+ private heldFieldsFor;
95
96
  private newRuntime;
96
97
  private applyName;
97
98
  private syncSources;
98
99
  /** A failed sync leaves the tracker as it was: the runtimes read what it holds. */
99
100
  private syncTracker;
101
+ /**
102
+ * Rule 3: the claims every live customization WANTS this apply; everything
103
+ * else the gate knows is a release candidate, handed back once two applies
104
+ * agree. A block that cannot plan — waiting, unreadable, failed, or with its
105
+ * target gone — HOLDS its claims (rule 4, rule 5): they are wanted as they
106
+ * are. A disabled block wants none (rule 7).
107
+ */
108
+ private reconcileClaims;
109
+ private release;
110
+ /**
111
+ * Rule 5: a customization whose target row is gone — the tracker's two
112
+ * agreeing reads — fails naming it, stops writing, and ungrafts every cap it
113
+ * added on that id (Task 10's seam). Its claims are left as they are: the row
114
+ * and its `$owned` died with the device. No auto-repair: an operator edit or
115
+ * restart re-resolves the target.
116
+ */
117
+ private failGoneTargets;
100
118
  private dispatch;
119
+ private deviceIdOf;
101
120
  private tagsOf;
102
121
  private verdict;
103
122
  }
@@ -1,4 +1,7 @@
1
- import { CompositionFieldPlan, CompositionFieldState, ExpressionValue, IScopedLogger, SourceReader } from '@camstack/types';
1
+ import { CapabilityLookup, ClaimOutcome, CompositionEventPlan, CompositionFieldPlan, CompositionFieldState, IScopedLogger, SourceReader, SystemEvent } from '@camstack/types';
2
+ import { SlicePatch } from './slice-assembler.js';
3
+ import { ReleasedField } from './field-record.js';
4
+ export type { ReleasedField } from './field-record.js';
2
5
  export interface WriteOk {
3
6
  readonly ok: true;
4
7
  }
@@ -7,26 +10,55 @@ export interface WriteRefused {
7
10
  readonly error: string;
8
11
  }
9
12
  export type WriteOutcome = WriteOk | WriteRefused;
13
+ /** Why an enqueued write did not land. `refused` carries the hub's code so the target can escalate by name (S5). */
14
+ export type TargetAsyncFailure = {
15
+ readonly kind: 'threw';
16
+ readonly error: string;
17
+ } | {
18
+ readonly kind: 'refused';
19
+ readonly code: Extract<ClaimOutcome, {
20
+ ok: false;
21
+ }>['code'];
22
+ readonly message: string;
23
+ };
10
24
  /**
11
- * The composed device. A write answers `{ ok: false }` rather than throwing,
12
- * and `setAvailable` does not throw. The runtime survives either anyway: a
13
- * throwing write is a failed write, a throwing `setAvailable` is retried on
14
- * the next pass.
25
+ * Where a composition writes. A write answers `{ ok: false }` rather than
26
+ * throwing, and `setAvailable` does not throw. The runtime survives either
27
+ * anyway: a throwing write is a failed write, a throwing `setAvailable` is
28
+ * retried on the next pass. A target whose writes are asynchronous reports
29
+ * what did not land through `onAsyncFailure`.
15
30
  */
16
31
  export interface ComposedWriteTarget {
17
32
  readonly id: number;
18
- seed(capName: string, slice: Readonly<Record<string, ExpressionValue>>): WriteOutcome;
19
- patch(capName: string, patch: Readonly<Record<string, ExpressionValue>>): WriteOutcome;
33
+ seed(capName: string, slice: SlicePatch): WriteOutcome;
34
+ patch(capName: string, patch: SlicePatch): WriteOutcome;
20
35
  setAvailable(available: boolean): void;
36
+ onAsyncFailure?(listener: (cap: string, failure: TargetAsyncFailure) => void): void;
37
+ /**
38
+ * S1b: these fields of `cap` are NOT the composition's to write right now
39
+ * (their `release`-mode source is unavailable). The claim is narrowed to the
40
+ * rest; an empty rest releases the whole cap. `[]` re-widens. Absent on a
41
+ * `new`-target device (`ComposedDevice`): `release` is planned only on
42
+ * existing targets.
43
+ */
44
+ withhold?(cap: string, fields: readonly string[]): WriteOutcome;
21
45
  }
22
46
  export interface DeadlineTimer {
23
47
  arm(at: number, fire: () => void): void;
24
48
  disarm(): void;
25
49
  }
50
+ export interface CompositionEventEmitter {
51
+ emit(event: SystemEvent): void;
52
+ }
26
53
  export interface CompositionRuntimeDeps {
27
54
  readonly blockId: string;
28
55
  readonly fields: readonly CompositionFieldPlan[];
56
+ readonly eventPlans: readonly CompositionEventPlan[];
57
+ readonly events: CompositionEventEmitter;
29
58
  readonly target: ComposedWriteTarget;
59
+ readonly lookupCap: CapabilityLookup;
60
+ /** Effective target slices captured before an existing-target runtime takes ownership. */
61
+ readonly initialByCap?: ReadonlyMap<string, Readonly<Record<string, unknown>> | null>;
30
62
  readonly read: SourceReader;
31
63
  readonly now: () => number;
32
64
  readonly timer: DeadlineTimer;
@@ -36,10 +68,14 @@ export interface CompositionRuntimeState {
36
68
  readonly failure: string | null;
37
69
  readonly available: boolean;
38
70
  readonly fields: readonly CompositionFieldState[];
71
+ readonly released: readonly ReleasedField[];
72
+ /** `release`-mode fields HELD on their last value: unavailable, but no source has answered so (D49). */
73
+ readonly held: readonly ReleasedField[];
39
74
  }
40
75
  export declare class CompositionRuntime {
41
76
  private readonly deps;
42
77
  private records;
78
+ private readonly startedAtMs;
43
79
  private seeded;
44
80
  private available;
45
81
  private failure;
@@ -49,6 +85,16 @@ export declare class CompositionRuntime {
49
85
  private lastPassError;
50
86
  private pending;
51
87
  private heldBack;
88
+ /** Caps whose item array is not written because a required leaf has no value. */
89
+ private itemsHeld;
90
+ /** Record keys currently released (S1b), for transition logs and the report. */
91
+ private released;
92
+ /** Per cap, the top-level fields the target last accepted as withheld; absent = none. */
93
+ private withheld;
94
+ /** Per record key, how many times the field has been released — churn the transition line reports (D391). */
95
+ private releaseFlaps;
96
+ /** Caps the target refused for good (`TERMINAL_REFUSALS`): never written again by this runtime. */
97
+ private terminal;
52
98
  constructor(deps: CompositionRuntimeDeps);
53
99
  start(): void;
54
100
  onSourcesChanged(sliceKeys: ReadonlySet<string>): void;
@@ -56,6 +102,20 @@ export declare class CompositionRuntime {
56
102
  /** Terminal: no write, no evaluation and no timer after this. */
57
103
  stop(): void;
58
104
  state(): CompositionRuntimeState;
105
+ /**
106
+ * The target's enqueued write of `cap` did not land: the cap is write-pending
107
+ * by name and re-written WHOLE on the next pass that evaluates it (or the next
108
+ * deadline pass). A withhold sent to that target is forgotten too, so it is
109
+ * re-sent. No retry timer (D3).
110
+ */
111
+ private onTargetWriteFailed;
112
+ /**
113
+ * The target refused this cap for good: nothing on this side can retry it
114
+ * away (I-3). The cap is never written again by this runtime, its fields
115
+ * report the refusal, and the block fails naming it — only the operator ends
116
+ * this (edit or delete the block, or the competing one), like a gone source.
117
+ */
118
+ private markTerminal;
59
119
  /**
60
120
  * Every entry point runs here: a no-op once stopped, nothing thrown escapes,
61
121
  * and the one timer is re-armed from the stored deadlines whatever happened.
@@ -65,16 +125,40 @@ export declare class CompositionRuntime {
65
125
  private logPassError;
66
126
  private pass;
67
127
  private evaluate;
128
+ private emitStateEvent;
68
129
  private recordsOf;
69
130
  /** `retry`: write-pending caps this pass re-writes WHOLE, whether or not a value changed. */
70
131
  private writeCaps;
132
+ /**
133
+ * S1b: tell the target which fields of `cap` are released, when that set
134
+ * changed since it last accepted one. A released record forgets what it wrote,
135
+ * so the source returning writes it again. False when the target refused or
136
+ * threw: the cap is write-pending and the withhold is re-sent next pass.
137
+ */
138
+ private syncReleased;
139
+ /** One info line per field per direction, never one per pass. */
140
+ private logReleaseTransitions;
71
141
  private seedCap;
72
142
  private patchCap;
73
- /** After a failed write nothing is known about what landed: send every current value. */
143
+ /**
144
+ * After a failed write nothing is known about what landed, and after a
145
+ * changed withhold the claim must be re-stated: send every current value.
146
+ * `afterWithhold` with nothing left to write means the cap was released
147
+ * whole — a pending write is moot then, not retried (M-2).
148
+ */
74
149
  private patchWholeCap;
150
+ private dropPending;
151
+ /**
152
+ * What one write of `cap` carries: the writable `candidates` — and, when any
153
+ * of them is an item leaf, EVERY item leaf of the cap, because the array is
154
+ * written whole (a patch of `items` replaces it). A required item leaf with
155
+ * no value holds the whole array (its top-level siblings still go out).
156
+ */
157
+ private entriesFor;
75
158
  private writableEntries;
159
+ private holdItems;
76
160
  private holdBackSeed;
77
- /** One write. A refusal or a throw marks the cap write-pending; true when it landed. */
161
+ /** One write. A refusal or a throw marks the cap write-pending; true when the target took it. */
78
162
  private write;
79
163
  private clearPending;
80
164
  private markPending;
@@ -0,0 +1,32 @@
1
+ import { IScopedLogger } from '@camstack/types';
2
+ export interface SeedItems<T> {
3
+ readonly kind: 'items';
4
+ readonly items: readonly T[];
5
+ }
6
+ export interface SeedUnknown {
7
+ readonly kind: 'unknown';
8
+ readonly error: string;
9
+ }
10
+ export type SeedRead<T> = SeedItems<T> | SeedUnknown;
11
+ export interface ConfirmedSeedDeps<T> {
12
+ readonly logger: IScopedLogger;
13
+ /** What this seed is about, for its log lines: `rows`, `claims`. */
14
+ readonly subject: string;
15
+ readonly read: () => Promise<SeedRead<T>>;
16
+ readonly onSeeded: (items: readonly T[]) => void;
17
+ /** The reason a block waits while the read failed / answered `[]` once. */
18
+ readonly unreadReason: string;
19
+ readonly emptyUnconfirmedReason: string;
20
+ }
21
+ export declare class ConfirmedSeed<T> {
22
+ private readonly deps;
23
+ private seeded;
24
+ /** The previous read answered `[]`: one more `[]` confirms it (D49). */
25
+ private emptySeen;
26
+ /** Consecutive applies whose read did not answer: warned once per run. */
27
+ private waitApplies;
28
+ constructor(deps: ConfirmedSeedDeps<T>);
29
+ get done(): boolean;
30
+ /** Null once seeded, else why this apply must wait. */
31
+ ensure(): Promise<string | null>;
32
+ }
@@ -0,0 +1,96 @@
1
+ import { ClaimsListing, CompositionFeaturePlan, CompositionFieldPlan, CompositionFieldState, CompositionSourceRef, CompositionTargetFound, deviceStateCapability, DeviceType, FieldClaimMode, IScopedLogger } from '@camstack/types';
2
+ import { z } from 'zod';
3
+ import { ReleasedField } from './composition-runtime.js';
4
+ import { GraftOutcome } from './graft-host.js';
5
+ import { OwnedFieldsApi } from './owned-fields-target.js';
6
+ import { BoundCapsRead } from './source-tracker.js';
7
+ export type ListClaimsInput = z.infer<typeof deviceStateCapability.methods.listClaims.input>;
8
+ /** The hub's claim surface the composer uses: the write target's three methods plus the boot seed. */
9
+ export interface ComposerClaimsApi extends OwnedFieldsApi {
10
+ listClaims(input: ListClaimsInput): Promise<ClaimsListing>;
11
+ }
12
+ export interface ExistingTargetSeedRead {
13
+ readonly byCap: ReadonlyMap<string, Readonly<Record<string, unknown>> | null>;
14
+ }
15
+ /**
16
+ * Read every affected effective capability exactly once, before the composer
17
+ * grafts or claims any of them. A null slice is a successful empty seed; a
18
+ * rejection is deliberately left to the caller so ownership cannot begin.
19
+ */
20
+ export declare function readExistingTargetSeeds(targetDeviceId: number, caps: readonly string[], read: (deviceId: number, capName: string) => Promise<Readonly<Record<string, unknown>> | null>): Promise<ExistingTargetSeedRead>;
21
+ /**
22
+ * The composer's grafts (`GraftHost`, Task 10): a method-serving native for each
23
+ * cap a customization ADDS. Grafted at PLAN time only; removed when a foreign
24
+ * native appears, when the target is gone (Kernel ruling,
25
+ * `DeviceContext.unregisterNativeCap`), or when the block's add claim is
26
+ * released — else the runner keeps a native on that id and `LocalChildRegistry`
27
+ * keeps routing to it.
28
+ */
29
+ export interface ComposerGraftPorts {
30
+ graft(deviceId: number, stableId: string, capName: string): GraftOutcome;
31
+ ungraft(deviceId: number, capName: string, reason: string): void;
32
+ grafted(deviceId: number): ReadonlySet<string>;
33
+ /**
34
+ * Retract a binding a PREVIOUS incarnation of the composer left on `deviceId`
35
+ * (I-3): this process never registered it, so `ungraft` cannot reach it. The
36
+ * caller has read that the binding IS the composer's.
37
+ */
38
+ retractStale(deviceId: number, capName: string, reason: string): void;
39
+ }
40
+ /** The reason every ungraft of a gone target carries (Task 9 rule 5 / Task 10 rule 2). */
41
+ export declare const TARGET_GONE_UNGRAFT_REASON = "target device gone";
42
+ export interface GoneTargetUngraft {
43
+ readonly grafts: ComposerGraftPorts | undefined;
44
+ readonly logger: IScopedLogger;
45
+ readonly blockId: string;
46
+ readonly deviceId: number;
47
+ readonly ref: CompositionSourceRef;
48
+ readonly features: readonly CompositionFeaturePlan[];
49
+ }
50
+ /**
51
+ * Rule 5, on either path that finds the target gone (`device.unregistered`, or
52
+ * a later plan when that event was dropped): ungraft every cap the block ADDED
53
+ * on that id, with the one ERROR that says so.
54
+ */
55
+ export declare function ungraftGoneTarget(input: GoneTargetUngraft): void;
56
+ /** ONE `getBindings` read: cap → the addon serving it natively. */
57
+ export type DescribeTarget = (deviceId: number) => Promise<BoundCapsRead>;
58
+ export declare class TargetDescriptions {
59
+ private readonly logger;
60
+ private readonly readBoundCaps;
61
+ private cache;
62
+ constructor(logger: IScopedLogger, readBoundCaps: DescribeTarget);
63
+ /** `device.bindings-changed` for this device: the next read is a real read. */
64
+ invalidate(deviceId: number): void;
65
+ /**
66
+ * The description of `ref`, resolved to `deviceId`. Served from the cache
67
+ * unless `force` (the block's plan changed) or nothing is cached. A read that
68
+ * fails keeps the previous description of the SAME device id, and answers
69
+ * null when there is none: the caller waits, and claims nothing.
70
+ */
71
+ describe(ref: CompositionSourceRef, deviceId: number, type: DeviceType, force: boolean): Promise<CompositionTargetFound | null>;
72
+ }
73
+ /** The PLANNED field set a claim carries for `cap` (S5): an item leaf claims its whole array. */
74
+ export declare function claimFieldsOf(fields: readonly CompositionFieldPlan[], cap: string): readonly string[];
75
+ /** The claim mode of `cap`: `replace` for a cap the device serves natively, else `add`. */
76
+ export declare function claimModeOf(features: readonly CompositionFeaturePlan[], cap: string): FieldClaimMode;
77
+ /** The caps a customization ADDS to its target — the ones it grafts a native for (Task 10). */
78
+ export declare function addedCapsOf(features: readonly CompositionFeaturePlan[]): readonly string[];
79
+ /** How the target's own caps are tracked for a block's self-reads (rule 6). */
80
+ export interface SelfReadCaps {
81
+ /** Added caps read back: the composition's own output, the effective view. */
82
+ readonly effective: readonly string[];
83
+ /** Every other self-read cap: the native view. */
84
+ readonly native: readonly string[];
85
+ }
86
+ export declare function selfReadCaps(fields: readonly CompositionFieldPlan[], target: CompositionSourceRef, features: readonly CompositionFeaturePlan[]): SelfReadCaps;
87
+ /** The `degraded` reason (rule 8): one entry per released field, the source's own reason after the dash. */
88
+ export declare function degradedReason(released: readonly ReleasedField[]): string;
89
+ /** The `starting` reason of a block with HELD fields: each keeps its last value and claim until its source answers (D49). */
90
+ export declare function heldReason(held: readonly ReleasedField[]): string;
91
+ /**
92
+ * Rule 4: a customization whose row exists but whose plan failed keeps its
93
+ * claims, so its owned fields stay at their last value on the hub. The report
94
+ * says so — each field that had a value is `stale`, with the reason.
95
+ */
96
+ export declare function heldFieldsOf(fields: readonly CompositionFieldState[], reason: string): readonly CompositionFieldState[];
@@ -0,0 +1,50 @@
1
+ import { CapabilityLookup, CompositionFieldItem, CompositionFieldPlan, CompositionFieldState, ExpressionValue, FieldOutcome, StatefulExpressionMemory } from '@camstack/types';
2
+ export declare const NO_FIELDS: ReadonlySet<string>;
3
+ export interface FieldRecord {
4
+ readonly plan: CompositionFieldPlan;
5
+ readonly outcome: FieldOutcome | null;
6
+ /** The effective target value observed before this runtime took ownership. Never changes. */
7
+ readonly initial: unknown;
8
+ /** Whether `written` is an accepted runtime answer; kept separate because every falsey value is valid. */
9
+ readonly hasWritten: boolean;
10
+ /** What the target last accepted from this runtime. */
11
+ readonly written: unknown;
12
+ readonly expressionMemory: StatefulExpressionMemory;
13
+ /** Last finite state value already represented by a derived event. */
14
+ readonly eventHighWater: number | null;
15
+ }
16
+ export declare function fieldKey(plan: CompositionFieldPlan): string;
17
+ export declare function createFieldRecord(plan: CompositionFieldPlan, byCap: ReadonlyMap<string, Readonly<Record<string, unknown>> | null> | undefined, lookupCap: CapabilityLookup): FieldRecord;
18
+ export declare function currentOutput(record: FieldRecord): unknown;
19
+ /** The top-level field a record writes: an item leaf writes its whole array. */
20
+ export declare function topLevelField(plan: CompositionFieldPlan): string;
21
+ /** The field as a report or a log line names it: `items[desiccant].status` for an item leaf. */
22
+ export declare function fieldPath(plan: CompositionFieldPlan): string;
23
+ export declare function itemOf(plan: CompositionFieldPlan): Partial<Pick<CompositionFieldState, 'item'>>;
24
+ export declare function sameSet(a: ReadonlySet<string>, b: ReadonlySet<string>): boolean;
25
+ export declare function valueOf(outcome: FieldOutcome | null): ExpressionValue | undefined;
26
+ export declare function reasonOf(outcome: FieldOutcome | null): string | null;
27
+ /** State, value and reason — what a write or a log line is about. NEVER the deadline (R1). */
28
+ export declare function sameOutcome(a: FieldOutcome | null, b: FieldOutcome): boolean;
29
+ /** A source is GONE: the outcome that must never be masked by a non-fatal one. */
30
+ export declare function isFatal(outcome: FieldOutcome | null): boolean;
31
+ /** A `release`-mode field whose source is CONFIRMED unavailable: not the composition's to write (S1b). Stale is a value (D49). */
32
+ export declare function isReleased(record: FieldRecord): boolean;
33
+ /** A `release`-mode field with no value and no ANSWER yet (a failed, first or empty read): held on its last value, never released (D49). */
34
+ export declare function isHeld(record: FieldRecord): boolean;
35
+ export declare function namedField(r: FieldRecord): ReleasedField;
36
+ export declare function writable(record: FieldRecord): ExpressionValue | undefined;
37
+ /** A `release`-mode field handed back to the native provider, and why (for the block report). */
38
+ export interface ReleasedField {
39
+ readonly cap: string;
40
+ readonly path: string;
41
+ readonly item?: CompositionFieldItem;
42
+ readonly reason: string;
43
+ }
44
+ /** Why a cap's last write did not land; the cap is re-written whole on the next pass that evaluates it. */
45
+ export interface PendingWrite {
46
+ readonly kind: 'refused' | 'threw';
47
+ readonly error: string;
48
+ }
49
+ export declare function errorText(error: unknown): string;
50
+ export declare function pendingReason(cap: string, pending: PendingWrite): string;
@@ -0,0 +1,47 @@
1
+ import { CapabilityLookup, DeviceContext, IScopedLogger } from '@camstack/types';
2
+ import { ComposerGraftPorts } from './existing-target.js';
3
+ export interface GraftPorts {
4
+ readonly createContext: (stableId: string, id: number) => DeviceContext;
5
+ readonly logger: IScopedLogger;
6
+ }
7
+ export type GraftOutcome = {
8
+ readonly kind: 'grafted';
9
+ } | {
10
+ readonly kind: 'already';
11
+ } | {
12
+ readonly kind: 'refused';
13
+ readonly reason: string;
14
+ };
15
+ /** The merged (effective) slice of `cap` on `deviceId`, as every consumer reads it; null when there is none. */
16
+ export type ReadMergedSlice = (deviceId: number, cap: string) => Promise<Readonly<Record<string, unknown>> | null>;
17
+ /** One grafted method: the composed answer, or the merged-slice status read. */
18
+ type GraftMethod = (input: unknown) => Promise<unknown>;
19
+ /** The provider a graft registers: method name → its composed implementation. */
20
+ export type GraftProvider = Readonly<Record<string, GraftMethod>>;
21
+ export declare class GraftHost implements ComposerGraftPorts {
22
+ private readonly ports;
23
+ private readonly lookupCap;
24
+ private readonly readMerged;
25
+ /** One context per foreign device: `registerNativeCap` is per context, and so is its unregister. */
26
+ private contexts;
27
+ private caps;
28
+ /** Devices already warned that this kernel cannot unregister a graft. */
29
+ private warnedNoUnregister;
30
+ constructor(ports: GraftPorts, lookupCap: CapabilityLookup, readMerged: ReadMergedSlice);
31
+ graft(deviceId: number, stableId: string, capName: string): GraftOutcome;
32
+ ungraft(deviceId: number, capName: string, reason: string): void;
33
+ /**
34
+ * I-3: a binding a PREVIOUS composer process left on `deviceId` — this one
35
+ * never registered it. The kernel retracts it under the composer's addon id;
36
+ * the hub drops only an entry the composer registered (rule 4). The caller has
37
+ * read that the binding is the composer's.
38
+ */
39
+ retractStale(deviceId: number, capName: string, reason: string): void;
40
+ grafted(deviceId: number): ReadonlySet<string>;
41
+ /** A throw keeps the graft known, so a later plan retries (M-2); an older kernel forgets it (warned once). */
42
+ private unregister;
43
+ private contextFor;
44
+ private providerFor;
45
+ private refuse;
46
+ }
47
+ export {};
@@ -0,0 +1,12 @@
1
+ import { FieldClaim } from '@camstack/types';
2
+ /** What one claim RPC of an owner did, as its target saw it. */
3
+ export type ClaimEvent = 'claiming' | 'held' | 'free' | 'unsettled';
4
+ export declare class HeldClaims {
5
+ private held;
6
+ private claiming;
7
+ /** The `replace` claims the hub holds before this process wrote any (the boot seed). */
8
+ seed(claims: readonly FieldClaim[]): void;
9
+ apply(deviceId: number, cap: string, owner: string, event: ClaimEvent): void;
10
+ /** `device.state-changed` of `cap` is NOT the native for `owner`'s self-reads: its claim is held, or being asked for. */
11
+ shadowsNative(deviceId: number, cap: string, owner: string): boolean;
12
+ }
@@ -0,0 +1,55 @@
1
+ import { ClaimOutcome, deviceStateCapability, FieldClaimMode, IScopedLogger } from '@camstack/types';
2
+ import { z } from 'zod';
3
+ import { ComposedWriteTarget, TargetAsyncFailure, WriteOutcome } from './composition-runtime.js';
4
+ import { ClaimEvent } from './held-claims.js';
5
+ import { SlicePatch } from './slice-assembler.js';
6
+ export type ClaimFieldsInput = z.infer<typeof deviceStateCapability.methods.claimFields.input>;
7
+ export type PatchOwnedFieldsInput = z.infer<typeof deviceStateCapability.methods.patchOwnedFields.input>;
8
+ export type ReleaseClaimInput = z.infer<typeof deviceStateCapability.methods.releaseClaim.input>;
9
+ export interface OwnedFieldsApi {
10
+ claimFields(input: ClaimFieldsInput): Promise<ClaimOutcome>;
11
+ patchOwnedFields(input: PatchOwnedFieldsInput): Promise<ClaimOutcome>;
12
+ releaseClaim(input: ReleaseClaimInput): Promise<ClaimOutcome>;
13
+ }
14
+ export interface OwnedFieldsTargetDeps {
15
+ readonly deviceId: number;
16
+ readonly owner: string;
17
+ readonly modeOf: (cap: string) => FieldClaimMode;
18
+ /** The PLANNED field set per cap, from the plan — sent on every claim, never inferred from values (S5). */
19
+ readonly fieldsOf: (cap: string) => readonly string[];
20
+ readonly api: OwnedFieldsApi;
21
+ readonly logger: IScopedLogger;
22
+ /** A claim RPC of this owner on a `replace` cap went out or was answered (see the header). */
23
+ readonly onClaim?: (cap: string, event: ClaimEvent) => void;
24
+ }
25
+ type AsyncFailureListener = (cap: string, failure: TargetAsyncFailure) => void;
26
+ export declare class OwnedFieldsTarget implements ComposedWriteTarget {
27
+ private readonly deps;
28
+ readonly id: number;
29
+ private landed;
30
+ private withheld;
31
+ private inFlight;
32
+ private pending;
33
+ private listeners;
34
+ /** Per cap, bumped by every `withhold`: a claim that settles under another epoch claimed a stale field set and does not land (I-2). */
35
+ private epoch;
36
+ constructor(deps: OwnedFieldsTargetDeps);
37
+ seed(cap: string, slice: SlicePatch): WriteOutcome;
38
+ patch(cap: string, patch: SlicePatch): WriteOutcome;
39
+ /** `device-status` belongs to the native device; availability lives in the block report only. */
40
+ setAvailable(_available: boolean): void;
41
+ onAsyncFailure(listener: AsyncFailureListener): void;
42
+ withhold(cap: string, fields: readonly string[]): WriteOutcome;
43
+ private claimableFields;
44
+ private enqueue;
45
+ private send;
46
+ private settled;
47
+ private sendWrite;
48
+ private sendRelease;
49
+ private logLanded;
50
+ /** Only a `replace` cap has a native the tracker must tell apart from the merge (N-7). */
51
+ private report;
52
+ /** Escalated to the runtime; a failure nobody listens to is logged — never silent (D391). */
53
+ private fail;
54
+ }
55
+ export {};
@@ -0,0 +1,10 @@
1
+ import { CapabilityLookup, CompositionFieldPlan, ExpressionValue } from '@camstack/types';
2
+ /** One item of an item-array field, as written. */
3
+ export type SliceItem = Readonly<Record<string, unknown>>;
4
+ export type SliceValue = ExpressionValue | readonly SliceItem[];
5
+ export type SlicePatch = Readonly<Record<string, SliceValue>>;
6
+ export interface AssemblerEntry {
7
+ readonly plan: CompositionFieldPlan;
8
+ readonly value: ExpressionValue;
9
+ }
10
+ export declare function assembleSlice(entries: readonly AssemblerEntry[], lookupCap: CapabilityLookup): SlicePatch;