@metamynd/mmt-graph 0.2.0

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,207 @@
1
+ /**
2
+ * The mutation-event vocabulary the engine consumes. Anchor-compatible from day one: a
3
+ * discriminated `{op, ...}` envelope (matching magp-hcs2's convention) carrying only the
4
+ * fields the projector needs — not full DB rows — the same additive-only discipline
5
+ * evidence leaves already follow (see evidence-batch.ts's canonical()). Where an event
6
+ * comes from (a Postgres trigger, a migration script, HCS replay) is Phase 2/3, not here.
7
+ *
8
+ * These map onto real `Policy`/`AgentIdentity`/`Principal` rows one-for-one — see
9
+ * docs/design/metamynd-trust-ontology.md domains A/C for the grounding. Notably, a
10
+ * delegated mandate is NOT a separate concept from a root mandate: `Policy` carries
11
+ * delegation fields (`delegatedFromPolicyId`, `delegationDepth`) on the same row as the
12
+ * mandate itself, so `MandateGranted` carries them as optional fields rather than being a
13
+ * distinct event type.
14
+ */
15
+
16
+ /**
17
+ * `principalDid` on AgentRegistered is the agent's HOME organisation (AgentIdentity.
18
+ * organizationPrincipalId in accountability.ts) — a structural fact independent of any
19
+ * mandate, distinct from `mmt:actsFor` (which domain A already defines as the root
20
+ * mandate's principalDid). Phase 5's fifth slice (mmt-graph.accountability.mjs) projects
21
+ * this onto `mmt:memberOf`, reusing the exact relationship the fourth slice's
22
+ * organization/unit cohorts already established for the same underlying field — see that
23
+ * slice's note in mmt-graph.project.mjs for why memberOf and actsFor must not be conflated.
24
+ */
25
+ /** @typedef {{ op: 'agent-registered', agentDid: string, kind?: string, principalDid?: string }} AgentRegistered */
26
+ /**
27
+ * The verification/`authorizedRepUserId` fields are a snapshot of the Principal row at
28
+ * event time, not a separate lifecycle event — the same "current-state field on a
29
+ * recording event" shape evidenceRecorded/batchAnchored already use for anchorStatus,
30
+ * chosen for the same reason: this package proves graph shape, not full temporal
31
+ * event-sourcing of every state transition.
32
+ */
33
+ /**
34
+ * @typedef {{
35
+ * op: 'principal-registered',
36
+ * principalDid: string,
37
+ * principalType?: string,
38
+ * verificationStatus?: string,
39
+ * assuranceLevel?: string,
40
+ * verificationExpiresAt?: string,
41
+ * authorizedRepUserId?: string,
42
+ * }} PrincipalRegistered
43
+ */
44
+ /**
45
+ * `expiresAt`, added in Phase 5's sixth slice, is Policy.expiresAt — needed for
46
+ * blast-radius.ts's live/expired split (`isLive`), which the earlier slices never needed
47
+ * (evidence-path and accountability don't gate on mandate liveness). Absent means no
48
+ * expiry, not expired — same rule as the real `isLive`.
49
+ */
50
+ /**
51
+ * @typedef {{
52
+ * op: 'mandate-granted',
53
+ * policyId: string,
54
+ * agentDid: string,
55
+ * principalDid?: string, // set on a root mandate (Policy.principalDid)
56
+ * delegatedFromPolicyId?: string, // set on a delegated mandate (Policy.delegatedFromPolicyId)
57
+ * delegationDepth?: number, // set alongside delegatedFromPolicyId, 1..MAX_DELEGATION_DEPTH
58
+ * expiresAt?: string, // Policy.expiresAt, ISO — absent means no expiry
59
+ * }} MandateGranted
60
+ */
61
+ /** @typedef {{ op: 'mandate-revoked', policyId: string }} MandateRevoked */
62
+
63
+ /**
64
+ * Domain H (runtime activity/evidence) — Phase 5's first slice, proving the engine can
65
+ * answer evidence-path.ts's "independently verifiable" question, not a full replacement
66
+ * of it (see mmt-graph.evidence-path.mjs's header). Grounded in the real fields
67
+ * evidence-path.ts/evidence.model.ts actually use, same additive-only discipline as the
68
+ * other event types above — not the full DB row.
69
+ */
70
+ /**
71
+ * `agentDid`, added in Phase 5's fifth slice, is the decision's actor — the edge
72
+ * accountability.ts's own graph builder already calls `decided-for`, adopted here as
73
+ * `mmt:decidedFor` since Domain H names no equivalent predicate (see
74
+ * mmt-graph.accountability.mjs).
75
+ */
76
+ /** @typedef {{ op: 'decision-recorded', decisionId: string, decisionDigest?: string, agentDid?: string }} DecisionRecorded */
77
+ /** @typedef {{ op: 'evidence-recorded', eventId: string, decisionDigest?: string, anchorStatus?: string }} EvidenceRecorded */
78
+ /** @typedef {{ op: 'batch-anchored', batchId: string, memberEventIds: string[], anchorStatus?: string }} BatchAnchored */
79
+
80
+ /**
81
+ * Domain F (governance) — Phase 5's second slice, proving the engine can answer
82
+ * contagion.ts's cohort-specificity question (see mmt-graph.contagion.mjs's header).
83
+ * `sop`/`standard` share one pattern in the real algorithm (`buildKeyed` in contagion.ts,
84
+ * keyed via a junction table). `organization`/`unit` (Phase 5's fourth slice) use a
85
+ * different shape (`buildSingle`, keyed off
86
+ * AgentIdentity.organizationPrincipalId/operatingUnitId — a direct FK, not a junction
87
+ * table) but the SAME event/projector mechanism — see mmt-graph.project.mjs's
88
+ * projectCohortShared for why `organization`/`unit` additionally assert `mmt:memberOf`,
89
+ * the real Domain A relationship, alongside the mechanical `mmt:sharesCohort` edge.
90
+ */
91
+ /** @typedef {{ op: 'cohort-shared', agentDid: string, kind: 'sop' | 'standard' | 'organization' | 'unit', key: string, label: string, version?: number }} CohortShared */
92
+
93
+ /**
94
+ * Domain A — Phase 5's fifth slice, proving the engine can answer accountability.ts's "who
95
+ * is answerable" question (see mmt-graph.accountability.mjs's header). `mmt:Human` is named
96
+ * in domain A's actor taxonomy but was `[PROPOSED]`/unimplemented until now; humans have no
97
+ * DID, so `userId` mints a synthetic IRI the same way `mandateIri`/`decisionIri` do for
98
+ * their own non-DID identifiers.
99
+ */
100
+ /** @typedef {{ op: 'human-registered', userId: string, email: string, firstName?: string, lastName?: string }} HumanRegistered */
101
+
102
+ /**
103
+ * Domain A/B — Phase 5's sixth slice, proving the engine can answer blast-radius.ts's
104
+ * "who does this issuer's tier depend on, and what breaks if it lapses" question (see
105
+ * mmt-graph.blast-radius.mjs's header). `mmt:Issuer` is the one genuinely new class this
106
+ * package adds — the W3C VC Data Model's own term, not invented, since `issuer.model.ts`'s
107
+ * `did` field means an issuer is exactly as IRI-able as an agent or a principal. `did` is
108
+ * required, not `issuerId`: same reasoning as agentIri/principalIri using the DID itself as
109
+ * the node's IRI rather than minting a synthetic one.
110
+ */
111
+ /** @typedef {{ op: 'issuer-registered', did: string, name?: string, principalDid?: string }} IssuerRegistered */
112
+
113
+ const COHORT_KINDS = ['sop', 'standard', 'organization', 'unit'];
114
+
115
+ /** The bound this engine enforces on delegationDepth — mirrors policy-core/delegation.ts's MAX_DELEGATION_DEPTH. */
116
+ export const MAX_DELEGATION_DEPTH = 3;
117
+
118
+ function requireFields(event, fields) {
119
+ const missing = fields.filter((f) => event[f] === undefined || event[f] === null);
120
+ if (missing.length > 0) throw new Error(`${event.op}: missing required field(s): ${missing.join(', ')}`);
121
+ }
122
+
123
+ /** @returns {AgentRegistered} */
124
+ export function agentRegistered({ agentDid, kind, principalDid }) {
125
+ const ev = { op: 'agent-registered', agentDid, kind, principalDid };
126
+ requireFields(ev, ['agentDid']);
127
+ return ev;
128
+ }
129
+
130
+ /** @returns {PrincipalRegistered} */
131
+ export function principalRegistered({
132
+ principalDid,
133
+ principalType,
134
+ verificationStatus,
135
+ assuranceLevel,
136
+ verificationExpiresAt,
137
+ authorizedRepUserId,
138
+ }) {
139
+ const ev = { op: 'principal-registered', principalDid, principalType, verificationStatus, assuranceLevel, verificationExpiresAt, authorizedRepUserId };
140
+ requireFields(ev, ['principalDid']);
141
+ return ev;
142
+ }
143
+
144
+ /** @returns {MandateGranted} */
145
+ export function mandateGranted({ policyId, agentDid, principalDid, delegatedFromPolicyId, delegationDepth, expiresAt }) {
146
+ const ev = { op: 'mandate-granted', policyId, agentDid, principalDid, delegatedFromPolicyId, delegationDepth, expiresAt };
147
+ requireFields(ev, ['policyId', 'agentDid']);
148
+ if (delegatedFromPolicyId && (delegationDepth === undefined || delegationDepth === null)) {
149
+ throw new Error(`mandate-granted for ${policyId}: delegatedFromPolicyId set without delegationDepth`);
150
+ }
151
+ if (delegationDepth !== undefined && (delegationDepth < 1 || delegationDepth > MAX_DELEGATION_DEPTH)) {
152
+ throw new Error(`mandate-granted for ${policyId}: delegationDepth ${delegationDepth} outside 1..${MAX_DELEGATION_DEPTH}`);
153
+ }
154
+ return ev;
155
+ }
156
+
157
+ /** @returns {MandateRevoked} */
158
+ export function mandateRevoked({ policyId }) {
159
+ const ev = { op: 'mandate-revoked', policyId };
160
+ requireFields(ev, ['policyId']);
161
+ return ev;
162
+ }
163
+
164
+ /** @returns {DecisionRecorded} */
165
+ export function decisionRecorded({ decisionId, decisionDigest, agentDid }) {
166
+ const ev = { op: 'decision-recorded', decisionId, decisionDigest, agentDid };
167
+ requireFields(ev, ['decisionId']);
168
+ return ev;
169
+ }
170
+
171
+ /** @returns {EvidenceRecorded} */
172
+ export function evidenceRecorded({ eventId, decisionDigest, anchorStatus }) {
173
+ const ev = { op: 'evidence-recorded', eventId, decisionDigest, anchorStatus };
174
+ requireFields(ev, ['eventId']);
175
+ return ev;
176
+ }
177
+
178
+ /** @returns {BatchAnchored} */
179
+ export function batchAnchored({ batchId, memberEventIds, anchorStatus }) {
180
+ const ev = { op: 'batch-anchored', batchId, memberEventIds, anchorStatus };
181
+ requireFields(ev, ['batchId', 'memberEventIds']);
182
+ return ev;
183
+ }
184
+
185
+ /** @returns {CohortShared} */
186
+ export function cohortShared({ agentDid, kind, key, label, version }) {
187
+ const ev = { op: 'cohort-shared', agentDid, kind, key, label, version };
188
+ requireFields(ev, ['agentDid', 'kind', 'key', 'label']);
189
+ if (!COHORT_KINDS.includes(kind)) {
190
+ throw new Error(`cohort-shared: kind must be one of ${COHORT_KINDS.join(', ')} (got "${kind}")`);
191
+ }
192
+ return ev;
193
+ }
194
+
195
+ /** @returns {HumanRegistered} */
196
+ export function humanRegistered({ userId, email, firstName, lastName }) {
197
+ const ev = { op: 'human-registered', userId, email, firstName, lastName };
198
+ requireFields(ev, ['userId', 'email']);
199
+ return ev;
200
+ }
201
+
202
+ /** @returns {IssuerRegistered} */
203
+ export function issuerRegistered({ did, name, principalDid }) {
204
+ const ev = { op: 'issuer-registered', did, name, principalDid };
205
+ requireFields(ev, ['did']);
206
+ return ev;
207
+ }
package/package.json ADDED
@@ -0,0 +1,75 @@
1
+ {
2
+ "name": "@metamynd/mmt-graph",
3
+ "version": "0.2.0",
4
+ "type": "module",
5
+ "description": "Reference trust-graph engine (Oxigraph/SPARQL + SHACL) + HCS-anchor contract: apply mutation events, validate the MMTO §3 authority-chain invariant, batch the changelog into a Merkle root ready to anchor, and verify a disclosed tenant graph with MetaMynd fully offline. Hosting-mode-agnostic — no MetaMynd backend, no Postgres, no MetaMynd credentials anywhere in the code path. See docs/design/metamynd-trust-ontology.md Appendix C.",
6
+ "main": "mmt-graph.engine.mjs",
7
+ "exports": {
8
+ ".": "./mmt-graph.engine.mjs",
9
+ "./types": "./mmt-graph.types.mjs",
10
+ "./project": "./mmt-graph.project.mjs",
11
+ "./changelog": "./mmt-graph.changelog.mjs",
12
+ "./anchor": "./mmt-graph.anchor.mjs",
13
+ "./evidence-path": "./mmt-graph.evidence-path.mjs",
14
+ "./contagion": "./mmt-graph.contagion.mjs",
15
+ "./accountability": "./mmt-graph.accountability.mjs",
16
+ "./blast-radius": "./mmt-graph.blast-radius.mjs",
17
+ "./auditor": "./mmt-graph.auditor.mjs"
18
+ },
19
+ "bin": {
20
+ "mmt-graph-auditor": "mmt-graph.auditor-cli.mjs"
21
+ },
22
+ "files": [
23
+ "mmt-graph.types.mjs",
24
+ "mmt-graph.project.mjs",
25
+ "mmt-graph.shapes.ttl",
26
+ "mmt-graph.engine.mjs",
27
+ "mmt-graph.merkle.mjs",
28
+ "mmt-graph.changelog.mjs",
29
+ "mmt-graph.anchor.mjs",
30
+ "mmt-graph.anchor-live-demo.mjs",
31
+ "mmt-graph.evidence-path.mjs",
32
+ "mmt-graph.contagion.mjs",
33
+ "mmt-graph.accountability.mjs",
34
+ "mmt-graph.blast-radius.mjs",
35
+ "mmt-graph.auditor.mjs",
36
+ "mmt-graph.auditor-cli.mjs",
37
+ "mmt-graph.self-host-quickstart.mjs",
38
+ "README.md"
39
+ ],
40
+ "scripts": {
41
+ "spike": "node mmt-graph.spike.mjs",
42
+ "test": "node mmt-graph.test.mjs && node mmt-graph.merkle.test.mjs && node mmt-graph.changelog.test.mjs && node mmt-graph.anchor.test.mjs && node mmt-graph.evidence-path.test.mjs && node mmt-graph.contagion.test.mjs && node mmt-graph.accountability.test.mjs && node mmt-graph.blast-radius.test.mjs && node mmt-graph.auditor.test.mjs",
43
+ "anchor-live-demo": "node mmt-graph.anchor-live-demo.mjs",
44
+ "auditor": "node mmt-graph.auditor-cli.mjs",
45
+ "self-host-quickstart": "node mmt-graph.self-host-quickstart.mjs"
46
+ },
47
+ "dependencies": {
48
+ "@zazuko/env-node": "^3.1.0",
49
+ "oxigraph": "^0.5.5",
50
+ "rdf-validate-shacl": "^0.6.5"
51
+ },
52
+ "devDependencies": {
53
+ "@hashgraph/sdk": "^2.81.0",
54
+ "dotenv": "^17.4.2"
55
+ },
56
+ "engines": {
57
+ "node": ">=18"
58
+ },
59
+ "keywords": [
60
+ "metamynd",
61
+ "trust-graph",
62
+ "rdf",
63
+ "sparql",
64
+ "shacl",
65
+ "ontology",
66
+ "mmto",
67
+ "hedera",
68
+ "hcs",
69
+ "agent-governance",
70
+ "self-hosted",
71
+ "verifiable"
72
+ ],
73
+ "author": "MetaMynd",
74
+ "license": "MIT"
75
+ }