@medicine-wheel/infra 0.5.7 → 0.5.8

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.
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Reconciliation — level-triggered, so `satisfied` cannot age into a lie.
3
+ *
4
+ * The outage this answers (jgwill/gaia#74) was not caused by a missing fact. It
5
+ * was caused by a fact that had been true. Something was recorded as running,
6
+ * the recording was never re-read, and every downstream decision was made
7
+ * against a snapshot.
8
+ *
9
+ * **Level-triggered** is the whole design: `reconcile()` compares the current
10
+ * declared set against the current observed set, every time, and remembers no
11
+ * transitions. There is no "it was converged a minute ago" state to go stale,
12
+ * because there is no state at all — only the two levels and the comparison.
13
+ *
14
+ * Four drift states, and the two asymmetric ones carry the most information:
15
+ *
16
+ * - `converged` — declared and observed agree on every compared field.
17
+ * - `drifted` — both sides exist and disagree. The differences are named.
18
+ * - `unrealized` — declared, not observed. Written down and not running.
19
+ * - `undeclared` — observed, not declared. **Running and nobody wrote it down**
20
+ * — the state that finds the service somebody started by hand
21
+ * at 2am, which is exactly the service holding the port the
22
+ * next tenant is about to be given.
23
+ *
24
+ * A pure function over plain data. No I/O, no clock: `now` is passed in, so two
25
+ * runs over the same input are identical.
26
+ *
27
+ * Spec: /opt/eury/rispecs/foundations/02-specifications.md §S6.
28
+ */
29
+ import type { MetisHold, NodeId, PortConflict, ServiceFacet, TenantFacet } from './types';
30
+ export type DriftState = 'converged' | 'drifted' | 'unrealized' | 'undeclared';
31
+ /** Every drift state, in the order a report should read them: agreement, then disagreement, then the two absences. */
32
+ export declare const DRIFT_STATES: readonly DriftState[];
33
+ /**
34
+ * A reading of a host, taken at a moment, by someone.
35
+ *
36
+ * `observedBy` and `observedAt` are required rather than optional. An
37
+ * observation with no reader and no time is a rumour, and reconciling against a
38
+ * rumour is how the snapshot problem comes back wearing a different hat.
39
+ */
40
+ export interface ObservedState {
41
+ /** → `HostFacet.nodeId` — the host this reading was taken on */
42
+ observedBy: NodeId;
43
+ /** ISO 8601 */
44
+ observedAt: string;
45
+ services: ServiceFacet[];
46
+ tenants?: TenantFacet[];
47
+ }
48
+ export interface FieldDifference {
49
+ field: string;
50
+ declared?: unknown;
51
+ observed?: unknown;
52
+ }
53
+ export interface DriftRow {
54
+ /** → `ServiceFacet.nodeId` */
55
+ nodeId: NodeId;
56
+ /** the unit name from whichever side has it, so a row is readable without a join */
57
+ unit: string;
58
+ state: DriftState;
59
+ /** present only when `state` is `drifted` */
60
+ differences?: FieldDifference[];
61
+ /**
62
+ * Métis from **both** sides, carried through untouched.
63
+ *
64
+ * An array, not a single hold, and not a merge. Two sides holding different
65
+ * tacit notes is not drift — it is two people knowing different things. Keeping
66
+ * only the declared side would delete the observing agent's note; merging the
67
+ * two into one `MetisHold` would concatenate two people into one `heldBy`
68
+ * string and lose attribution. Both are the same erasure in different shapes,
69
+ * and this field exists to refuse it.
70
+ *
71
+ * Ordered declared-first, so a reader sees the recorded knowledge before the
72
+ * observed knowledge.
73
+ */
74
+ metis?: MetisHold[];
75
+ }
76
+ export interface ReconcileResult {
77
+ observedBy: NodeId;
78
+ observedAt: string;
79
+ /**
80
+ * Milliseconds between `observedAt` and `now`, when `now` was given.
81
+ *
82
+ * Exposed so a caller can refuse to act on an old reading. Negative when the
83
+ * observation is stamped in the future — surfaced rather than clamped, because
84
+ * a future timestamp means clock skew and that is worth seeing.
85
+ */
86
+ observationAgeMs?: number;
87
+ rows: DriftRow[];
88
+ /** counts per drift state; every state present, including the zeroes */
89
+ summary: Record<DriftState, number>;
90
+ /**
91
+ * Port conflicts over declared ∪ observed — the §S3 traversal folded in here
92
+ * because this is the one place both sides are in hand at once.
93
+ */
94
+ conflicts: PortConflict[];
95
+ /** true when nothing drifted, nothing is unrealized, nothing is undeclared, and no port collides */
96
+ converged: boolean;
97
+ }
98
+ /**
99
+ * Compare what was declared against what was observed, right now.
100
+ *
101
+ * Rows are keyed by `nodeId` and sorted by it, so two runs over the same input
102
+ * are byte-identical and a caller can diff them.
103
+ *
104
+ * @param declared desired state, authored by eury
105
+ * @param observed a reading taken on a host by gaia, with its provenance
106
+ * @param opts.now ISO 8601 — supplied by the caller so this stays clock-free
107
+ */
108
+ export declare function reconcile(declared: ServiceFacet[], observed: ObservedState, opts?: {
109
+ now?: string;
110
+ }): ReconcileResult;
111
+ //# sourceMappingURL=reconcile.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reconcile.d.ts","sourceRoot":"","sources":["../src/reconcile.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAGH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,EAAe,YAAY,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAEvG,MAAM,MAAM,UAAU,GAAG,WAAW,GAAG,SAAS,GAAG,YAAY,GAAG,YAAY,CAAC;AAE/E,sHAAsH;AACtH,eAAO,MAAM,YAAY,EAAE,SAAS,UAAU,EAAyD,CAAC;AAExG;;;;;;GAMG;AACH,MAAM,WAAW,aAAa;IAC5B,gEAAgE;IAChE,UAAU,EAAE,MAAM,CAAC;IACnB,eAAe;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,YAAY,EAAE,CAAC;IACzB,OAAO,CAAC,EAAE,WAAW,EAAE,CAAC;CACzB;AAED,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,QAAQ;IACvB,8BAA8B;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,oFAAoF;IACpF,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,UAAU,CAAC;IAClB,6CAA6C;IAC7C,WAAW,CAAC,EAAE,eAAe,EAAE,CAAC;IAChC;;;;;;;;;;;;OAYG;IACH,KAAK,CAAC,EAAE,SAAS,EAAE,CAAC;CACrB;AAED,MAAM,WAAW,eAAe;IAC9B,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB;;;;;;OAMG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,IAAI,EAAE,QAAQ,EAAE,CAAC;IACjB,wEAAwE;IACxE,OAAO,EAAE,MAAM,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC;IACpC;;;OAGG;IACH,SAAS,EAAE,YAAY,EAAE,CAAC;IAC1B,oGAAoG;IACpG,SAAS,EAAE,OAAO,CAAC;CACpB;AA2CD;;;;;;;;;GASG;AACH,wBAAgB,SAAS,CACvB,QAAQ,EAAE,YAAY,EAAE,EACxB,QAAQ,EAAE,aAAa,EACvB,IAAI,GAAE;IAAE,GAAG,CAAC,EAAE,MAAM,CAAA;CAAO,GAC1B,eAAe,CA+DjB"}
@@ -0,0 +1,130 @@
1
+ "use strict";
2
+ /**
3
+ * Reconciliation — level-triggered, so `satisfied` cannot age into a lie.
4
+ *
5
+ * The outage this answers (jgwill/gaia#74) was not caused by a missing fact. It
6
+ * was caused by a fact that had been true. Something was recorded as running,
7
+ * the recording was never re-read, and every downstream decision was made
8
+ * against a snapshot.
9
+ *
10
+ * **Level-triggered** is the whole design: `reconcile()` compares the current
11
+ * declared set against the current observed set, every time, and remembers no
12
+ * transitions. There is no "it was converged a minute ago" state to go stale,
13
+ * because there is no state at all — only the two levels and the comparison.
14
+ *
15
+ * Four drift states, and the two asymmetric ones carry the most information:
16
+ *
17
+ * - `converged` — declared and observed agree on every compared field.
18
+ * - `drifted` — both sides exist and disagree. The differences are named.
19
+ * - `unrealized` — declared, not observed. Written down and not running.
20
+ * - `undeclared` — observed, not declared. **Running and nobody wrote it down**
21
+ * — the state that finds the service somebody started by hand
22
+ * at 2am, which is exactly the service holding the port the
23
+ * next tenant is about to be given.
24
+ *
25
+ * A pure function over plain data. No I/O, no clock: `now` is passed in, so two
26
+ * runs over the same input are identical.
27
+ *
28
+ * Spec: /opt/eury/rispecs/foundations/02-specifications.md §S6.
29
+ */
30
+ Object.defineProperty(exports, "__esModule", { value: true });
31
+ exports.DRIFT_STATES = void 0;
32
+ exports.reconcile = reconcile;
33
+ const ports_1 = require("./ports");
34
+ /** Every drift state, in the order a report should read them: agreement, then disagreement, then the two absences. */
35
+ exports.DRIFT_STATES = ['converged', 'drifted', 'unrealized', 'undeclared'];
36
+ /** Fields compared between a declared and an observed service. `metis` is deliberately absent. */
37
+ const COMPARED_FIELDS = [
38
+ 'unit',
39
+ 'scope',
40
+ 'ownedBy',
41
+ 'workingDirectory',
42
+ 'execStop',
43
+ ];
44
+ /** `host|proto|port` — the identity of a claim, matching the slot key `detectPortConflicts` uses. */
45
+ function portKey(binding) {
46
+ return `${binding.host} ${binding.proto ?? 'tcp'} ${binding.port}`;
47
+ }
48
+ /** Ports compared as sets of claims, so ordering differences between systemd and a manifest are not drift. */
49
+ function portsDiffer(declared, observed) {
50
+ const a = [...new Set(declared.map(portKey))].sort();
51
+ const b = [...new Set(observed.map(portKey))].sort();
52
+ return a.length !== b.length || a.some((k, i) => k !== b[i]);
53
+ }
54
+ function diffService(declared, observed) {
55
+ const differences = [];
56
+ for (const field of COMPARED_FIELDS) {
57
+ if (declared[field] !== observed[field]) {
58
+ differences.push({ field, declared: declared[field], observed: observed[field] });
59
+ }
60
+ }
61
+ if (portsDiffer(declared.ports, observed.ports)) {
62
+ differences.push({
63
+ field: 'ports',
64
+ declared: [...declared.ports].map(portKey).sort(),
65
+ observed: [...observed.ports].map(portKey).sort(),
66
+ });
67
+ }
68
+ return differences;
69
+ }
70
+ /**
71
+ * Compare what was declared against what was observed, right now.
72
+ *
73
+ * Rows are keyed by `nodeId` and sorted by it, so two runs over the same input
74
+ * are byte-identical and a caller can diff them.
75
+ *
76
+ * @param declared desired state, authored by eury
77
+ * @param observed a reading taken on a host by gaia, with its provenance
78
+ * @param opts.now ISO 8601 — supplied by the caller so this stays clock-free
79
+ */
80
+ function reconcile(declared, observed, opts = {}) {
81
+ const declaredById = new Map(declared.map((s) => [s.nodeId, s]));
82
+ const observedById = new Map(observed.services.map((s) => [s.nodeId, s]));
83
+ const rows = [];
84
+ /** Both sides' holds, declared first, with the empty case left undefined. */
85
+ const heldBy = (...sides) => {
86
+ const held = sides.filter((m) => m !== undefined);
87
+ return held.length > 0 ? held : undefined;
88
+ };
89
+ for (const [nodeId, d] of declaredById) {
90
+ const o = observedById.get(nodeId);
91
+ if (!o) {
92
+ rows.push({ nodeId, unit: d.unit, state: 'unrealized', metis: heldBy(d.metis) });
93
+ continue;
94
+ }
95
+ const differences = diffService(d, o);
96
+ const metis = heldBy(d.metis, o.metis);
97
+ rows.push(differences.length === 0
98
+ ? { nodeId, unit: d.unit, state: 'converged', metis }
99
+ : { nodeId, unit: d.unit, state: 'drifted', differences, metis });
100
+ }
101
+ for (const [nodeId, o] of observedById) {
102
+ if (declaredById.has(nodeId))
103
+ continue;
104
+ rows.push({ nodeId, unit: o.unit, state: 'undeclared', metis: heldBy(o.metis) });
105
+ }
106
+ rows.sort((a, b) => a.nodeId.localeCompare(b.nodeId));
107
+ const summary = exports.DRIFT_STATES.reduce((acc, state) => ({ ...acc, [state]: rows.filter((r) => r.state === state).length }), {});
108
+ const conflicts = (0, ports_1.detectPortConflicts)([
109
+ ...declared.flatMap((s) => s.ports),
110
+ ...observed.services.flatMap((s) => s.ports),
111
+ ]);
112
+ const result = {
113
+ observedBy: observed.observedBy,
114
+ observedAt: observed.observedAt,
115
+ rows,
116
+ summary,
117
+ conflicts,
118
+ converged: summary.drifted === 0 &&
119
+ summary.unrealized === 0 &&
120
+ summary.undeclared === 0 &&
121
+ conflicts.length === 0,
122
+ };
123
+ if (opts.now) {
124
+ const age = Date.parse(opts.now) - Date.parse(observed.observedAt);
125
+ if (!Number.isNaN(age))
126
+ result.observationAgeMs = age;
127
+ }
128
+ return result;
129
+ }
130
+ //# sourceMappingURL=reconcile.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reconcile.js","sourceRoot":"","sources":["../src/reconcile.ts"],"names":[],"mappings":"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"}