@medicine-wheel/infra 0.5.7 → 0.5.9
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/README.md +92 -15
- package/dist/index.d.ts +14 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +29 -5
- package/dist/index.js.map +1 -1
- package/dist/preconditions.d.ts +198 -0
- package/dist/preconditions.d.ts.map +1 -0
- package/dist/preconditions.js +181 -0
- package/dist/preconditions.js.map +1 -0
- package/dist/reconcile.d.ts +111 -0
- package/dist/reconcile.d.ts.map +1 -0
- package/dist/reconcile.js +130 -0
- package/dist/reconcile.js.map +1 -0
- package/dist/schemas.d.ts +406 -0
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +71 -2
- package/dist/schemas.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -14,7 +14,7 @@ does not have to change to make that true.
|
|
|
14
14
|
## Install
|
|
15
15
|
|
|
16
16
|
```bash
|
|
17
|
-
npm install @medicine-wheel/infra
|
|
17
|
+
npm install @medicine-wheel/infra@^0.5.8
|
|
18
18
|
```
|
|
19
19
|
|
|
20
20
|
Depends only on `@medicine-wheel/ontology-core` and `zod`.
|
|
@@ -110,26 +110,103 @@ if (!parsed.success) { /* the emitter misread systemd — do not reconcile on th
|
|
|
110
110
|
Port numbers are integers in 1–65535. A misparsed `0` is rejected here rather
|
|
111
111
|
than silently never colliding with anything downstream.
|
|
112
112
|
|
|
113
|
-
##
|
|
113
|
+
## `Precondition` — where the machine fact and the human do not merge
|
|
114
114
|
|
|
115
|
-
|
|
116
|
-
|
|
115
|
+
```ts
|
|
116
|
+
import { preconditionGuard, readyService, lingerFact } from '@medicine-wheel/infra';
|
|
117
|
+
|
|
118
|
+
const p = {
|
|
119
|
+
id: 'pre:gmusic-linger',
|
|
120
|
+
gates: 'node:knowledge:assembly-mux',
|
|
121
|
+
fact: lingerFact('node:human:gmusic', 'enabled', '2026-08-05T12:00:00Z'),
|
|
122
|
+
consent: { consentId: 'consent:root-step:gmusic', state: 'withdrawn', readAt: '2026-08-05T12:00:00Z' },
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
preconditionGuard(p).verdict; // 'unauthorized' — NOT 'satisfied', NOT 'unsatisfied'
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
`linger` is a machine fact: a checkbox with no authority and no ability to
|
|
129
|
+
withdraw. The human authorization to run the root step that set it is a
|
|
130
|
+
`ConsentRecord` in `@medicine-wheel/consent-lifecycle`. A `Precondition`
|
|
131
|
+
references both **by id**, and `infra` never imports that package — a package
|
|
132
|
+
that can construct a consent record can fabricate one.
|
|
133
|
+
|
|
134
|
+
Four verdicts, and the distinctions are the whole point:
|
|
117
135
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
136
|
+
| verdict | means |
|
|
137
|
+
|---|---|
|
|
138
|
+
| `satisfied` | every declared half was read and holds |
|
|
139
|
+
| `unsatisfied` | a machine fact was read and does not match |
|
|
140
|
+
| `unauthorized` | the machine half holds and consent does not authorize |
|
|
141
|
+
| `unknown` | a declared half has **not been read**, which is not the same as false |
|
|
142
|
+
|
|
143
|
+
`unauthorized` is separate from `unsatisfied` on purpose. Collapse them and a
|
|
144
|
+
withdrawn consent looks like a technical failure an operator can fix by
|
|
145
|
+
restarting something. `unknown` is separate from both because a guard that
|
|
146
|
+
reports "blocked" when nobody has looked yet is a guard operators learn to
|
|
147
|
+
ignore.
|
|
148
|
+
|
|
149
|
+
`readyService` reports `declared: 0` out loud rather than letting an
|
|
150
|
+
unconfigured service look checked, and carries every `MetisHold` from the
|
|
151
|
+
blocking preconditions forward — the reason a gate is stuck is exactly where
|
|
152
|
+
"you have to restart it twice" lives.
|
|
153
|
+
|
|
154
|
+
## `reconcile` — level-triggered, so `satisfied` cannot age
|
|
155
|
+
|
|
156
|
+
```ts
|
|
157
|
+
const result = reconcile(declaredServices, {
|
|
158
|
+
observedBy: 'node:land:gaia',
|
|
159
|
+
observedAt: '2026-08-05T12:00:00Z',
|
|
160
|
+
services: readLiveFromSystemd(),
|
|
161
|
+
}, { now: '2026-08-05T12:05:00Z' });
|
|
162
|
+
|
|
163
|
+
result.summary; // { converged: 4, drifted: 1, unrealized: 0, undeclared: 2 }
|
|
164
|
+
result.observationAgeMs; // 300000 — refuse to act on a reading that is too old
|
|
165
|
+
```
|
|
127
166
|
|
|
128
|
-
|
|
167
|
+
The outage this answers (jgwill/gaia#74) was not caused by a missing fact. It was
|
|
168
|
+
caused by a fact that *had been* true. So `reconcile` remembers no transitions:
|
|
169
|
+
it compares the current declared level against the current observed level, every
|
|
170
|
+
time. There is no "converged a minute ago" to go stale.
|
|
171
|
+
|
|
172
|
+
| drift state | means |
|
|
173
|
+
|---|---|
|
|
174
|
+
| `converged` | both sides agree on every compared field |
|
|
175
|
+
| `drifted` | both exist, they disagree — the differences are named |
|
|
176
|
+
| `unrealized` | declared, not observed — written down, not running |
|
|
177
|
+
| `undeclared` | **observed, not declared** — running and nobody wrote it down |
|
|
178
|
+
|
|
179
|
+
`undeclared` carries the most information: it finds the service somebody started
|
|
180
|
+
by hand at 2am, which is the service holding the port the next tenant is about to
|
|
181
|
+
be given. The §S3 port traversal is folded in here, because this is the one place
|
|
182
|
+
declared and observed are both in hand.
|
|
183
|
+
|
|
184
|
+
**Métis is never a difference.** Two sides holding different tacit notes is not
|
|
185
|
+
drift — it is two people knowing different things. A reconciler that reported it
|
|
186
|
+
would be asking an operator to delete what they know to make a table go green.
|
|
187
|
+
|
|
188
|
+
## Scope
|
|
189
|
+
|
|
190
|
+
Every specification ships: S1 facets, S3 `detectPortConflicts`, S4
|
|
191
|
+
`Precondition`, S5 `MetisHold`, S6 `reconcile`. S2 — the governed `part-of`,
|
|
192
|
+
`ordered-after` and `binds-port` edges — landed in
|
|
193
|
+
`ontology-core`'s `KINSHIP_EDGE_TYPES`, which is where edge vocabulary belongs.
|
|
194
|
+
`infra` never forked it.
|
|
195
|
+
|
|
196
|
+
S4 and S6 first ship in **0.5.8**. `0.5.7` and earlier contain S1, S3 and S5
|
|
197
|
+
only — a package that installs `^0.5.7` may resolve a dist with no
|
|
198
|
+
`preconditionGuard` and no `reconcile`.
|
|
199
|
+
|
|
200
|
+
The package stays types plus pure functions: zero I/O, zero persistence, zero
|
|
201
|
+
clock. `reconcile` takes `now` as an argument for exactly that reason.
|
|
202
|
+
Persistence belongs to `@medicine-wheel/mcp`, which is where these are reachable
|
|
203
|
+
from — `register_host`, `register_tenant`, `register_service`,
|
|
204
|
+
`detect_port_conflicts`, `hold_metis`, `list_infra_topology`,
|
|
205
|
+
`reconcile_infra_state`, and `mw_enforce_gate` for the precondition gate.
|
|
129
206
|
|
|
130
207
|
## Grounding
|
|
131
208
|
|
|
132
|
-
- Specification: `/opt/eury/rispecs/foundations/02-specifications.md` §S1
|
|
209
|
+
- Specification: `/opt/eury/rispecs/foundations/02-specifications.md` §S1–§S6
|
|
133
210
|
- Argument: `/opt/eury/foundations/relational-infrastructure/synthesis.md`
|
|
134
211
|
- The collision this exists to catch: `jgwill/gaia#75`
|
|
135
212
|
- The outage that motivated observed state: `jgwill/gaia#74`
|
package/dist/index.d.ts
CHANGED
|
@@ -10,15 +10,25 @@
|
|
|
10
10
|
* package: the closed `NodeType` union is reused, never widened, and facets are
|
|
11
11
|
* keyed by node id rather than re-declaring nodes.
|
|
12
12
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* (
|
|
13
|
+
* Every specification is now shipped: S1 (facets), S3 (`detectPortConflicts`),
|
|
14
|
+
* S4 (`Precondition` / `preconditionGuard` / `readyService`), S5 (`MetisHold`),
|
|
15
|
+
* S6 (`ObservedState` / `reconcile`). S2 — the governed `part-of`,
|
|
16
|
+
* `ordered-after` and `binds-port` edges — landed in `ontology-core`, which is
|
|
17
|
+
* where edge vocabulary belongs; `infra` never forked it.
|
|
18
|
+
*
|
|
19
|
+
* The package remains types plus pure functions. Zero I/O, zero persistence,
|
|
20
|
+
* zero clock: `reconcile` takes `now` as an argument for exactly that reason.
|
|
16
21
|
*
|
|
17
22
|
* @packageDocumentation
|
|
18
23
|
*/
|
|
19
24
|
export type { NodeId, MetisHold, LingerState, Reachability, PortProto, ServiceScope, HostFacet, TenantFacet, ServiceFacet, PortBinding, PortConflict, FacetKind, } from './types';
|
|
20
25
|
export { DEFAULT_PORT_PROTO, FACET_NODE_TYPES } from './types';
|
|
21
26
|
export { MetisHoldSchema, LingerStateSchema, ReachabilitySchema, PortProtoSchema, ServiceScopeSchema, PortNumberSchema, PortBindingSchema, PortConflictSchema, HostFacetSchema, TenantFacetSchema, ServiceFacetSchema, } from './schemas';
|
|
22
|
-
export
|
|
27
|
+
export { PreconditionKindSchema, MachineFactSchema, ConsentReferenceSchema, PreconditionSchema, PreconditionVerdictSchema, DriftStateSchema, ObservedStateSchema, } from './schemas';
|
|
28
|
+
export type { ValidatedMetisHold, ValidatedPortBinding, ValidatedPortConflict, ValidatedHostFacet, ValidatedTenantFacet, ValidatedServiceFacet, ValidatedMachineFact, ValidatedConsentReference, ValidatedPrecondition, ValidatedObservedState, } from './schemas';
|
|
23
29
|
export { detectPortConflicts } from './ports';
|
|
30
|
+
export { preconditionGuard, readyService, lingerFact, AUTHORIZING_CONSENT_STATES, CONSENT_REQUIRING_KINDS, } from './preconditions';
|
|
31
|
+
export type { PreconditionKind, MachineFact, ConsentReference, Precondition, PreconditionVerdict, PreconditionResult, ServiceReadiness, } from './preconditions';
|
|
32
|
+
export { reconcile, DRIFT_STATES } from './reconcile';
|
|
33
|
+
export type { DriftState, ObservedState, FieldDifference, DriftRow, ReconcileResult, } from './reconcile';
|
|
24
34
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAGH,YAAY,EACV,MAAM,EACN,SAAS,EACT,WAAW,EACX,YAAY,EACZ,SAAS,EACT,YAAY,EACZ,SAAS,EACT,WAAW,EACX,YAAY,EACZ,WAAW,EACX,YAAY,EACZ,SAAS,GACV,MAAM,SAAS,CAAC;AAEjB,OAAO,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAG/D,OAAO,EACL,eAAe,EACf,iBAAiB,EACjB,kBAAkB,EAClB,eAAe,EACf,kBAAkB,EAClB,gBAAgB,EAChB,iBAAiB,EACjB,kBAAkB,EAClB,eAAe,EACf,iBAAiB,EACjB,kBAAkB,GACnB,MAAM,WAAW,CAAC;AAEnB,OAAO,EACL,sBAAsB,EACtB,iBAAiB,EACjB,sBAAsB,EACtB,kBAAkB,EAClB,yBAAyB,EACzB,gBAAgB,EAChB,mBAAmB,GACpB,MAAM,WAAW,CAAC;AAEnB,YAAY,EACV,kBAAkB,EAClB,oBAAoB,EACpB,qBAAqB,EACrB,kBAAkB,EAClB,oBAAoB,EACpB,qBAAqB,EACrB,oBAAoB,EACpB,yBAAyB,EACzB,qBAAqB,EACrB,sBAAsB,GACvB,MAAM,WAAW,CAAC;AAGnB,OAAO,EAAE,mBAAmB,EAAE,MAAM,SAAS,CAAC;AAG9C,OAAO,EACL,iBAAiB,EACjB,YAAY,EACZ,UAAU,EACV,0BAA0B,EAC1B,uBAAuB,GACxB,MAAM,iBAAiB,CAAC;AAEzB,YAAY,EACV,gBAAgB,EAChB,WAAW,EACX,gBAAgB,EAChB,YAAY,EACZ,mBAAmB,EACnB,kBAAkB,EAClB,gBAAgB,GACjB,MAAM,iBAAiB,CAAC;AAGzB,OAAO,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAEtD,YAAY,EACV,UAAU,EACV,aAAa,EACb,eAAe,EACf,QAAQ,EACR,eAAe,GAChB,MAAM,aAAa,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -11,14 +11,19 @@
|
|
|
11
11
|
* package: the closed `NodeType` union is reused, never widened, and facets are
|
|
12
12
|
* keyed by node id rather than re-declaring nodes.
|
|
13
13
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* (
|
|
14
|
+
* Every specification is now shipped: S1 (facets), S3 (`detectPortConflicts`),
|
|
15
|
+
* S4 (`Precondition` / `preconditionGuard` / `readyService`), S5 (`MetisHold`),
|
|
16
|
+
* S6 (`ObservedState` / `reconcile`). S2 — the governed `part-of`,
|
|
17
|
+
* `ordered-after` and `binds-port` edges — landed in `ontology-core`, which is
|
|
18
|
+
* where edge vocabulary belongs; `infra` never forked it.
|
|
19
|
+
*
|
|
20
|
+
* The package remains types plus pure functions. Zero I/O, zero persistence,
|
|
21
|
+
* zero clock: `reconcile` takes `now` as an argument for exactly that reason.
|
|
17
22
|
*
|
|
18
23
|
* @packageDocumentation
|
|
19
24
|
*/
|
|
20
25
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
21
|
-
exports.detectPortConflicts = exports.ServiceFacetSchema = exports.TenantFacetSchema = exports.HostFacetSchema = exports.PortConflictSchema = exports.PortBindingSchema = exports.PortNumberSchema = exports.ServiceScopeSchema = exports.PortProtoSchema = exports.ReachabilitySchema = exports.LingerStateSchema = exports.MetisHoldSchema = exports.FACET_NODE_TYPES = exports.DEFAULT_PORT_PROTO = void 0;
|
|
26
|
+
exports.DRIFT_STATES = exports.reconcile = exports.CONSENT_REQUIRING_KINDS = exports.AUTHORIZING_CONSENT_STATES = exports.lingerFact = exports.readyService = exports.preconditionGuard = exports.detectPortConflicts = exports.ObservedStateSchema = exports.DriftStateSchema = exports.PreconditionVerdictSchema = exports.PreconditionSchema = exports.ConsentReferenceSchema = exports.MachineFactSchema = exports.PreconditionKindSchema = exports.ServiceFacetSchema = exports.TenantFacetSchema = exports.HostFacetSchema = exports.PortConflictSchema = exports.PortBindingSchema = exports.PortNumberSchema = exports.ServiceScopeSchema = exports.PortProtoSchema = exports.ReachabilitySchema = exports.LingerStateSchema = exports.MetisHoldSchema = exports.FACET_NODE_TYPES = exports.DEFAULT_PORT_PROTO = void 0;
|
|
22
27
|
var types_1 = require("./types");
|
|
23
28
|
Object.defineProperty(exports, "DEFAULT_PORT_PROTO", { enumerable: true, get: function () { return types_1.DEFAULT_PORT_PROTO; } });
|
|
24
29
|
Object.defineProperty(exports, "FACET_NODE_TYPES", { enumerable: true, get: function () { return types_1.FACET_NODE_TYPES; } });
|
|
@@ -35,7 +40,26 @@ Object.defineProperty(exports, "PortConflictSchema", { enumerable: true, get: fu
|
|
|
35
40
|
Object.defineProperty(exports, "HostFacetSchema", { enumerable: true, get: function () { return schemas_1.HostFacetSchema; } });
|
|
36
41
|
Object.defineProperty(exports, "TenantFacetSchema", { enumerable: true, get: function () { return schemas_1.TenantFacetSchema; } });
|
|
37
42
|
Object.defineProperty(exports, "ServiceFacetSchema", { enumerable: true, get: function () { return schemas_1.ServiceFacetSchema; } });
|
|
38
|
-
|
|
43
|
+
var schemas_2 = require("./schemas");
|
|
44
|
+
Object.defineProperty(exports, "PreconditionKindSchema", { enumerable: true, get: function () { return schemas_2.PreconditionKindSchema; } });
|
|
45
|
+
Object.defineProperty(exports, "MachineFactSchema", { enumerable: true, get: function () { return schemas_2.MachineFactSchema; } });
|
|
46
|
+
Object.defineProperty(exports, "ConsentReferenceSchema", { enumerable: true, get: function () { return schemas_2.ConsentReferenceSchema; } });
|
|
47
|
+
Object.defineProperty(exports, "PreconditionSchema", { enumerable: true, get: function () { return schemas_2.PreconditionSchema; } });
|
|
48
|
+
Object.defineProperty(exports, "PreconditionVerdictSchema", { enumerable: true, get: function () { return schemas_2.PreconditionVerdictSchema; } });
|
|
49
|
+
Object.defineProperty(exports, "DriftStateSchema", { enumerable: true, get: function () { return schemas_2.DriftStateSchema; } });
|
|
50
|
+
Object.defineProperty(exports, "ObservedStateSchema", { enumerable: true, get: function () { return schemas_2.ObservedStateSchema; } });
|
|
51
|
+
// ── Port-conflict detection (S3) ────────────────────────────────────────────
|
|
39
52
|
var ports_1 = require("./ports");
|
|
40
53
|
Object.defineProperty(exports, "detectPortConflicts", { enumerable: true, get: function () { return ports_1.detectPortConflicts; } });
|
|
54
|
+
// ── Preconditions (S4) ──────────────────────────────────────────────────────
|
|
55
|
+
var preconditions_1 = require("./preconditions");
|
|
56
|
+
Object.defineProperty(exports, "preconditionGuard", { enumerable: true, get: function () { return preconditions_1.preconditionGuard; } });
|
|
57
|
+
Object.defineProperty(exports, "readyService", { enumerable: true, get: function () { return preconditions_1.readyService; } });
|
|
58
|
+
Object.defineProperty(exports, "lingerFact", { enumerable: true, get: function () { return preconditions_1.lingerFact; } });
|
|
59
|
+
Object.defineProperty(exports, "AUTHORIZING_CONSENT_STATES", { enumerable: true, get: function () { return preconditions_1.AUTHORIZING_CONSENT_STATES; } });
|
|
60
|
+
Object.defineProperty(exports, "CONSENT_REQUIRING_KINDS", { enumerable: true, get: function () { return preconditions_1.CONSENT_REQUIRING_KINDS; } });
|
|
61
|
+
// ── Reconciliation (S6) ─────────────────────────────────────────────────────
|
|
62
|
+
var reconcile_1 = require("./reconcile");
|
|
63
|
+
Object.defineProperty(exports, "reconcile", { enumerable: true, get: function () { return reconcile_1.reconcile; } });
|
|
64
|
+
Object.defineProperty(exports, "DRIFT_STATES", { enumerable: true, get: function () { return reconcile_1.DRIFT_STATES; } });
|
|
41
65
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAAA
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;;;AAkBH,iCAA+D;AAAtD,2GAAA,kBAAkB,OAAA;AAAE,yGAAA,gBAAgB,OAAA;AAE7C,+EAA+E;AAC/E,qCAYmB;AAXjB,0GAAA,eAAe,OAAA;AACf,4GAAA,iBAAiB,OAAA;AACjB,6GAAA,kBAAkB,OAAA;AAClB,0GAAA,eAAe,OAAA;AACf,6GAAA,kBAAkB,OAAA;AAClB,2GAAA,gBAAgB,OAAA;AAChB,4GAAA,iBAAiB,OAAA;AACjB,6GAAA,kBAAkB,OAAA;AAClB,0GAAA,eAAe,OAAA;AACf,4GAAA,iBAAiB,OAAA;AACjB,6GAAA,kBAAkB,OAAA;AAGpB,qCAQmB;AAPjB,iHAAA,sBAAsB,OAAA;AACtB,4GAAA,iBAAiB,OAAA;AACjB,iHAAA,sBAAsB,OAAA;AACtB,6GAAA,kBAAkB,OAAA;AAClB,oHAAA,yBAAyB,OAAA;AACzB,2GAAA,gBAAgB,OAAA;AAChB,8GAAA,mBAAmB,OAAA;AAgBrB,+EAA+E;AAC/E,iCAA8C;AAArC,4GAAA,mBAAmB,OAAA;AAE5B,+EAA+E;AAC/E,iDAMyB;AALvB,kHAAA,iBAAiB,OAAA;AACjB,6GAAA,YAAY,OAAA;AACZ,2GAAA,UAAU,OAAA;AACV,2HAAA,0BAA0B,OAAA;AAC1B,wHAAA,uBAAuB,OAAA;AAazB,+EAA+E;AAC/E,yCAAsD;AAA7C,sGAAA,SAAS,OAAA;AAAE,yGAAA,YAAY,OAAA"}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Preconditions — what must hold before West runs.
|
|
3
|
+
*
|
|
4
|
+
* A service is West: the thing that executes. A port claim, a linger flag, a
|
|
5
|
+
* unit file on disk are South: claims that must hold *first*. This module makes
|
|
6
|
+
* that ordering checkable without letting either half stand in for the other.
|
|
7
|
+
*
|
|
8
|
+
* The load-bearing separation, and the reason this file exists at all:
|
|
9
|
+
*
|
|
10
|
+
* > `linger` is a machine fact — a checkbox with no authority and no ability to
|
|
11
|
+
* > withdraw. The human authorization to run the root step that sets it is a
|
|
12
|
+
* > `ConsentRecord` in `@medicine-wheel/consent-lifecycle`. A `Precondition`
|
|
13
|
+
* > references both **by id**. Neither collapses into the other.
|
|
14
|
+
*
|
|
15
|
+
* Collapsing them is the failure this guards against: once a granted consent is
|
|
16
|
+
* inferred from a machine fact, withdrawing consent stops meaning anything,
|
|
17
|
+
* because the flag is still set. So `unauthorized` is a verdict of its own — the
|
|
18
|
+
* machine is ready and the human has not said yes — and it never reads as
|
|
19
|
+
* `unsatisfied`.
|
|
20
|
+
*
|
|
21
|
+
* A pure function over plain data. No I/O, no ontology traversal, no clock.
|
|
22
|
+
*
|
|
23
|
+
* Spec: /opt/eury/rispecs/foundations/02-specifications.md §S4.
|
|
24
|
+
*/
|
|
25
|
+
import type { LingerState, MetisHold, NodeId, ServiceFacet } from './types';
|
|
26
|
+
/**
|
|
27
|
+
* The kinds of machine fact a precondition can read.
|
|
28
|
+
*
|
|
29
|
+
* Every one of these is something a host can be asked and will answer without a
|
|
30
|
+
* person present. That is precisely what disqualifies them from carrying
|
|
31
|
+
* consent.
|
|
32
|
+
*/
|
|
33
|
+
export type PreconditionKind =
|
|
34
|
+
/** the tenant's user units survive logout — `loginctl show-user <account>` */
|
|
35
|
+
'linger'
|
|
36
|
+
/** the slot this service claims is not held by another service */
|
|
37
|
+
| 'port-free'
|
|
38
|
+
/** the unit file exists in the scope the facet declares */
|
|
39
|
+
| 'unit-present'
|
|
40
|
+
/** the directory the unit reads its environment from exists (jgwill/gaia#74) */
|
|
41
|
+
| 'working-directory';
|
|
42
|
+
/**
|
|
43
|
+
* A fact read off a machine, with its provenance attached.
|
|
44
|
+
*
|
|
45
|
+
* `observed` being absent means **never read**, which is not the same as read
|
|
46
|
+
* and false. A guard that treats the two alike will report a service blocked
|
|
47
|
+
* when nobody has looked yet, and the operator learns to ignore it.
|
|
48
|
+
*/
|
|
49
|
+
export interface MachineFact {
|
|
50
|
+
kind: PreconditionKind;
|
|
51
|
+
/** the facet whose field carries this fact — host, tenant or service */
|
|
52
|
+
facetNodeId: NodeId;
|
|
53
|
+
/** what the fact must read for the precondition to hold */
|
|
54
|
+
expected: string;
|
|
55
|
+
/** what it actually read. Absent means unread. */
|
|
56
|
+
observed?: string;
|
|
57
|
+
/** ISO 8601 — when `observed` was read. Staleness is a fact, not an inference. */
|
|
58
|
+
observedAt?: string;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* The consent states that authorize an action.
|
|
62
|
+
*
|
|
63
|
+
* Mirrors `ConsentState` in `@medicine-wheel/consent-lifecycle` and is exported
|
|
64
|
+
* as data so a caller can pass its own set rather than fork this file. `infra`
|
|
65
|
+
* deliberately does **not** import that package: a package that can construct a
|
|
66
|
+
* consent record can fabricate one, and this one must not be able to.
|
|
67
|
+
*/
|
|
68
|
+
export declare const AUTHORIZING_CONSENT_STATES: readonly string[];
|
|
69
|
+
/**
|
|
70
|
+
* Fact kinds that cannot stand alone.
|
|
71
|
+
*
|
|
72
|
+
* `linger` is here because enabling it required running a root step, and that
|
|
73
|
+
* root step required a human to say yes. A precondition that reads the flag and
|
|
74
|
+
* references no `ConsentRecord` therefore opens the gate on the machine fact
|
|
75
|
+
* alone — which is the exact collapse this module's docstring forbids, arriving
|
|
76
|
+
* not through a wrong verdict but through an incomplete declaration.
|
|
77
|
+
*
|
|
78
|
+
* Such a precondition returns `unknown`, never `satisfied`: nothing authorized
|
|
79
|
+
* it, and nothing was checked. `unauthorized` would be wrong too — that verdict
|
|
80
|
+
* means consent was read and did not authorize, and here it was never named.
|
|
81
|
+
*/
|
|
82
|
+
export declare const CONSENT_REQUIRING_KINDS: readonly PreconditionKind[];
|
|
83
|
+
/**
|
|
84
|
+
* A pointer at a `ConsentRecord`, never a copy of one.
|
|
85
|
+
*
|
|
86
|
+
* `state` is an opaque string on purpose. `infra` does not own consent semantics;
|
|
87
|
+
* it records what the lifecycle said and when it was asked.
|
|
88
|
+
*/
|
|
89
|
+
export interface ConsentReference {
|
|
90
|
+
/** → `ConsentRecord.id` in `@medicine-wheel/consent-lifecycle` */
|
|
91
|
+
consentId: string;
|
|
92
|
+
/** the state as read at `readAt`. Absent means unread. */
|
|
93
|
+
state?: string;
|
|
94
|
+
/** ISO 8601 */
|
|
95
|
+
readAt?: string;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* One condition that must hold before a service may be considered ready.
|
|
99
|
+
*
|
|
100
|
+
* A precondition may carry a machine fact, a consent reference, or both. Both is
|
|
101
|
+
* the interesting case and the one the whole module is shaped around: running
|
|
102
|
+
* the root step that enables linger needed a human to say yes, and that yes can
|
|
103
|
+
* be withdrawn later while the flag stays enabled.
|
|
104
|
+
*/
|
|
105
|
+
export interface Precondition {
|
|
106
|
+
/** stable id for the precondition itself */
|
|
107
|
+
id: string;
|
|
108
|
+
/** the service whose readiness this gates → `ServiceFacet.nodeId` */
|
|
109
|
+
gates: NodeId;
|
|
110
|
+
/** one line an operator can read — 'gmusic linger must survive logout' */
|
|
111
|
+
description?: string;
|
|
112
|
+
/** the machine half */
|
|
113
|
+
fact?: MachineFact;
|
|
114
|
+
/** the human half, referenced by id and never embedded */
|
|
115
|
+
consent?: ConsentReference;
|
|
116
|
+
/** what a person knows about this precondition that the fields cannot hold */
|
|
117
|
+
metis?: MetisHold;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* How a precondition reads right now.
|
|
121
|
+
*
|
|
122
|
+
* Four values, and the distinctions between them are the point:
|
|
123
|
+
*
|
|
124
|
+
* - `satisfied` — every half that was declared was read, and holds.
|
|
125
|
+
* - `unsatisfied` — a machine fact was read and does not match what is expected.
|
|
126
|
+
* - `unauthorized` — the machine half holds (or was not required) and consent
|
|
127
|
+
* does not authorize. Kept separate from `unsatisfied` so a
|
|
128
|
+
* withdrawn consent can never be mistaken for a technical
|
|
129
|
+
* failure, and so an operator cannot "fix" it by restarting
|
|
130
|
+
* something.
|
|
131
|
+
* - `unknown` — a declared half has not been read. Never read as either
|
|
132
|
+
* success or failure.
|
|
133
|
+
*/
|
|
134
|
+
export type PreconditionVerdict = 'satisfied' | 'unsatisfied' | 'unauthorized' | 'unknown';
|
|
135
|
+
export interface PreconditionResult {
|
|
136
|
+
preconditionId: string;
|
|
137
|
+
gates: NodeId;
|
|
138
|
+
verdict: PreconditionVerdict;
|
|
139
|
+
/** one sentence naming what decided it */
|
|
140
|
+
reason: string;
|
|
141
|
+
/** echoed so a caller rendering a table does not have to re-join */
|
|
142
|
+
fact?: MachineFact;
|
|
143
|
+
consent?: ConsentReference;
|
|
144
|
+
metis?: MetisHold;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Read one precondition.
|
|
148
|
+
*
|
|
149
|
+
* Order of decision, and it matters:
|
|
150
|
+
*
|
|
151
|
+
* 1. A declared-but-unread half yields `unknown`. Nothing else is decided from
|
|
152
|
+
* an unread fact.
|
|
153
|
+
* 2. A read machine fact that does not match yields `unsatisfied`. The human
|
|
154
|
+
* half is not consulted — there is nothing yet to authorize.
|
|
155
|
+
* 3. A consent that does not authorize yields `unauthorized`, whatever the
|
|
156
|
+
* machine says.
|
|
157
|
+
* 4. A fact whose kind is in {@link CONSENT_REQUIRING_KINDS} and that names no
|
|
158
|
+
* consent yields `unknown`. The machine half holding is not the question.
|
|
159
|
+
* 5. A precondition declaring neither half yields `unknown`, not `satisfied`.
|
|
160
|
+
* An empty gate that reports success is how a gate stops being one.
|
|
161
|
+
*/
|
|
162
|
+
export declare function preconditionGuard(precondition: Precondition, authorizingStates?: readonly string[]): PreconditionResult;
|
|
163
|
+
export interface ServiceReadiness {
|
|
164
|
+
/** → `ServiceFacet.nodeId` */
|
|
165
|
+
service: NodeId;
|
|
166
|
+
/** true only when every declared precondition is `satisfied` */
|
|
167
|
+
ready: boolean;
|
|
168
|
+
/** how many preconditions were declared for this service. Zero is a finding. */
|
|
169
|
+
declared: number;
|
|
170
|
+
/** every result, in the order the preconditions were given */
|
|
171
|
+
results: PreconditionResult[];
|
|
172
|
+
/** the subset that is not `satisfied` — what an operator has to act on */
|
|
173
|
+
blocking: PreconditionResult[];
|
|
174
|
+
/**
|
|
175
|
+
* Every `MetisHold` carried by a blocking precondition or by the service.
|
|
176
|
+
*
|
|
177
|
+
* Surfaced rather than summarised: the reason a gate is stuck is exactly where
|
|
178
|
+
* "you have to restart it twice" tends to live, and a readiness report that
|
|
179
|
+
* drops it has flattened the one thing the operator needed.
|
|
180
|
+
*/
|
|
181
|
+
metis: MetisHold[];
|
|
182
|
+
/** one sentence for a caller that renders a single line */
|
|
183
|
+
reason: string;
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Whether a service may be considered ready, given the preconditions that gate it.
|
|
187
|
+
*
|
|
188
|
+
* Preconditions gating other services are ignored rather than silently counted —
|
|
189
|
+
* the filter is on `gates`, so passing the whole set for a host is fine.
|
|
190
|
+
*
|
|
191
|
+
* A service with **no** declared preconditions is reported `ready: true` with
|
|
192
|
+
* `declared: 0`, and the reason says so. That is a vacuous truth and the caller
|
|
193
|
+
* is told it is one; hiding it would let an unconfigured service look checked.
|
|
194
|
+
*/
|
|
195
|
+
export declare function readyService(service: ServiceFacet, preconditions: Precondition[], authorizingStates?: readonly string[]): ServiceReadiness;
|
|
196
|
+
/** Convenience: the linger fact for a tenant, shaped so the expected value is not retyped at every call site. */
|
|
197
|
+
export declare function lingerFact(tenantNodeId: NodeId, observed?: LingerState, observedAt?: string): MachineFact;
|
|
198
|
+
//# sourceMappingURL=preconditions.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"preconditions.d.ts","sourceRoot":"","sources":["../src/preconditions.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Preconditions — what must hold before West runs.
|
|
4
|
+
*
|
|
5
|
+
* A service is West: the thing that executes. A port claim, a linger flag, a
|
|
6
|
+
* unit file on disk are South: claims that must hold *first*. This module makes
|
|
7
|
+
* that ordering checkable without letting either half stand in for the other.
|
|
8
|
+
*
|
|
9
|
+
* The load-bearing separation, and the reason this file exists at all:
|
|
10
|
+
*
|
|
11
|
+
* > `linger` is a machine fact — a checkbox with no authority and no ability to
|
|
12
|
+
* > withdraw. The human authorization to run the root step that sets it is a
|
|
13
|
+
* > `ConsentRecord` in `@medicine-wheel/consent-lifecycle`. A `Precondition`
|
|
14
|
+
* > references both **by id**. Neither collapses into the other.
|
|
15
|
+
*
|
|
16
|
+
* Collapsing them is the failure this guards against: once a granted consent is
|
|
17
|
+
* inferred from a machine fact, withdrawing consent stops meaning anything,
|
|
18
|
+
* because the flag is still set. So `unauthorized` is a verdict of its own — the
|
|
19
|
+
* machine is ready and the human has not said yes — and it never reads as
|
|
20
|
+
* `unsatisfied`.
|
|
21
|
+
*
|
|
22
|
+
* A pure function over plain data. No I/O, no ontology traversal, no clock.
|
|
23
|
+
*
|
|
24
|
+
* Spec: /opt/eury/rispecs/foundations/02-specifications.md §S4.
|
|
25
|
+
*/
|
|
26
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
27
|
+
exports.CONSENT_REQUIRING_KINDS = exports.AUTHORIZING_CONSENT_STATES = void 0;
|
|
28
|
+
exports.preconditionGuard = preconditionGuard;
|
|
29
|
+
exports.readyService = readyService;
|
|
30
|
+
exports.lingerFact = lingerFact;
|
|
31
|
+
/**
|
|
32
|
+
* The consent states that authorize an action.
|
|
33
|
+
*
|
|
34
|
+
* Mirrors `ConsentState` in `@medicine-wheel/consent-lifecycle` and is exported
|
|
35
|
+
* as data so a caller can pass its own set rather than fork this file. `infra`
|
|
36
|
+
* deliberately does **not** import that package: a package that can construct a
|
|
37
|
+
* consent record can fabricate one, and this one must not be able to.
|
|
38
|
+
*/
|
|
39
|
+
exports.AUTHORIZING_CONSENT_STATES = ['granted', 'active'];
|
|
40
|
+
/**
|
|
41
|
+
* Fact kinds that cannot stand alone.
|
|
42
|
+
*
|
|
43
|
+
* `linger` is here because enabling it required running a root step, and that
|
|
44
|
+
* root step required a human to say yes. A precondition that reads the flag and
|
|
45
|
+
* references no `ConsentRecord` therefore opens the gate on the machine fact
|
|
46
|
+
* alone — which is the exact collapse this module's docstring forbids, arriving
|
|
47
|
+
* not through a wrong verdict but through an incomplete declaration.
|
|
48
|
+
*
|
|
49
|
+
* Such a precondition returns `unknown`, never `satisfied`: nothing authorized
|
|
50
|
+
* it, and nothing was checked. `unauthorized` would be wrong too — that verdict
|
|
51
|
+
* means consent was read and did not authorize, and here it was never named.
|
|
52
|
+
*/
|
|
53
|
+
exports.CONSENT_REQUIRING_KINDS = ['linger'];
|
|
54
|
+
/**
|
|
55
|
+
* Read one precondition.
|
|
56
|
+
*
|
|
57
|
+
* Order of decision, and it matters:
|
|
58
|
+
*
|
|
59
|
+
* 1. A declared-but-unread half yields `unknown`. Nothing else is decided from
|
|
60
|
+
* an unread fact.
|
|
61
|
+
* 2. A read machine fact that does not match yields `unsatisfied`. The human
|
|
62
|
+
* half is not consulted — there is nothing yet to authorize.
|
|
63
|
+
* 3. A consent that does not authorize yields `unauthorized`, whatever the
|
|
64
|
+
* machine says.
|
|
65
|
+
* 4. A fact whose kind is in {@link CONSENT_REQUIRING_KINDS} and that names no
|
|
66
|
+
* consent yields `unknown`. The machine half holding is not the question.
|
|
67
|
+
* 5. A precondition declaring neither half yields `unknown`, not `satisfied`.
|
|
68
|
+
* An empty gate that reports success is how a gate stops being one.
|
|
69
|
+
*/
|
|
70
|
+
function preconditionGuard(precondition, authorizingStates = exports.AUTHORIZING_CONSENT_STATES) {
|
|
71
|
+
const base = {
|
|
72
|
+
preconditionId: precondition.id,
|
|
73
|
+
gates: precondition.gates,
|
|
74
|
+
fact: precondition.fact,
|
|
75
|
+
consent: precondition.consent,
|
|
76
|
+
metis: precondition.metis,
|
|
77
|
+
};
|
|
78
|
+
const { fact, consent } = precondition;
|
|
79
|
+
if (!fact && !consent) {
|
|
80
|
+
return {
|
|
81
|
+
...base,
|
|
82
|
+
verdict: 'unknown',
|
|
83
|
+
reason: 'precondition declares neither a machine fact nor a consent reference — nothing was checked',
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
if (fact && fact.observed === undefined) {
|
|
87
|
+
return {
|
|
88
|
+
...base,
|
|
89
|
+
verdict: 'unknown',
|
|
90
|
+
reason: `machine fact '${fact.kind}' on ${fact.facetNodeId} has not been read`,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
if (fact && fact.observed !== fact.expected) {
|
|
94
|
+
return {
|
|
95
|
+
...base,
|
|
96
|
+
verdict: 'unsatisfied',
|
|
97
|
+
reason: `machine fact '${fact.kind}' on ${fact.facetNodeId} read '${fact.observed}', expected '${fact.expected}'`,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
if (fact && !consent && exports.CONSENT_REQUIRING_KINDS.includes(fact.kind)) {
|
|
101
|
+
return {
|
|
102
|
+
...base,
|
|
103
|
+
verdict: 'unknown',
|
|
104
|
+
reason: `machine fact '${fact.kind}' holds, but this precondition names no ConsentRecord — ` +
|
|
105
|
+
`the root step that set it required a human to say yes, and no yes was referenced`,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
if (consent && consent.state === undefined) {
|
|
109
|
+
return {
|
|
110
|
+
...base,
|
|
111
|
+
verdict: 'unknown',
|
|
112
|
+
reason: `consent ${consent.consentId} has not been read from the lifecycle`,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
if (consent && !authorizingStates.includes(consent.state)) {
|
|
116
|
+
return {
|
|
117
|
+
...base,
|
|
118
|
+
verdict: 'unauthorized',
|
|
119
|
+
reason: `consent ${consent.consentId} is '${consent.state}' — the machine half may hold, but this is not authorization`,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
return {
|
|
123
|
+
...base,
|
|
124
|
+
verdict: 'satisfied',
|
|
125
|
+
reason: fact
|
|
126
|
+
? `machine fact '${fact.kind}' reads '${fact.observed}'${consent ? ` and consent ${consent.consentId} is '${consent.state}'` : ''}`
|
|
127
|
+
: `consent ${consent?.consentId} is '${consent?.state}'`,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Whether a service may be considered ready, given the preconditions that gate it.
|
|
132
|
+
*
|
|
133
|
+
* Preconditions gating other services are ignored rather than silently counted —
|
|
134
|
+
* the filter is on `gates`, so passing the whole set for a host is fine.
|
|
135
|
+
*
|
|
136
|
+
* A service with **no** declared preconditions is reported `ready: true` with
|
|
137
|
+
* `declared: 0`, and the reason says so. That is a vacuous truth and the caller
|
|
138
|
+
* is told it is one; hiding it would let an unconfigured service look checked.
|
|
139
|
+
*/
|
|
140
|
+
function readyService(service, preconditions, authorizingStates = exports.AUTHORIZING_CONSENT_STATES) {
|
|
141
|
+
const mine = preconditions.filter((p) => p.gates === service.nodeId);
|
|
142
|
+
const results = mine.map((p) => preconditionGuard(p, authorizingStates));
|
|
143
|
+
const blocking = results.filter((r) => r.verdict !== 'satisfied');
|
|
144
|
+
const metis = [];
|
|
145
|
+
if (service.metis)
|
|
146
|
+
metis.push(service.metis);
|
|
147
|
+
for (const r of blocking)
|
|
148
|
+
if (r.metis)
|
|
149
|
+
metis.push(r.metis);
|
|
150
|
+
const ready = blocking.length === 0;
|
|
151
|
+
let reason;
|
|
152
|
+
if (mine.length === 0) {
|
|
153
|
+
reason = `no preconditions are declared for ${service.unit} — ready is vacuously true, not verified`;
|
|
154
|
+
}
|
|
155
|
+
else if (ready) {
|
|
156
|
+
reason = `all ${mine.length} precondition(s) satisfied for ${service.unit}`;
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
const counts = blocking.reduce((acc, r) => {
|
|
160
|
+
acc[r.verdict] = (acc[r.verdict] ?? 0) + 1;
|
|
161
|
+
return acc;
|
|
162
|
+
}, {});
|
|
163
|
+
const shape = Object.entries(counts)
|
|
164
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
165
|
+
.map(([verdict, n]) => `${n} ${verdict}`)
|
|
166
|
+
.join(', ');
|
|
167
|
+
reason = `${service.unit} is blocked: ${shape}`;
|
|
168
|
+
}
|
|
169
|
+
return { service: service.nodeId, ready, declared: mine.length, results, blocking, metis, reason };
|
|
170
|
+
}
|
|
171
|
+
/** Convenience: the linger fact for a tenant, shaped so the expected value is not retyped at every call site. */
|
|
172
|
+
function lingerFact(tenantNodeId, observed, observedAt) {
|
|
173
|
+
return {
|
|
174
|
+
kind: 'linger',
|
|
175
|
+
facetNodeId: tenantNodeId,
|
|
176
|
+
expected: 'enabled',
|
|
177
|
+
observed,
|
|
178
|
+
observedAt,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
//# sourceMappingURL=preconditions.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"preconditions.js","sourceRoot":"","sources":["../src/preconditions.ts"],"names":[],"mappings":"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"}
|