holarchy-core-logic 0.1.1
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/dist/authoring.d.ts +44 -0
- package/dist/authoring.d.ts.map +1 -0
- package/dist/authoring.js +580 -0
- package/dist/authoring.js.map +1 -0
- package/dist/authority-store.d.ts +11 -0
- package/dist/authority-store.d.ts.map +1 -0
- package/dist/authority-store.js +170 -0
- package/dist/authority-store.js.map +1 -0
- package/dist/effective-snapshot.d.ts +7 -0
- package/dist/effective-snapshot.d.ts.map +1 -0
- package/dist/effective-snapshot.js +160 -0
- package/dist/effective-snapshot.js.map +1 -0
- package/dist/effective.d.ts +21 -0
- package/dist/effective.d.ts.map +1 -0
- package/dist/effective.js +72 -0
- package/dist/effective.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/validator.d.ts +79 -0
- package/dist/validator.d.ts.map +1 -0
- package/dist/validator.js +176 -0
- package/dist/validator.js.map +1 -0
- package/package.json +12 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { type AggregateDiffDto, type AuthorDraftCommand, type DependencyImpactDto, type HolarchyChangeDto, type HolarchyChangeSetDto, type HolarchyDraftInput, type HolarchySnapshotDto, type HolonDiagnostic, type HolonDiagnosticCode, type PreviewBasisDto, type VersionFrontier } from "holarchy-core-contract";
|
|
2
|
+
import type { JsonObject } from "depa-domain-protocol";
|
|
3
|
+
import { type DesiredHolarchyFacts } from "./validator";
|
|
4
|
+
export declare class AuthoringContractError extends Error {
|
|
5
|
+
readonly code: HolonDiagnosticCode;
|
|
6
|
+
readonly path: string;
|
|
7
|
+
constructor(code: HolonDiagnosticCode, path: string, message: string);
|
|
8
|
+
}
|
|
9
|
+
export declare function parseHolarchyChange(input: unknown, path: string): HolarchyChangeDto;
|
|
10
|
+
export declare function parseHolarchyDraft(input: unknown, path?: string): HolarchyDraftInput;
|
|
11
|
+
export declare function parseAuthorDraftCommand(input: unknown, path?: string): AuthorDraftCommand;
|
|
12
|
+
export declare function sha256Basis(input: string): string;
|
|
13
|
+
export type VersionEntity = keyof VersionFrontier;
|
|
14
|
+
export interface PlannedIdentityCreate {
|
|
15
|
+
readonly entity: "OrganizationalSubject" | "Member" | "Holon" | "Role" | "GovernancePolicy" | "HolonMembership" | "RoleAssignment";
|
|
16
|
+
readonly values: JsonObject;
|
|
17
|
+
}
|
|
18
|
+
export interface PlannedVersionAppend {
|
|
19
|
+
readonly entity: VersionEntity;
|
|
20
|
+
readonly logicalId: string;
|
|
21
|
+
readonly logicalIdKey: "memberId" | "holonId" | "roleId" | "governancePolicyId" | "membershipId" | "roleAssignmentId";
|
|
22
|
+
readonly effectiveState: boolean;
|
|
23
|
+
readonly payload: JsonObject;
|
|
24
|
+
}
|
|
25
|
+
export interface HolarchyMutationPlanStep {
|
|
26
|
+
readonly change: HolarchyChangeDto;
|
|
27
|
+
readonly identities: readonly PlannedIdentityCreate[];
|
|
28
|
+
readonly version: PlannedVersionAppend;
|
|
29
|
+
}
|
|
30
|
+
export interface HolarchyChangeSetPlan {
|
|
31
|
+
readonly steps: readonly HolarchyMutationPlanStep[];
|
|
32
|
+
readonly facts: DesiredHolarchyFacts;
|
|
33
|
+
readonly before: HolarchySnapshotDto;
|
|
34
|
+
readonly after: HolarchySnapshotDto;
|
|
35
|
+
readonly aggregateDiff: AggregateDiffDto;
|
|
36
|
+
readonly dependencyImpacts: readonly DependencyImpactDto[];
|
|
37
|
+
readonly diagnostics: readonly HolonDiagnostic[];
|
|
38
|
+
readonly basis: PreviewBasisDto;
|
|
39
|
+
readonly valid: boolean;
|
|
40
|
+
}
|
|
41
|
+
export declare function projectHolarchySnapshot(facts: DesiredHolarchyFacts, effectiveDate: string): HolarchySnapshotDto;
|
|
42
|
+
export declare function previewBasis(facts: DesiredHolarchyFacts, changeSet: Pick<HolarchyChangeSetDto, "id" | "revision" | "effectiveDate">): PreviewBasisDto;
|
|
43
|
+
export declare function planHolarchyChangeSet(base: DesiredHolarchyFacts, changeSet: HolarchyChangeSetDto, executionInstant: string): HolarchyChangeSetPlan;
|
|
44
|
+
//# sourceMappingURL=authoring.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"authoring.d.ts","sourceRoot":"","sources":["../src/authoring.ts"],"names":[],"mappings":"AACA,OAAO,EAGL,KAAK,gBAAgB,EAErB,KAAK,kBAAkB,EACvB,KAAK,mBAAmB,EACxB,KAAK,iBAAiB,EAEtB,KAAK,oBAAoB,EACzB,KAAK,kBAAkB,EAEvB,KAAK,mBAAmB,EACxB,KAAK,eAAe,EACpB,KAAK,mBAAmB,EAExB,KAAK,eAAe,EACpB,KAAK,eAAe,EACrB,MAAM,wBAAwB,CAAC;AAChC,OAAO,KAAK,EAAE,UAAU,EAAa,MAAM,sBAAsB,CAAC;AAElE,OAAO,EAAsD,KAAK,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAE5G,qBAAa,sBAAuB,SAAQ,KAAK;IACnC,QAAQ,CAAC,IAAI,EAAE,mBAAmB;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM;gBAAhD,IAAI,EAAE,mBAAmB,EAAW,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM;CAIvF;AAsKD,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,iBAAiB,CAanF;AAED,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,SAA+B,GAAG,kBAAkB,CAe1G;AAED,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,SAAyB,GAAG,kBAAkB,CAOzG;AAED,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEjD;AAED,MAAM,MAAM,aAAa,GAAG,MAAM,eAAe,CAAC;AAClD,MAAM,WAAW,qBAAqB;IAAG,QAAQ,CAAC,MAAM,EAAE,uBAAuB,GAAG,QAAQ,GAAG,OAAO,GAAG,MAAM,GAAG,kBAAkB,GAAG,iBAAiB,GAAG,gBAAgB,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAA;CAAE;AAC1M,MAAM,WAAW,oBAAoB;IAAG,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC;IAAC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,YAAY,EAAE,UAAU,GAAG,SAAS,GAAG,QAAQ,GAAG,oBAAoB,GAAG,cAAc,GAAG,kBAAkB,CAAC;IAAC,QAAQ,CAAC,cAAc,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,UAAU,CAAA;CAAE;AAC3R,MAAM,WAAW,wBAAwB;IAAG,QAAQ,CAAC,MAAM,EAAE,iBAAiB,CAAC;IAAC,QAAQ,CAAC,UAAU,EAAE,SAAS,qBAAqB,EAAE,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,oBAAoB,CAAA;CAAE;AAC/K,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,KAAK,EAAE,SAAS,wBAAwB,EAAE,CAAC;IACpD,QAAQ,CAAC,KAAK,EAAE,oBAAoB,CAAC;IACrC,QAAQ,CAAC,MAAM,EAAE,mBAAmB,CAAC;IACrC,QAAQ,CAAC,KAAK,EAAE,mBAAmB,CAAC;IACpC,QAAQ,CAAC,aAAa,EAAE,gBAAgB,CAAC;IACzC,QAAQ,CAAC,iBAAiB,EAAE,SAAS,mBAAmB,EAAE,CAAC;IAC3D,QAAQ,CAAC,WAAW,EAAE,SAAS,eAAe,EAAE,CAAC;IACjD,QAAQ,CAAC,KAAK,EAAE,eAAe,CAAC;IAChC,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;CACzB;AAWD,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,oBAAoB,EAAE,aAAa,EAAE,MAAM,GAAG,mBAAmB,CAU/G;AAuCD,wBAAgB,YAAY,CAAC,KAAK,EAAE,oBAAoB,EAAE,SAAS,EAAE,IAAI,CAAC,oBAAoB,EAAE,IAAI,GAAG,UAAU,GAAG,eAAe,CAAC,GAAG,eAAe,CAIrJ;AAED,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,oBAAoB,EAAE,SAAS,EAAE,oBAAoB,EAAE,gBAAgB,EAAE,MAAM,GAAG,qBAAqB,CA0JlJ"}
|
|
@@ -0,0 +1,580 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { HOLON_CHANGE_KINDS, MEMBER_PRINCIPAL_KINDS, } from "holarchy-core-contract";
|
|
3
|
+
import { isCanonicalDay } from "./effective";
|
|
4
|
+
import { validateDesiredHolarchy } from "./validator";
|
|
5
|
+
export class AuthoringContractError extends Error {
|
|
6
|
+
code;
|
|
7
|
+
path;
|
|
8
|
+
constructor(code, path, message) {
|
|
9
|
+
super(`${path}: ${message}`);
|
|
10
|
+
this.code = code;
|
|
11
|
+
this.path = path;
|
|
12
|
+
this.name = "AuthoringContractError";
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
const fail = (code, path, message) => {
|
|
16
|
+
throw new AuthoringContractError(code, path, message);
|
|
17
|
+
};
|
|
18
|
+
function object(value, path) {
|
|
19
|
+
if (value === null || typeof value !== "object" || Array.isArray(value))
|
|
20
|
+
return fail("INVALID_CHANGE_PAYLOAD", path, "Expected an object");
|
|
21
|
+
let prototype;
|
|
22
|
+
let keys;
|
|
23
|
+
try {
|
|
24
|
+
prototype = Object.getPrototypeOf(value);
|
|
25
|
+
keys = Reflect.ownKeys(value);
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return fail("INVALID_CHANGE_PAYLOAD", path, "Object shape cannot be inspected safely");
|
|
29
|
+
}
|
|
30
|
+
if (prototype !== Object.prototype && prototype !== null)
|
|
31
|
+
return fail("INVALID_CHANGE_PAYLOAD", path, "Expected a plain object");
|
|
32
|
+
const normalized = Object.create(null);
|
|
33
|
+
for (const key of keys) {
|
|
34
|
+
if (typeof key === "symbol")
|
|
35
|
+
return fail("INVALID_CHANGE_PAYLOAD", path, "Symbol fields are not supported");
|
|
36
|
+
let descriptor;
|
|
37
|
+
try {
|
|
38
|
+
descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return fail("INVALID_CHANGE_PAYLOAD", `${path}.${key}`, "Field cannot be inspected safely");
|
|
42
|
+
}
|
|
43
|
+
if (descriptor === undefined || !("value" in descriptor))
|
|
44
|
+
return fail("INVALID_CHANGE_PAYLOAD", `${path}.${key}`, "Accessor fields are not supported");
|
|
45
|
+
if (!descriptor.enumerable)
|
|
46
|
+
return fail("INVALID_CHANGE_PAYLOAD", `${path}.${key}`, "Non-enumerable fields are not supported");
|
|
47
|
+
normalized[key] = descriptor.value;
|
|
48
|
+
}
|
|
49
|
+
return normalized;
|
|
50
|
+
}
|
|
51
|
+
function array(value, path) {
|
|
52
|
+
if (!Array.isArray(value))
|
|
53
|
+
return fail("INVALID_CHANGE_PAYLOAD", path, "Expected an array");
|
|
54
|
+
let prototype;
|
|
55
|
+
let keys;
|
|
56
|
+
let lengthDescriptor;
|
|
57
|
+
try {
|
|
58
|
+
prototype = Object.getPrototypeOf(value);
|
|
59
|
+
keys = Reflect.ownKeys(value);
|
|
60
|
+
lengthDescriptor = Object.getOwnPropertyDescriptor(value, "length");
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return fail("INVALID_CHANGE_PAYLOAD", path, "Array shape cannot be inspected safely");
|
|
64
|
+
}
|
|
65
|
+
if (prototype !== Array.prototype || lengthDescriptor === undefined || !("value" in lengthDescriptor) || !Number.isSafeInteger(lengthDescriptor.value)) {
|
|
66
|
+
return fail("INVALID_CHANGE_PAYLOAD", path, "Expected a plain array");
|
|
67
|
+
}
|
|
68
|
+
const length = Number(lengthDescriptor.value);
|
|
69
|
+
for (const key of keys) {
|
|
70
|
+
if (typeof key === "symbol")
|
|
71
|
+
return fail("INVALID_CHANGE_PAYLOAD", path, "Symbol fields are not supported");
|
|
72
|
+
if (key === "length")
|
|
73
|
+
continue;
|
|
74
|
+
const index = Number(key);
|
|
75
|
+
if (!Number.isSafeInteger(index) || index < 0 || index >= length || String(index) !== key) {
|
|
76
|
+
return fail("INVALID_CHANGE_PAYLOAD", `${path}.${key}`, "Unsupported array field");
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
const normalized = [];
|
|
80
|
+
for (let index = 0; index < length; index += 1) {
|
|
81
|
+
let descriptor;
|
|
82
|
+
try {
|
|
83
|
+
descriptor = Object.getOwnPropertyDescriptor(value, String(index));
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return fail("INVALID_CHANGE_PAYLOAD", `${path}[${index}]`, "Array item cannot be inspected safely");
|
|
87
|
+
}
|
|
88
|
+
if (descriptor === undefined)
|
|
89
|
+
return fail("INVALID_CHANGE_PAYLOAD", `${path}[${index}]`, "Sparse arrays are not supported");
|
|
90
|
+
if (!("value" in descriptor))
|
|
91
|
+
return fail("INVALID_CHANGE_PAYLOAD", `${path}[${index}]`, "Accessor array items are not supported");
|
|
92
|
+
if (!descriptor.enumerable)
|
|
93
|
+
return fail("INVALID_CHANGE_PAYLOAD", `${path}[${index}]`, "Non-enumerable array items are not supported");
|
|
94
|
+
normalized.push(descriptor.value);
|
|
95
|
+
}
|
|
96
|
+
return normalized;
|
|
97
|
+
}
|
|
98
|
+
function exactKeys(value, required, optional, path) {
|
|
99
|
+
for (const key of required)
|
|
100
|
+
if (!Object.hasOwn(value, key))
|
|
101
|
+
fail("INVALID_CHANGE_PAYLOAD", `${path}.${key}`, "Required field is missing");
|
|
102
|
+
const allowed = new Set([...required, ...optional]);
|
|
103
|
+
for (const key of Object.keys(value))
|
|
104
|
+
if (!allowed.has(key))
|
|
105
|
+
fail("INVALID_CHANGE_PAYLOAD", `${path}.${key}`, "Unsupported field");
|
|
106
|
+
}
|
|
107
|
+
function string(value, path) {
|
|
108
|
+
if (typeof value !== "string" || value.length === 0)
|
|
109
|
+
return fail("INVALID_CHANGE_PAYLOAD", path, "Expected a non-empty string");
|
|
110
|
+
return value;
|
|
111
|
+
}
|
|
112
|
+
function integer(value, path, minimum = 0) {
|
|
113
|
+
if (!Number.isSafeInteger(value) || Number(value) < minimum)
|
|
114
|
+
return fail("INVALID_CHANGE_PAYLOAD", path, `Expected an integer >= ${minimum}`);
|
|
115
|
+
return Number(value);
|
|
116
|
+
}
|
|
117
|
+
function nativeBoolean(value, path) {
|
|
118
|
+
if (typeof value !== "boolean")
|
|
119
|
+
return fail("INVALID_CHANGE_PAYLOAD", path, "Expected a native boolean");
|
|
120
|
+
return value;
|
|
121
|
+
}
|
|
122
|
+
function strings(value, path) {
|
|
123
|
+
return array(value, path).map((item, index) => string(item, `${path}[${index}]`));
|
|
124
|
+
}
|
|
125
|
+
function json(value, path, seen = new WeakSet()) {
|
|
126
|
+
if (value === null || typeof value === "string" || typeof value === "boolean")
|
|
127
|
+
return value;
|
|
128
|
+
if (typeof value === "number") {
|
|
129
|
+
if (!Number.isFinite(value))
|
|
130
|
+
return fail("INVALID_CHANGE_PAYLOAD", path, "Expected a finite JSON number");
|
|
131
|
+
return value;
|
|
132
|
+
}
|
|
133
|
+
if (typeof value !== "object")
|
|
134
|
+
return fail("INVALID_CHANGE_PAYLOAD", path, "Expected a JSON value");
|
|
135
|
+
if (seen.has(value))
|
|
136
|
+
return fail("INVALID_CHANGE_PAYLOAD", path, "Cyclic JSON values are not supported");
|
|
137
|
+
seen.add(value);
|
|
138
|
+
const normalized = Array.isArray(value)
|
|
139
|
+
? array(value, path).map((item, index) => json(item, `${path}[${index}]`, seen))
|
|
140
|
+
: Object.fromEntries(Object.entries(object(value, path)).map(([key, item]) => [key, json(item, `${path}.${key}`, seen)]));
|
|
141
|
+
seen.delete(value);
|
|
142
|
+
return normalized;
|
|
143
|
+
}
|
|
144
|
+
function principalKind(value, path) {
|
|
145
|
+
if (typeof value === "string" && MEMBER_PRINCIPAL_KINDS.includes(value))
|
|
146
|
+
return value;
|
|
147
|
+
return fail("INVALID_CHANGE_PAYLOAD", path, `Expected one of ${MEMBER_PRINCIPAL_KINDS.join(", ")}`);
|
|
148
|
+
}
|
|
149
|
+
function parsePayload(kind, input, path) {
|
|
150
|
+
const value = object(input, path);
|
|
151
|
+
const empty = () => { exactKeys(value, [], [], path); return {}; };
|
|
152
|
+
switch (kind) {
|
|
153
|
+
case "MemberCreate":
|
|
154
|
+
exactKeys(value, ["subjectId", "displayName", "principalKind"], [], path);
|
|
155
|
+
return { subjectId: string(value.subjectId, `${path}.subjectId`), displayName: string(value.displayName, `${path}.displayName`), principalKind: principalKind(value.principalKind, `${path}.principalKind`) };
|
|
156
|
+
case "MemberRevise": {
|
|
157
|
+
if (Object.hasOwn(value, "subjectId"))
|
|
158
|
+
fail("FROZEN_SCOPE_DRIFT", `${path}.subjectId`, "Member subjectId is immutable");
|
|
159
|
+
exactKeys(value, [], ["displayName", "principalKind"], path);
|
|
160
|
+
if (!Object.hasOwn(value, "displayName") && !Object.hasOwn(value, "principalKind"))
|
|
161
|
+
fail("EMPTY_REVISION", path, "Member revision requires at least one field");
|
|
162
|
+
return {
|
|
163
|
+
...(Object.hasOwn(value, "displayName") ? { displayName: string(value.displayName, `${path}.displayName`) } : {}),
|
|
164
|
+
...(Object.hasOwn(value, "principalKind") ? { principalKind: principalKind(value.principalKind, `${path}.principalKind`) } : {}),
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
case "MemberDeactivate": return empty();
|
|
168
|
+
case "HolonCreate":
|
|
169
|
+
exactKeys(value, ["subjectId", "code", "name", "purpose", "boundary"], [], path);
|
|
170
|
+
return { subjectId: string(value.subjectId, `${path}.subjectId`), code: string(value.code, `${path}.code`), name: string(value.name, `${path}.name`), purpose: string(value.purpose, `${path}.purpose`), boundary: string(value.boundary, `${path}.boundary`) };
|
|
171
|
+
case "HolonRevise": {
|
|
172
|
+
for (const key of ["subjectId", "code"])
|
|
173
|
+
if (key in value)
|
|
174
|
+
fail("FROZEN_SCOPE_DRIFT", `${path}.${key}`, `${key} is immutable`);
|
|
175
|
+
exactKeys(value, [], ["name", "purpose", "boundary"], path);
|
|
176
|
+
if (![value.name, value.purpose, value.boundary].some((item) => item !== undefined))
|
|
177
|
+
fail("EMPTY_REVISION", path, "Holon revision requires at least one field");
|
|
178
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, string(item, `${path}.${key}`)]));
|
|
179
|
+
}
|
|
180
|
+
case "HolonDeactivate": return empty();
|
|
181
|
+
case "RoleCreate": {
|
|
182
|
+
const required = ["holonId", "name", "purpose", "domains", "accountabilities", "policies", "capabilityRequirements"];
|
|
183
|
+
exactKeys(value, required, [], path);
|
|
184
|
+
return { holonId: string(value.holonId, `${path}.holonId`), name: string(value.name, `${path}.name`), purpose: string(value.purpose, `${path}.purpose`), domains: strings(value.domains, `${path}.domains`), accountabilities: strings(value.accountabilities, `${path}.accountabilities`), policies: strings(value.policies, `${path}.policies`), capabilityRequirements: strings(value.capabilityRequirements, `${path}.capabilityRequirements`) };
|
|
185
|
+
}
|
|
186
|
+
case "RoleRevise": {
|
|
187
|
+
if ("holonId" in value)
|
|
188
|
+
fail("FROZEN_SCOPE_DRIFT", `${path}.holonId`, "Role holonId is immutable");
|
|
189
|
+
const optional = ["name", "purpose", "domains", "accountabilities", "policies", "capabilityRequirements"];
|
|
190
|
+
exactKeys(value, [], optional, path);
|
|
191
|
+
if (!optional.some((key) => value[key] !== undefined))
|
|
192
|
+
fail("EMPTY_REVISION", path, "Role revision requires at least one field");
|
|
193
|
+
const parsed = {};
|
|
194
|
+
for (const key of optional)
|
|
195
|
+
if (value[key] !== undefined)
|
|
196
|
+
parsed[key] = key === "name" || key === "purpose" ? string(value[key], `${path}.${key}`) : strings(value[key], `${path}.${key}`);
|
|
197
|
+
return parsed;
|
|
198
|
+
}
|
|
199
|
+
case "RoleDeactivate": return empty();
|
|
200
|
+
case "GovernancePolicyCreate":
|
|
201
|
+
exactKeys(value, ["holonId", "name", "profile", "rules"], [], path);
|
|
202
|
+
return { holonId: string(value.holonId, `${path}.holonId`), name: string(value.name, `${path}.name`), profile: string(value.profile, `${path}.profile`), rules: json(value.rules, `${path}.rules`) };
|
|
203
|
+
case "GovernancePolicyRevise": {
|
|
204
|
+
if ("holonId" in value)
|
|
205
|
+
fail("FROZEN_SCOPE_DRIFT", `${path}.holonId`, "GovernancePolicy holonId is immutable");
|
|
206
|
+
exactKeys(value, [], ["name", "profile", "rules"], path);
|
|
207
|
+
if (![value.name, value.profile, value.rules].some((item) => item !== undefined))
|
|
208
|
+
fail("EMPTY_REVISION", path, "GovernancePolicy revision requires at least one field");
|
|
209
|
+
return { ...(value.name === undefined ? {} : { name: string(value.name, `${path}.name`) }), ...(value.profile === undefined ? {} : { profile: string(value.profile, `${path}.profile`) }), ...(value.rules === undefined ? {} : { rules: json(value.rules, `${path}.rules`) }) };
|
|
210
|
+
}
|
|
211
|
+
case "GovernancePolicyDeactivate": return empty();
|
|
212
|
+
case "MembershipEstablish":
|
|
213
|
+
case "MembershipReconfigure": {
|
|
214
|
+
exactKeys(value, ["subjectId", "parentHolonId", "mode"], [], path);
|
|
215
|
+
const mode = value.mode;
|
|
216
|
+
if (mode !== "primary" && mode !== "affiliate")
|
|
217
|
+
fail("INVALID_CHANGE_PAYLOAD", `${path}.mode`, "Expected primary or affiliate");
|
|
218
|
+
return { subjectId: string(value.subjectId, `${path}.subjectId`), parentHolonId: string(value.parentHolonId, `${path}.parentHolonId`), mode: mode };
|
|
219
|
+
}
|
|
220
|
+
case "MembershipEnd": return empty();
|
|
221
|
+
case "RoleAssignmentEstablish":
|
|
222
|
+
case "RoleAssignmentReconfigure":
|
|
223
|
+
exactKeys(value, ["membershipId", "roleId"], [], path);
|
|
224
|
+
return { membershipId: string(value.membershipId, `${path}.membershipId`), roleId: string(value.roleId, `${path}.roleId`) };
|
|
225
|
+
case "RoleAssignmentEnd": return empty();
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
const TRUE_KINDS = new Set(HOLON_CHANGE_KINDS.filter((kind) => !kind.endsWith("Deactivate") && !kind.endsWith("End")));
|
|
229
|
+
export function parseHolarchyChange(input, path) {
|
|
230
|
+
const value = object(input, path);
|
|
231
|
+
exactKeys(value, ["id", "order", "kind", "resourceId", "effectiveState", "payload"], [], path);
|
|
232
|
+
const rawKind = string(value.kind, `${path}.kind`);
|
|
233
|
+
if (!HOLON_CHANGE_KINDS.includes(rawKind))
|
|
234
|
+
fail("UNSUPPORTED_CHANGE", `${path}.kind`, `Unsupported Change kind ${rawKind}`);
|
|
235
|
+
const kind = rawKind;
|
|
236
|
+
const effectiveState = nativeBoolean(value.effectiveState, `${path}.effectiveState`);
|
|
237
|
+
if (effectiveState !== TRUE_KINDS.has(kind))
|
|
238
|
+
fail("INVALID_CHANGE_PAYLOAD", `${path}.effectiveState`, `${kind} requires effectiveState=${TRUE_KINDS.has(kind)}`);
|
|
239
|
+
return {
|
|
240
|
+
id: string(value.id, `${path}.id`), order: integer(value.order, `${path}.order`, 1), kind,
|
|
241
|
+
resourceId: string(value.resourceId, `${path}.resourceId`), effectiveState,
|
|
242
|
+
payload: parsePayload(kind, value.payload, `${path}.payload`),
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
export function parseHolarchyDraft(input, path = "$.invocation.payload.draft") {
|
|
246
|
+
const value = object(input, path);
|
|
247
|
+
exactKeys(value, ["id", "title", "effectiveDate", "actorRef", "changes"], [], path);
|
|
248
|
+
const changesInput = array(value.changes, `${path}.changes`);
|
|
249
|
+
const parsedChanges = changesInput.map((item, index) => parseHolarchyChange(item, `${path}.changes[${index}]`));
|
|
250
|
+
const orders = new Set();
|
|
251
|
+
const ids = new Set();
|
|
252
|
+
for (const [index, change] of parsedChanges.entries()) {
|
|
253
|
+
if (orders.has(change.order))
|
|
254
|
+
fail("DUPLICATE_RESOURCE", `${path}.changes[${index}].order`, `Duplicate Change order ${change.order}`);
|
|
255
|
+
if (ids.has(change.id))
|
|
256
|
+
fail("DUPLICATE_RESOURCE", `${path}.changes[${index}].id`, `Duplicate Change id ${change.id}`);
|
|
257
|
+
orders.add(change.order);
|
|
258
|
+
ids.add(change.id);
|
|
259
|
+
}
|
|
260
|
+
const effectiveDate = string(value.effectiveDate, `${path}.effectiveDate`);
|
|
261
|
+
if (!isCanonicalDay(effectiveDate))
|
|
262
|
+
fail("INVALID_TIMELINE", `${path}.effectiveDate`, "Expected a canonical YYYY-MM-DD date");
|
|
263
|
+
return { id: string(value.id, `${path}.id`), title: string(value.title, `${path}.title`), effectiveDate, actorRef: string(value.actorRef, `${path}.actorRef`), changes: [...parsedChanges].sort((left, right) => left.order - right.order) };
|
|
264
|
+
}
|
|
265
|
+
export function parseAuthorDraftCommand(input, path = "$.invocation.payload") {
|
|
266
|
+
const value = object(input, path);
|
|
267
|
+
const action = string(value.action, `${path}.action`);
|
|
268
|
+
if (action === "Create") {
|
|
269
|
+
exactKeys(value, ["action", "draft"], [], path);
|
|
270
|
+
return { action, draft: parseHolarchyDraft(value.draft, `${path}.draft`) };
|
|
271
|
+
}
|
|
272
|
+
if (action === "Replace") {
|
|
273
|
+
exactKeys(value, ["action", "expectedRevision", "draft"], [], path);
|
|
274
|
+
return { action, expectedRevision: integer(value.expectedRevision, `${path}.expectedRevision`, 1), draft: parseHolarchyDraft(value.draft, `${path}.draft`) };
|
|
275
|
+
}
|
|
276
|
+
if (action === "Delete") {
|
|
277
|
+
exactKeys(value, ["action", "expectedRevision"], [], path);
|
|
278
|
+
return { action, expectedRevision: integer(value.expectedRevision, `${path}.expectedRevision`, 1) };
|
|
279
|
+
}
|
|
280
|
+
return fail("INVALID_AUTHORING_ACTION", `${path}.action`, `Unsupported draft action ${action}`);
|
|
281
|
+
}
|
|
282
|
+
export function sha256Basis(input) {
|
|
283
|
+
return `sha256:${createHash("sha256").update(input).digest("hex")}`;
|
|
284
|
+
}
|
|
285
|
+
function latestAny(rows, key, id, day) {
|
|
286
|
+
return rows.filter((row) => String(row[key]) === id && row.effectiveDate <= day)
|
|
287
|
+
.sort((left, right) => left.effectiveDate.localeCompare(right.effectiveDate) || left.sequence - right.sequence).at(-1);
|
|
288
|
+
}
|
|
289
|
+
function cloneJson(value) { return JSON.parse(JSON.stringify(value)); }
|
|
290
|
+
function eventMetadata(row) {
|
|
291
|
+
return { versionId: row.id, effectiveDate: row.effectiveDate, effectiveState: true, createdAt: row.createdAt, createdBy: row.createdBy, changeSetId: row.changeSetId, sequence: row.sequence };
|
|
292
|
+
}
|
|
293
|
+
function parseArray(value) { const parsed = JSON.parse(value); return Array.isArray(parsed) ? parsed.filter((item) => typeof item === "string") : []; }
|
|
294
|
+
export function projectHolarchySnapshot(facts, effectiveDate) {
|
|
295
|
+
const subjectTypes = new Map(facts.subjects.map(({ id, subjectType }) => [id, subjectType]));
|
|
296
|
+
const members = facts.members.flatMap((identity) => { const version = latestAny(facts.memberVersions, "memberId", identity.id, effectiveDate); return version?.effectiveState === true ? [{ id: identity.id, subjectId: identity.subjectId, displayName: version.displayName, principalKind: version.principalKind, ...eventMetadata(version) }] : []; });
|
|
297
|
+
const holons = facts.holons.flatMap((identity) => { const version = latestAny(facts.holonVersions, "holonId", identity.id, effectiveDate); return version?.effectiveState === true ? [{ id: identity.id, subjectId: identity.subjectId, code: identity.code ?? identity.id, name: version.name, purpose: version.purpose, boundary: version.boundary, ...eventMetadata(version) }] : []; });
|
|
298
|
+
const memberships = facts.memberships.flatMap((identity) => { const version = latestAny(facts.membershipVersions, "membershipId", identity.id, effectiveDate); return version?.effectiveState === true ? [{ id: identity.id, subjectId: version.subjectId, subjectType: subjectTypes.get(version.subjectId) ?? "member", parentHolonId: version.parentHolonId, mode: version.mode, ...eventMetadata(version) }] : []; });
|
|
299
|
+
const roles = facts.roles.flatMap((identity) => { const version = latestAny(facts.roleVersions, "roleId", identity.id, effectiveDate); return version?.effectiveState === true ? [{ id: identity.id, holonId: identity.holonId, name: version.name, purpose: version.purpose, domains: parseArray(version.domainsJson), accountabilities: parseArray(version.accountabilitiesJson), policies: parseArray(version.policiesJson), capabilityRequirements: parseArray(version.capabilityRequirementsJson), ...eventMetadata(version) }] : []; });
|
|
300
|
+
const roleAssignments = facts.roleAssignments.flatMap((identity) => { const version = latestAny(facts.roleAssignmentVersions, "roleAssignmentId", identity.id, effectiveDate); return version?.effectiveState === true ? [{ id: identity.id, membershipId: version.membershipId, roleId: version.roleId, ...eventMetadata(version) }] : []; });
|
|
301
|
+
const governancePolicies = facts.governancePolicies.flatMap((identity) => { const version = latestAny(facts.governancePolicyVersions, "governancePolicyId", identity.id, effectiveDate); return version?.effectiveState === true ? [{ id: identity.id, holonId: identity.holonId, name: version.name, profile: version.profile, rules: JSON.parse(version.rulesJson), ...eventMetadata(version) }] : []; });
|
|
302
|
+
const byId = (rows) => [...rows].sort((left, right) => left.id.localeCompare(right.id));
|
|
303
|
+
return { effectiveDate, members: byId(members), holons: byId(holons), memberships: byId(memberships), roles: byId(roles), roleAssignments: byId(roleAssignments), governancePolicies: byId(governancePolicies) };
|
|
304
|
+
}
|
|
305
|
+
function resourceRows(snapshot) {
|
|
306
|
+
const value = (row) => cloneJson(row);
|
|
307
|
+
return [
|
|
308
|
+
...snapshot.members.map((row) => ["Member", row.id, value(row)]),
|
|
309
|
+
...snapshot.holons.map((row) => ["Holon", row.id, value(row)]),
|
|
310
|
+
...snapshot.roles.map((row) => ["Role", row.id, value(row)]),
|
|
311
|
+
...snapshot.governancePolicies.map((row) => ["GovernancePolicy", row.id, value(row)]),
|
|
312
|
+
...snapshot.memberships.map((row) => ["Membership", row.id, value(row)]),
|
|
313
|
+
...snapshot.roleAssignments.map((row) => ["RoleAssignment", row.id, value(row)]),
|
|
314
|
+
];
|
|
315
|
+
}
|
|
316
|
+
function inactiveKeys(facts, day) {
|
|
317
|
+
const result = new Set();
|
|
318
|
+
const collect = (type, rows, key) => {
|
|
319
|
+
for (const id of new Set(rows.map((row) => String(row[key]))))
|
|
320
|
+
if (latestAny(rows, key, id, day)?.effectiveState === false)
|
|
321
|
+
result.add(`${type}:${id}`);
|
|
322
|
+
};
|
|
323
|
+
collect("Member", facts.memberVersions, "memberId");
|
|
324
|
+
collect("Holon", facts.holonVersions, "holonId");
|
|
325
|
+
collect("Membership", facts.membershipVersions, "membershipId");
|
|
326
|
+
collect("Role", facts.roleVersions, "roleId");
|
|
327
|
+
collect("RoleAssignment", facts.roleAssignmentVersions, "roleAssignmentId");
|
|
328
|
+
collect("GovernancePolicy", facts.governancePolicyVersions, "governancePolicyId");
|
|
329
|
+
return result;
|
|
330
|
+
}
|
|
331
|
+
function aggregateDiff(before, after, factsBefore) {
|
|
332
|
+
const beforeMap = new Map(resourceRows(before).map(([type, id, value]) => [`${type}:${id}`, { type, id, value }]));
|
|
333
|
+
const afterMap = new Map(resourceRows(after).map(([type, id, value]) => [`${type}:${id}`, { type, id, value }]));
|
|
334
|
+
const inactive = inactiveKeys(factsBefore, before.effectiveDate);
|
|
335
|
+
const items = [];
|
|
336
|
+
for (const key of [...new Set([...beforeMap.keys(), ...afterMap.keys()])].sort()) {
|
|
337
|
+
const prior = beforeMap.get(key), next = afterMap.get(key);
|
|
338
|
+
if (prior === undefined && next !== undefined)
|
|
339
|
+
items.push({ resourceType: next.type, resourceId: next.id, kind: inactive.has(key) ? "Reactivated" : "Added", after: next.value });
|
|
340
|
+
else if (prior !== undefined && next === undefined)
|
|
341
|
+
items.push({ resourceType: prior.type, resourceId: prior.id, kind: "Deactivated", before: prior.value });
|
|
342
|
+
else if (prior !== undefined && next !== undefined && JSON.stringify(prior.value) !== JSON.stringify(next.value))
|
|
343
|
+
items.push({ resourceType: prior.type, resourceId: prior.id, kind: "Changed", before: prior.value, after: next.value });
|
|
344
|
+
}
|
|
345
|
+
return { items, added: items.filter(({ kind }) => kind === "Added").length, changed: items.filter(({ kind }) => kind === "Changed").length, deactivated: items.filter(({ kind }) => kind === "Deactivated").length, reactivated: items.filter(({ kind }) => kind === "Reactivated").length };
|
|
346
|
+
}
|
|
347
|
+
function frontier(facts) {
|
|
348
|
+
const maximum = (rows) => Math.max(0, ...rows.map(({ sequence }) => sequence));
|
|
349
|
+
return { MemberVersion: maximum(facts.memberVersions), HolonVersion: maximum(facts.holonVersions), HolonMembershipVersion: maximum(facts.membershipVersions), RoleVersion: maximum(facts.roleVersions), RoleAssignmentVersion: maximum(facts.roleAssignmentVersions), GovernancePolicyVersion: maximum(facts.governancePolicyVersions) };
|
|
350
|
+
}
|
|
351
|
+
export function previewBasis(facts, changeSet) {
|
|
352
|
+
const authoritySequence = frontier(facts);
|
|
353
|
+
const canonical = JSON.stringify({ schema: "HolonPreviewBasis@1", changeSetId: changeSet.id, changeSetRevision: changeSet.revision, effectiveDate: changeSet.effectiveDate, authoritySequence });
|
|
354
|
+
return { schema: "HolonPreviewBasis@1", algorithm: "SHA-256", token: sha256Basis(canonical), changeSetId: changeSet.id, changeSetRevision: changeSet.revision, effectiveDate: changeSet.effectiveDate, authoritySequence };
|
|
355
|
+
}
|
|
356
|
+
export function planHolarchyChangeSet(base, changeSet, executionInstant) {
|
|
357
|
+
let facts = cloneJson(base);
|
|
358
|
+
let sequence = Math.max(0, ...Object.values(frontier(base)));
|
|
359
|
+
const steps = [];
|
|
360
|
+
const diagnostics = [];
|
|
361
|
+
const addDiagnostic = (code, path, message) => diagnostics.push({ code, path, message });
|
|
362
|
+
const hasIdentity = (kind, id) => ({ Member: facts.members, Holon: facts.holons, Membership: facts.memberships, Role: facts.roles, RoleAssignment: facts.roleAssignments, GovernancePolicy: facts.governancePolicies }[kind]).some((row) => row.id === id);
|
|
363
|
+
const metadata = (change, path) => ({ id: `${change.id}:preview`, effectiveDate: changeSet.effectiveDate, effectiveState: change.effectiveState, createdAt: executionInstant, createdBy: changeSet.actorRef, changeSetId: changeSet.id, sequence: ++sequence, path });
|
|
364
|
+
const appendStep = (change, identities, version) => steps.push(Object.freeze({ change, identities: Object.freeze([...identities]), version: Object.freeze({ ...version, payload: Object.freeze({ ...version.payload }) }) }));
|
|
365
|
+
for (const [index, change] of [...changeSet.changes].sort((left, right) => left.order - right.order).entries()) {
|
|
366
|
+
const path = `$.changeSet.changes[${index}]`, identities = [];
|
|
367
|
+
const missing = (type) => { addDiagnostic("MISSING_IDENTITY", `${path}.resourceId`, `${type} ${change.resourceId} does not exist`); };
|
|
368
|
+
const duplicate = (type) => { addDiagnostic("DUPLICATE_RESOURCE", `${path}.resourceId`, `${type} ${change.resourceId} already exists`); };
|
|
369
|
+
switch (change.kind) {
|
|
370
|
+
case "MemberCreate": {
|
|
371
|
+
if (hasIdentity("Member", change.resourceId) || facts.subjects.some(({ id }) => id === change.payload.subjectId)) {
|
|
372
|
+
duplicate("Member");
|
|
373
|
+
break;
|
|
374
|
+
}
|
|
375
|
+
identities.push({ entity: "OrganizationalSubject", values: { id: change.payload.subjectId, subjectType: "member" } }, { entity: "Member", values: { id: change.resourceId, subjectId: change.payload.subjectId } });
|
|
376
|
+
const payload = { displayName: change.payload.displayName, principalKind: change.payload.principalKind };
|
|
377
|
+
facts = { ...facts, subjects: [...facts.subjects, { id: change.payload.subjectId, subjectType: "member" }], members: [...facts.members, { id: change.resourceId, subjectId: change.payload.subjectId }], memberVersions: [...facts.memberVersions, { ...metadata(change, path), memberId: change.resourceId, ...payload }] };
|
|
378
|
+
appendStep(change, identities, { entity: "MemberVersion", logicalId: change.resourceId, logicalIdKey: "memberId", effectiveState: change.effectiveState, payload });
|
|
379
|
+
break;
|
|
380
|
+
}
|
|
381
|
+
case "MemberRevise":
|
|
382
|
+
case "MemberDeactivate": {
|
|
383
|
+
const current = latestAny(facts.memberVersions, "memberId", change.resourceId, changeSet.effectiveDate);
|
|
384
|
+
if (!hasIdentity("Member", change.resourceId) || current === undefined) {
|
|
385
|
+
missing("Member");
|
|
386
|
+
break;
|
|
387
|
+
}
|
|
388
|
+
const payload = change.kind === "MemberRevise"
|
|
389
|
+
? { displayName: change.payload.displayName ?? current.displayName, principalKind: change.payload.principalKind ?? current.principalKind }
|
|
390
|
+
: { displayName: current.displayName, principalKind: current.principalKind };
|
|
391
|
+
facts = { ...facts, memberVersions: [...facts.memberVersions, { ...metadata(change, path), memberId: change.resourceId, ...payload }] };
|
|
392
|
+
appendStep(change, identities, { entity: "MemberVersion", logicalId: change.resourceId, logicalIdKey: "memberId", effectiveState: change.effectiveState, payload });
|
|
393
|
+
break;
|
|
394
|
+
}
|
|
395
|
+
case "HolonCreate": {
|
|
396
|
+
if (hasIdentity("Holon", change.resourceId) || facts.subjects.some(({ id }) => id === change.payload.subjectId)) {
|
|
397
|
+
duplicate("Holon");
|
|
398
|
+
break;
|
|
399
|
+
}
|
|
400
|
+
identities.push({ entity: "OrganizationalSubject", values: { id: change.payload.subjectId, subjectType: "holon" } }, { entity: "Holon", values: { id: change.resourceId, subjectId: change.payload.subjectId, code: change.payload.code } });
|
|
401
|
+
facts = { ...facts, subjects: [...facts.subjects, { id: change.payload.subjectId, subjectType: "holon" }], holons: [...facts.holons, { id: change.resourceId, subjectId: change.payload.subjectId, code: change.payload.code }], holonVersions: [...facts.holonVersions, { ...metadata(change, path), holonId: change.resourceId, name: change.payload.name, purpose: change.payload.purpose, boundary: change.payload.boundary }] };
|
|
402
|
+
appendStep(change, identities, { entity: "HolonVersion", logicalId: change.resourceId, logicalIdKey: "holonId", effectiveState: change.effectiveState, payload: { name: change.payload.name, purpose: change.payload.purpose, boundary: change.payload.boundary } });
|
|
403
|
+
break;
|
|
404
|
+
}
|
|
405
|
+
case "HolonRevise":
|
|
406
|
+
case "HolonDeactivate": {
|
|
407
|
+
const current = latestAny(facts.holonVersions, "holonId", change.resourceId, changeSet.effectiveDate);
|
|
408
|
+
if (!hasIdentity("Holon", change.resourceId) || current === undefined) {
|
|
409
|
+
missing("Holon");
|
|
410
|
+
break;
|
|
411
|
+
}
|
|
412
|
+
const payload = change.kind === "HolonRevise" ? { name: change.payload.name ?? current.name, purpose: change.payload.purpose ?? current.purpose, boundary: change.payload.boundary ?? current.boundary } : { name: current.name, purpose: current.purpose, boundary: current.boundary };
|
|
413
|
+
facts = { ...facts, holonVersions: [...facts.holonVersions, { ...metadata(change, path), holonId: change.resourceId, ...payload }] };
|
|
414
|
+
appendStep(change, identities, { entity: "HolonVersion", logicalId: change.resourceId, logicalIdKey: "holonId", effectiveState: change.effectiveState, payload });
|
|
415
|
+
break;
|
|
416
|
+
}
|
|
417
|
+
case "RoleCreate": {
|
|
418
|
+
if (hasIdentity("Role", change.resourceId)) {
|
|
419
|
+
duplicate("Role");
|
|
420
|
+
break;
|
|
421
|
+
}
|
|
422
|
+
identities.push({ entity: "Role", values: { id: change.resourceId, holonId: change.payload.holonId } });
|
|
423
|
+
const payload = { name: change.payload.name, purpose: change.payload.purpose, domainsJson: JSON.stringify(change.payload.domains), accountabilitiesJson: JSON.stringify(change.payload.accountabilities), policiesJson: JSON.stringify(change.payload.policies), capabilityRequirementsJson: JSON.stringify(change.payload.capabilityRequirements) };
|
|
424
|
+
facts = { ...facts, roles: [...facts.roles, { id: change.resourceId, holonId: change.payload.holonId }], roleVersions: [...facts.roleVersions, { ...metadata(change, path), roleId: change.resourceId, ...payload }] };
|
|
425
|
+
appendStep(change, identities, { entity: "RoleVersion", logicalId: change.resourceId, logicalIdKey: "roleId", effectiveState: change.effectiveState, payload });
|
|
426
|
+
break;
|
|
427
|
+
}
|
|
428
|
+
case "RoleRevise":
|
|
429
|
+
case "RoleDeactivate": {
|
|
430
|
+
const current = latestAny(facts.roleVersions, "roleId", change.resourceId, changeSet.effectiveDate);
|
|
431
|
+
if (!hasIdentity("Role", change.resourceId) || current === undefined) {
|
|
432
|
+
missing("Role");
|
|
433
|
+
break;
|
|
434
|
+
}
|
|
435
|
+
const revision = change.kind === "RoleRevise" ? change.payload : {};
|
|
436
|
+
const payload = { name: revision.name ?? current.name, purpose: revision.purpose ?? current.purpose, domainsJson: revision.domains === undefined ? current.domainsJson : JSON.stringify(revision.domains), accountabilitiesJson: revision.accountabilities === undefined ? current.accountabilitiesJson : JSON.stringify(revision.accountabilities), policiesJson: revision.policies === undefined ? current.policiesJson : JSON.stringify(revision.policies), capabilityRequirementsJson: revision.capabilityRequirements === undefined ? current.capabilityRequirementsJson : JSON.stringify(revision.capabilityRequirements) };
|
|
437
|
+
facts = { ...facts, roleVersions: [...facts.roleVersions, { ...metadata(change, path), roleId: change.resourceId, ...payload }] };
|
|
438
|
+
appendStep(change, identities, { entity: "RoleVersion", logicalId: change.resourceId, logicalIdKey: "roleId", effectiveState: change.effectiveState, payload });
|
|
439
|
+
break;
|
|
440
|
+
}
|
|
441
|
+
case "GovernancePolicyCreate": {
|
|
442
|
+
if (hasIdentity("GovernancePolicy", change.resourceId)) {
|
|
443
|
+
duplicate("GovernancePolicy");
|
|
444
|
+
break;
|
|
445
|
+
}
|
|
446
|
+
identities.push({ entity: "GovernancePolicy", values: { id: change.resourceId, holonId: change.payload.holonId } });
|
|
447
|
+
const payload = { holonId: change.payload.holonId, name: change.payload.name, profile: change.payload.profile, rulesJson: JSON.stringify(change.payload.rules) };
|
|
448
|
+
facts = { ...facts, governancePolicies: [...facts.governancePolicies, { id: change.resourceId, holonId: change.payload.holonId }], governancePolicyVersions: [...facts.governancePolicyVersions, { ...metadata(change, path), governancePolicyId: change.resourceId, ...payload }] };
|
|
449
|
+
appendStep(change, identities, { entity: "GovernancePolicyVersion", logicalId: change.resourceId, logicalIdKey: "governancePolicyId", effectiveState: change.effectiveState, payload });
|
|
450
|
+
break;
|
|
451
|
+
}
|
|
452
|
+
case "GovernancePolicyRevise":
|
|
453
|
+
case "GovernancePolicyDeactivate": {
|
|
454
|
+
const current = latestAny(facts.governancePolicyVersions, "governancePolicyId", change.resourceId, changeSet.effectiveDate);
|
|
455
|
+
if (!hasIdentity("GovernancePolicy", change.resourceId) || current === undefined) {
|
|
456
|
+
missing("GovernancePolicy");
|
|
457
|
+
break;
|
|
458
|
+
}
|
|
459
|
+
const revision = change.kind === "GovernancePolicyRevise" ? change.payload : {};
|
|
460
|
+
const payload = { holonId: current.holonId, name: revision.name ?? current.name, profile: revision.profile ?? current.profile, rulesJson: revision.rules === undefined ? current.rulesJson : JSON.stringify(revision.rules) };
|
|
461
|
+
facts = { ...facts, governancePolicyVersions: [...facts.governancePolicyVersions, { ...metadata(change, path), governancePolicyId: change.resourceId, ...payload }] };
|
|
462
|
+
appendStep(change, identities, { entity: "GovernancePolicyVersion", logicalId: change.resourceId, logicalIdKey: "governancePolicyId", effectiveState: change.effectiveState, payload });
|
|
463
|
+
break;
|
|
464
|
+
}
|
|
465
|
+
case "MembershipEstablish": {
|
|
466
|
+
if (hasIdentity("Membership", change.resourceId)) {
|
|
467
|
+
duplicate("Membership");
|
|
468
|
+
break;
|
|
469
|
+
}
|
|
470
|
+
identities.push({ entity: "HolonMembership", values: { id: change.resourceId } });
|
|
471
|
+
const payload = { subjectId: change.payload.subjectId, parentHolonId: change.payload.parentHolonId, mode: change.payload.mode };
|
|
472
|
+
facts = { ...facts, memberships: [...facts.memberships, { id: change.resourceId }], membershipVersions: [...facts.membershipVersions, { ...metadata(change, path), membershipId: change.resourceId, ...payload }] };
|
|
473
|
+
appendStep(change, identities, { entity: "HolonMembershipVersion", logicalId: change.resourceId, logicalIdKey: "membershipId", effectiveState: change.effectiveState, payload });
|
|
474
|
+
break;
|
|
475
|
+
}
|
|
476
|
+
case "MembershipReconfigure":
|
|
477
|
+
case "MembershipEnd": {
|
|
478
|
+
const current = latestAny(facts.membershipVersions, "membershipId", change.resourceId, changeSet.effectiveDate);
|
|
479
|
+
if (!hasIdentity("Membership", change.resourceId) || current === undefined) {
|
|
480
|
+
missing("Membership");
|
|
481
|
+
break;
|
|
482
|
+
}
|
|
483
|
+
if (change.kind === "MembershipReconfigure" && change.payload.subjectId !== current.subjectId) {
|
|
484
|
+
addDiagnostic("FROZEN_SCOPE_DRIFT", `${path}.payload.subjectId`, "Membership subjectId is immutable");
|
|
485
|
+
break;
|
|
486
|
+
}
|
|
487
|
+
const payload = change.kind === "MembershipReconfigure" ? { subjectId: change.payload.subjectId, parentHolonId: change.payload.parentHolonId, mode: change.payload.mode } : { subjectId: current.subjectId, parentHolonId: current.parentHolonId, mode: current.mode };
|
|
488
|
+
facts = { ...facts, membershipVersions: [...facts.membershipVersions, { ...metadata(change, path), membershipId: change.resourceId, ...payload }] };
|
|
489
|
+
appendStep(change, identities, { entity: "HolonMembershipVersion", logicalId: change.resourceId, logicalIdKey: "membershipId", effectiveState: change.effectiveState, payload });
|
|
490
|
+
break;
|
|
491
|
+
}
|
|
492
|
+
case "RoleAssignmentEstablish": {
|
|
493
|
+
if (hasIdentity("RoleAssignment", change.resourceId)) {
|
|
494
|
+
duplicate("RoleAssignment");
|
|
495
|
+
break;
|
|
496
|
+
}
|
|
497
|
+
identities.push({ entity: "RoleAssignment", values: { id: change.resourceId } });
|
|
498
|
+
const payload = { membershipId: change.payload.membershipId, roleId: change.payload.roleId };
|
|
499
|
+
facts = { ...facts, roleAssignments: [...facts.roleAssignments, { id: change.resourceId }], roleAssignmentVersions: [...facts.roleAssignmentVersions, { ...metadata(change, path), roleAssignmentId: change.resourceId, ...payload }] };
|
|
500
|
+
appendStep(change, identities, { entity: "RoleAssignmentVersion", logicalId: change.resourceId, logicalIdKey: "roleAssignmentId", effectiveState: change.effectiveState, payload });
|
|
501
|
+
break;
|
|
502
|
+
}
|
|
503
|
+
case "RoleAssignmentReconfigure":
|
|
504
|
+
case "RoleAssignmentEnd": {
|
|
505
|
+
const current = latestAny(facts.roleAssignmentVersions, "roleAssignmentId", change.resourceId, changeSet.effectiveDate);
|
|
506
|
+
if (!hasIdentity("RoleAssignment", change.resourceId) || current === undefined) {
|
|
507
|
+
missing("RoleAssignment");
|
|
508
|
+
break;
|
|
509
|
+
}
|
|
510
|
+
if (change.kind === "RoleAssignmentReconfigure" && change.payload.membershipId === current.membershipId && change.payload.roleId === current.roleId) {
|
|
511
|
+
addDiagnostic("EMPTY_REVISION", `${path}.payload`, "RoleAssignment reconfiguration must change membershipId or roleId");
|
|
512
|
+
break;
|
|
513
|
+
}
|
|
514
|
+
const payload = change.kind === "RoleAssignmentReconfigure" ? { membershipId: change.payload.membershipId, roleId: change.payload.roleId } : { membershipId: current.membershipId, roleId: current.roleId };
|
|
515
|
+
facts = { ...facts, roleAssignmentVersions: [...facts.roleAssignmentVersions, { ...metadata(change, path), roleAssignmentId: change.resourceId, ...payload }] };
|
|
516
|
+
appendStep(change, identities, { entity: "RoleAssignmentVersion", logicalId: change.resourceId, logicalIdKey: "roleAssignmentId", effectiveState: change.effectiveState, payload });
|
|
517
|
+
break;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
const before = projectHolarchySnapshot(base, changeSet.effectiveDate), after = projectHolarchySnapshot(facts, changeSet.effectiveDate);
|
|
522
|
+
const afterMembershipIds = new Set(after.memberships.map(({ id }) => id)), afterRoleIds = new Set(after.roles.map(({ id }) => id)), afterAssignmentIds = new Set(after.roleAssignments.map(({ id }) => id));
|
|
523
|
+
const explicitlyHandledAssignments = new Set(steps
|
|
524
|
+
.filter(({ change }) => change.kind === "RoleAssignmentEnd" || change.kind === "RoleAssignmentReconfigure")
|
|
525
|
+
.map(({ change }) => change.resourceId));
|
|
526
|
+
const impacts = [];
|
|
527
|
+
const impact = (change, index, resourceType, resourceId, resolved, message) => {
|
|
528
|
+
if (impacts.some((item) => item.changeId === change.id && item.resourceType === resourceType && item.resourceId === resourceId))
|
|
529
|
+
return;
|
|
530
|
+
impacts.push({ changeId: change.id, resourceType, resourceId, resolution: resolved ? "ResolvedInBatch" : "BlocksApply", path: `$.changeSet.changes[${index}]`, message });
|
|
531
|
+
if (!resolved)
|
|
532
|
+
addDiagnostic("DEPENDENCY_BLOCKS_APPLY", `$.changeSet.changes[${index}]`, message);
|
|
533
|
+
};
|
|
534
|
+
for (const [index, change] of [...changeSet.changes].sort((left, right) => left.order - right.order).entries()) {
|
|
535
|
+
if (change.kind === "MemberDeactivate") {
|
|
536
|
+
const subjectId = base.members.find(({ id }) => id === change.resourceId)?.subjectId;
|
|
537
|
+
for (const membership of before.memberships.filter((item) => item.subjectId === subjectId)) {
|
|
538
|
+
impact(change, index, "Membership", membership.id, !afterMembershipIds.has(membership.id), `Membership ${membership.id} depends on Member ${change.resourceId}`);
|
|
539
|
+
for (const assignment of before.roleAssignments.filter((item) => item.membershipId === membership.id))
|
|
540
|
+
impact(change, index, "RoleAssignment", assignment.id, !afterAssignmentIds.has(assignment.id), `RoleAssignment ${assignment.id} depends on Member ${change.resourceId}`);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
else if (change.kind === "HolonDeactivate") {
|
|
544
|
+
const subjectId = base.holons.find(({ id }) => id === change.resourceId)?.subjectId;
|
|
545
|
+
const affectedMemberships = before.memberships.filter((item) => item.parentHolonId === change.resourceId || item.subjectId === subjectId);
|
|
546
|
+
const affectedRoles = before.roles.filter((item) => item.holonId === change.resourceId);
|
|
547
|
+
for (const membership of affectedMemberships)
|
|
548
|
+
impact(change, index, "Membership", membership.id, !afterMembershipIds.has(membership.id), `Membership ${membership.id} depends on Holon ${change.resourceId}`);
|
|
549
|
+
for (const role of affectedRoles)
|
|
550
|
+
impact(change, index, "Role", role.id, !afterRoleIds.has(role.id), `Role ${role.id} depends on Holon ${change.resourceId}`);
|
|
551
|
+
const membershipIds = new Set(affectedMemberships.map(({ id }) => id)), roleIds = new Set(affectedRoles.map(({ id }) => id));
|
|
552
|
+
for (const assignment of before.roleAssignments.filter((item) => membershipIds.has(item.membershipId) || roleIds.has(item.roleId)))
|
|
553
|
+
impact(change, index, "RoleAssignment", assignment.id, !afterAssignmentIds.has(assignment.id), `RoleAssignment ${assignment.id} depends on Holon ${change.resourceId}`);
|
|
554
|
+
for (const policy of before.governancePolicies.filter((item) => item.holonId === change.resourceId))
|
|
555
|
+
impact(change, index, "GovernancePolicy", policy.id, !after.governancePolicies.some(({ id }) => id === policy.id), `GovernancePolicy ${policy.id} depends on Holon ${change.resourceId}`);
|
|
556
|
+
}
|
|
557
|
+
else if (change.kind === "RoleDeactivate") {
|
|
558
|
+
for (const assignment of before.roleAssignments.filter((item) => item.roleId === change.resourceId))
|
|
559
|
+
impact(change, index, "RoleAssignment", assignment.id, !afterAssignmentIds.has(assignment.id), `RoleAssignment ${assignment.id} depends on Role ${change.resourceId}`);
|
|
560
|
+
}
|
|
561
|
+
else if (change.kind === "MembershipEnd" || change.kind === "MembershipReconfigure") {
|
|
562
|
+
for (const assignment of before.roleAssignments.filter((item) => item.membershipId === change.resourceId)) {
|
|
563
|
+
const afterAssignment = after.roleAssignments.find(({ id }) => id === assignment.id);
|
|
564
|
+
const movedOrEnded = afterAssignment === undefined || afterAssignment.membershipId !== change.resourceId;
|
|
565
|
+
const afterMembership = after.memberships.find(({ id }) => id === change.resourceId);
|
|
566
|
+
const afterRole = afterAssignment === undefined ? undefined : after.roles.find(({ id }) => id === afterAssignment.roleId);
|
|
567
|
+
const remainsValidAfterReconfigure = change.kind === "MembershipReconfigure"
|
|
568
|
+
&& afterMembership !== undefined
|
|
569
|
+
&& afterRole !== undefined
|
|
570
|
+
&& afterMembership.parentHolonId === afterRole.holonId;
|
|
571
|
+
const resolved = explicitlyHandledAssignments.has(assignment.id) && (movedOrEnded || remainsValidAfterReconfigure);
|
|
572
|
+
impact(change, index, "RoleAssignment", assignment.id, resolved, `RoleAssignment ${assignment.id} depends on Membership ${change.resourceId}`);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
diagnostics.push(...validateDesiredHolarchy(facts, changeSet.effectiveDate));
|
|
577
|
+
const uniqueDiagnostics = diagnostics.filter((item, index) => diagnostics.findIndex((candidate) => candidate.code === item.code && candidate.path === item.path && candidate.message === item.message) === index);
|
|
578
|
+
return Object.freeze({ steps: Object.freeze(steps), facts, before, after, aggregateDiff: aggregateDiff(before, after, base), dependencyImpacts: Object.freeze(impacts), diagnostics: Object.freeze(uniqueDiagnostics), basis: previewBasis(base, changeSet), valid: uniqueDiagnostics.length === 0 });
|
|
579
|
+
}
|
|
580
|
+
//# sourceMappingURL=authoring.js.map
|