@elevasis/core 0.38.0 → 0.40.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.
@@ -4383,6 +4383,7 @@ type SidebarNode = SidebarSurfaceNode | SidebarGroupNode;
4383
4383
 
4384
4384
  declare const OrganizationModelSchema: z.ZodObject<{
4385
4385
  version: z.ZodDefault<z.ZodLiteral<1>>;
4386
+ snapshotHash: z.ZodOptional<z.ZodString>;
4386
4387
  domainMetadata: z.ZodPipe<z.ZodDefault<z.ZodObject<{
4387
4388
  branding: z.ZodOptional<z.ZodObject<{
4388
4389
  version: z.ZodDefault<z.ZodLiteral<1>>;
package/dist/index.d.ts CHANGED
@@ -4223,6 +4223,7 @@ declare const OrganizationModelDomainMetadataByDomainSchema: z.ZodPipe<z.ZodDefa
4223
4223
  }>>;
4224
4224
  declare const OrganizationModelSchema: z.ZodObject<{
4225
4225
  version: z.ZodDefault<z.ZodLiteral<1>>;
4226
+ snapshotHash: z.ZodOptional<z.ZodString>;
4226
4227
  domainMetadata: z.ZodPipe<z.ZodDefault<z.ZodObject<{
4227
4228
  branding: z.ZodOptional<z.ZodObject<{
4228
4229
  version: z.ZodDefault<z.ZodLiteral<1>>;
package/dist/index.js CHANGED
@@ -2464,6 +2464,18 @@ var OrganizationModelDomainMetadataByDomainSchema = z.object({
2464
2464
  }).partial().default(DEFAULT_ORGANIZATION_MODEL_DOMAIN_METADATA).transform((metadata) => ({ ...DEFAULT_ORGANIZATION_MODEL_DOMAIN_METADATA, ...metadata }));
2465
2465
  var OrganizationModelSchemaBase = z.object({
2466
2466
  version: z.literal(1).default(1),
2467
+ /**
2468
+ * Deterministic SHA-256 hex hash of the full resolved model, excluding this
2469
+ * field itself and volatile domainMetadata.lastModified values.
2470
+ *
2471
+ * Stamped at deploy time by the platform OM assembly and persisted alongside
2472
+ * the snapshot in the DB. Compared at API boot to detect stale deployed
2473
+ * snapshots (primary gate: deploy; secondary backstop: boot).
2474
+ *
2475
+ * Optional — absent on models that predate Step 2 versioning or that have
2476
+ * not been stamped (e.g. tenant partial overrides before re-deploy).
2477
+ */
2478
+ snapshotHash: z.string().optional(),
2467
2479
  domainMetadata: OrganizationModelDomainMetadataByDomainSchema,
2468
2480
  branding: OrganizationModelBrandingSchema.default(DEFAULT_ORGANIZATION_MODEL_BRANDING),
2469
2481
  navigation: OrganizationModelNavigationSchema,
@@ -374,6 +374,7 @@ type OrganizationModelIconToken = z.infer<typeof OrganizationModelIconTokenSchem
374
374
 
375
375
  declare const OrganizationModelSchema: z.ZodObject<{
376
376
  version: z.ZodDefault<z.ZodLiteral<1>>;
377
+ snapshotHash: z.ZodOptional<z.ZodString>;
377
378
  domainMetadata: z.ZodPipe<z.ZodDefault<z.ZodObject<{
378
379
  branding: z.ZodOptional<z.ZodObject<{
379
380
  version: z.ZodDefault<z.ZodLiteral<1>>;
@@ -1467,19 +1468,22 @@ declare function governs(graph: OrganizationGraph, nodeId: string): string[];
1467
1468
  */
1468
1469
  declare function governedBy(graph: OrganizationGraph, nodeId: string): string[];
1469
1470
  /** The recognized mount axes for Knowledge Map paths. */
1470
- type KnowledgeMount = 'by-system' | 'by-ontology' | 'by-kind' | 'by-owner' | 'graph' | 'node';
1471
+ type KnowledgeMount = 'by-system' | 'by-ontology' | 'by-kind' | 'by-owner' | 'graph' | 'node' | 'all-systems' | 'all-resources' | 'all-roles';
1471
1472
  /**
1472
1473
  * The result of parsing a Knowledge Map path string.
1473
1474
  *
1474
1475
  * Shape: `{ mount: KnowledgeMount, args: string[] }`
1475
1476
  *
1476
1477
  * Per-mount arg arrays:
1477
- * - `by-system`: `[systemId]` (e.g. `['sales.crm']`)
1478
- * - `by-ontology`:`[ontologyId]` (e.g. `['sales.crm:object/deal']`)
1479
- * - `by-kind`: `[kind]` (e.g. `['playbook']`)
1480
- * - `by-owner`: `[ownerId]` (e.g. `['role.ops-lead']`)
1481
- * - `graph`: `[nodeId, verb]` where verb is `'governs'` or `'governed-by'`
1482
- * - `node`: `[nodeId]` (single node lookup, no sub-path)
1478
+ * - `by-system`: `[systemId]` (e.g. `['sales.crm']`)
1479
+ * - `by-ontology`: `[ontologyId]` (e.g. `['sales.crm:object/deal']`)
1480
+ * - `by-kind`: `[kind]` (e.g. `['playbook']`)
1481
+ * - `by-owner`: `[ownerId]` (e.g. `['role.ops-lead']`)
1482
+ * - `graph`: `[nodeId, verb]` where verb is `'governs'` or `'governed-by'`
1483
+ * - `node`: `[nodeId]` (single node lookup, no sub-path)
1484
+ * - `all-systems`: `[]` (no args — returns all systems flattened)
1485
+ * - `all-resources`:`[]` (no args — returns all resources)
1486
+ * - `all-roles`: `[]` (no args — returns all roles)
1483
1487
  */
1484
1488
  interface ParsedKnowledgePath {
1485
1489
  mount: KnowledgeMount;
@@ -1489,13 +1493,16 @@ interface ParsedKnowledgePath {
1489
1493
  * Parses a Knowledge Map path string into a `{ mount, args }` descriptor.
1490
1494
  *
1491
1495
  * Supported path patterns:
1492
- * `/by-system/<systemId>` -> `{ mount: 'by-system', args: ['<systemId>'] }`
1493
- * `/by-ontology/<ontologyId>` -> `{ mount: 'by-ontology', args: ['<ontologyId>'] }`
1494
- * `/by-kind/<kind>` → `{ mount: 'by-kind', args: ['<kind>'] }`
1495
- * `/by-owner/<ownerId>` → `{ mount: 'by-owner', args: ['<ownerId>'] }`
1496
- * `/graph/<nodeId>/governs` → `{ mount: 'graph', args: ['<nodeId>', 'governs'] }`
1497
- * `/graph/<nodeId>/governed-by` → `{ mount: 'graph', args: ['<nodeId>', 'governed-by'] }`
1498
- * `/<nodeId>` → `{ mount: 'node', args: ['<nodeId>'] }`
1496
+ * `/by-system/<systemId>` -> `{ mount: by-system’, args: [‘<systemId>’] }`
1497
+ * `/by-ontology/<ontologyId>` -> `{ mount: by-ontology’, args: [‘<ontologyId>’] }`
1498
+ * `/by-kind/<kind>` -> `{ mount: by-kind’, args: [‘<kind>’] }`
1499
+ * `/by-owner/<ownerId>` -> `{ mount: by-owner’, args: [‘<ownerId>’] }`
1500
+ * `/graph/<nodeId>/governs` -> `{ mount: graph’, args: [‘<nodeId>’, governs] }`
1501
+ * `/graph/<nodeId>/governed-by` -> `{ mount: graph’, args: [‘<nodeId>’, governed-by] }`
1502
+ * `/<nodeId>` -> `{ mount: node’, args: [‘<nodeId>’] }`
1503
+ * `/all-systems` -> `{ mount: ‘all-systems’, args: [] }`
1504
+ * `/all-resources` -> `{ mount: ‘all-resources’,args: [] }`
1505
+ * `/all-roles` -> `{ mount: ‘all-roles’, args: [] }`
1499
1506
  *
1500
1507
  * The path MUST start with `/`. Trailing slashes are stripped before parsing.
1501
1508
  *
@@ -200,11 +200,20 @@ function parsePath(pathString) {
200
200
  }
201
201
  return { mount: "graph", args: [graphNodeId, verb] };
202
202
  }
203
+ if (first === "all-systems" && rest.length === 0) {
204
+ return { mount: "all-systems", args: [] };
205
+ }
206
+ if (first === "all-resources" && rest.length === 0) {
207
+ return { mount: "all-resources", args: [] };
208
+ }
209
+ if (first === "all-roles" && rest.length === 0) {
210
+ return { mount: "all-roles", args: [] };
211
+ }
203
212
  if (segments.length === 1) {
204
213
  return { mount: "node", args: [first] };
205
214
  }
206
215
  throw new Error(
207
- `parsePath: unrecognized path pattern "${pathString}". Supported: /by-system/<id>, /by-kind/<kind>, /by-owner/<id>, /graph/<nodeId>/governs, /graph/<nodeId>/governed-by, /<nodeId>`
216
+ `parsePath: unrecognized path pattern "${pathString}". Supported: /by-system/<id>, /by-kind/<kind>, /by-owner/<id>, /graph/<nodeId>/governs, /graph/<nodeId>/governed-by, /<nodeId>, /all-systems, /all-resources, /all-roles`
208
217
  );
209
218
  }
210
219
 
@@ -4223,6 +4223,7 @@ declare const OrganizationModelDomainMetadataByDomainSchema: z.ZodPipe<z.ZodDefa
4223
4223
  }>>;
4224
4224
  declare const OrganizationModelSchema: z.ZodObject<{
4225
4225
  version: z.ZodDefault<z.ZodLiteral<1>>;
4226
+ snapshotHash: z.ZodOptional<z.ZodString>;
4226
4227
  domainMetadata: z.ZodPipe<z.ZodDefault<z.ZodObject<{
4227
4228
  branding: z.ZodOptional<z.ZodObject<{
4228
4229
  version: z.ZodDefault<z.ZodLiteral<1>>;
@@ -2464,6 +2464,18 @@ var OrganizationModelDomainMetadataByDomainSchema = z.object({
2464
2464
  }).partial().default(DEFAULT_ORGANIZATION_MODEL_DOMAIN_METADATA).transform((metadata) => ({ ...DEFAULT_ORGANIZATION_MODEL_DOMAIN_METADATA, ...metadata }));
2465
2465
  var OrganizationModelSchemaBase = z.object({
2466
2466
  version: z.literal(1).default(1),
2467
+ /**
2468
+ * Deterministic SHA-256 hex hash of the full resolved model, excluding this
2469
+ * field itself and volatile domainMetadata.lastModified values.
2470
+ *
2471
+ * Stamped at deploy time by the platform OM assembly and persisted alongside
2472
+ * the snapshot in the DB. Compared at API boot to detect stale deployed
2473
+ * snapshots (primary gate: deploy; secondary backstop: boot).
2474
+ *
2475
+ * Optional — absent on models that predate Step 2 versioning or that have
2476
+ * not been stamped (e.g. tenant partial overrides before re-deploy).
2477
+ */
2478
+ snapshotHash: z.string().optional(),
2467
2479
  domainMetadata: OrganizationModelDomainMetadataByDomainSchema,
2468
2480
  branding: OrganizationModelBrandingSchema.default(DEFAULT_ORGANIZATION_MODEL_BRANDING),
2469
2481
  navigation: OrganizationModelNavigationSchema,
@@ -3859,6 +3859,7 @@ type DeepPartial<T> = T extends Array<infer U> ? Array<DeepPartial<U>> : T exten
3859
3859
 
3860
3860
  declare const OrganizationModelSchema: z.ZodObject<{
3861
3861
  version: z.ZodDefault<z.ZodLiteral<1>>;
3862
+ snapshotHash: z.ZodOptional<z.ZodString>;
3862
3863
  domainMetadata: z.ZodPipe<z.ZodDefault<z.ZodObject<{
3863
3864
  branding: z.ZodOptional<z.ZodObject<{
3864
3865
  version: z.ZodDefault<z.ZodLiteral<1>>;
@@ -21612,6 +21612,18 @@ var OrganizationModelDomainMetadataByDomainSchema = z.object({
21612
21612
  }).partial().default(DEFAULT_ORGANIZATION_MODEL_DOMAIN_METADATA).transform((metadata) => ({ ...DEFAULT_ORGANIZATION_MODEL_DOMAIN_METADATA, ...metadata }));
21613
21613
  var OrganizationModelSchemaBase = z.object({
21614
21614
  version: z.literal(1).default(1),
21615
+ /**
21616
+ * Deterministic SHA-256 hex hash of the full resolved model, excluding this
21617
+ * field itself and volatile domainMetadata.lastModified values.
21618
+ *
21619
+ * Stamped at deploy time by the platform OM assembly and persisted alongside
21620
+ * the snapshot in the DB. Compared at API boot to detect stale deployed
21621
+ * snapshots (primary gate: deploy; secondary backstop: boot).
21622
+ *
21623
+ * Optional — absent on models that predate Step 2 versioning or that have
21624
+ * not been stamped (e.g. tenant partial overrides before re-deploy).
21625
+ */
21626
+ snapshotHash: z.string().optional(),
21615
21627
  domainMetadata: OrganizationModelDomainMetadataByDomainSchema,
21616
21628
  branding: OrganizationModelBrandingSchema.default(DEFAULT_ORGANIZATION_MODEL_BRANDING),
21617
21629
  navigation: OrganizationModelNavigationSchema,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elevasis/core",
3
- "version": "0.38.0",
3
+ "version": "0.40.0",
4
4
  "license": "MIT",
5
5
  "description": "Minimal shared constants across Elevasis monorepo",
6
6
  "sideEffects": false,
@@ -15,10 +15,7 @@ import {
15
15
  SYSTEM_INTERFACE_READINESS_PROFILES
16
16
  } from '../../organization-model/domains/systems'
17
17
  import type { OmTopologyRelationship } from '../../organization-model/domains/topology'
18
- import {
19
- type LeadGenStageCatalogEntry,
20
- type StatefulStateDefinition
21
- } from '../../organization-model/domains/sales'
18
+ import { type LeadGenStageCatalogEntry, type StatefulStateDefinition } from '../../organization-model/domains/sales'
22
19
  import { getSystem } from '../../organization-model/helpers'
23
20
  import { getLeadGenStageCatalog } from '../../organization-model/migration-helpers'
24
21
 
@@ -107,6 +104,11 @@ export type SystemInterfaceReadinessIssueFamily =
107
104
  | 'SYSTEM_INTERFACE_INVALID'
108
105
  | 'SYSTEM_INTERFACE_NOT_READY'
109
106
  | 'SYSTEM_BRIDGE_NOT_READY'
107
+ /**
108
+ * The deployed OM snapshot hash does not match the canonical source hash.
109
+ * Redeploy with `pnpm operations:deploy` to refresh the snapshot.
110
+ */
111
+ | 'STALE_OM_SNAPSHOT'
110
112
 
111
113
  export interface SystemInterfaceReadinessIssue {
112
114
  family: SystemInterfaceReadinessIssueFamily
@@ -266,7 +268,10 @@ function getActiveScopedResources(
266
268
  return resources
267
269
  }
268
270
 
269
- function resourceBindingIds(resources: ResourceEntry[], key: 'reads' | 'writes' | 'usesCatalogs' | 'actions' | 'emits'): Set<string> {
271
+ function resourceBindingIds(
272
+ resources: ResourceEntry[],
273
+ key: 'reads' | 'writes' | 'usesCatalogs' | 'actions' | 'emits'
274
+ ): Set<string> {
270
275
  return new Set(resources.flatMap((resource) => resource.ontology?.[key] ?? []))
271
276
  }
272
277
 
@@ -288,7 +293,11 @@ function requireScopedBinding(
288
293
  )
289
294
  }
290
295
 
291
- function ontologyOwnerMatches(ontologyId: string, expectedSystemPath: string, catalog: OntologyCatalogType | undefined): boolean {
296
+ function ontologyOwnerMatches(
297
+ ontologyId: string,
298
+ expectedSystemPath: string,
299
+ catalog: OntologyCatalogType | undefined
300
+ ): boolean {
292
301
  if (catalog?.ownerSystemId !== undefined) return catalog.ownerSystemId === expectedSystemPath
293
302
  return parseOntologyId(ontologyId).scope === expectedSystemPath
294
303
  }
@@ -579,7 +588,13 @@ export function computeInterfaceReadiness(
579
588
  `System "${request.systemPath}" is missing.`,
580
589
  { ref: request.systemPath }
581
590
  )
582
- return { ready: false, systemPath: request.systemPath, interfaceKey: request.interfaceKey, scopedResourceIds, issues }
591
+ return {
592
+ ready: false,
593
+ systemPath: request.systemPath,
594
+ interfaceKey: request.interfaceKey,
595
+ scopedResourceIds,
596
+ issues
597
+ }
583
598
  }
584
599
 
585
600
  if (systemInterface === undefined) {
@@ -590,7 +605,13 @@ export function computeInterfaceReadiness(
590
605
  `System "${request.systemPath}" does not declare interface "${request.interfaceKey}".`,
591
606
  { path: readinessMarkerPath(request) }
592
607
  )
593
- return { ready: false, systemPath: request.systemPath, interfaceKey: request.interfaceKey, scopedResourceIds, issues }
608
+ return {
609
+ ready: false,
610
+ systemPath: request.systemPath,
611
+ interfaceKey: request.interfaceKey,
612
+ scopedResourceIds,
613
+ issues
614
+ }
594
615
  }
595
616
 
596
617
  if (systemInterface.lifecycle !== 'active') {
@@ -604,7 +625,8 @@ export function computeInterfaceReadiness(
604
625
  }
605
626
 
606
627
  const supportedProfile =
607
- readinessProfile !== undefined && SYSTEM_INTERFACE_PROFILES.some((profile) => profile.readinessProfile === readinessProfile)
628
+ readinessProfile !== undefined &&
629
+ SYSTEM_INTERFACE_PROFILES.some((profile) => profile.readinessProfile === readinessProfile)
608
630
 
609
631
  if (!supportedProfile) {
610
632
  addReadinessIssue(
@@ -1,4 +1,17 @@
1
- export { bySystem, byOntology, byKind, byOwner, governs, governedBy, parsePath, omSearch, omDescribe } from './queries'
1
+ export {
2
+ bySystem,
3
+ byOntology,
4
+ byKind,
5
+ byOwner,
6
+ governs,
7
+ governedBy,
8
+ parsePath,
9
+ omSearch,
10
+ omDescribe,
11
+ listAllSystemsFlat,
12
+ listAllResources,
13
+ listAllRoles
14
+ } from './queries'
2
15
  export type {
3
16
  KnowledgeMount,
4
17
  ParsedKnowledgePath,
@@ -217,12 +217,54 @@ export function governedBy(graph: OrganizationGraph, nodeId: string): string[] {
217
217
  return results
218
218
  }
219
219
 
220
+ // ---------------------------------------------------------------------------
221
+ // Enumeration helpers (Bucket 4: /all-* mounts)
222
+ // ---------------------------------------------------------------------------
223
+
224
+ /**
225
+ * Returns all systems flattened from the recursive OM tree via `listAllSystems`.
226
+ *
227
+ * Each entry is `{ path, system }` — the same shape produced by `listAllSystems`.
228
+ *
229
+ * @param model - The resolved OrganizationModel.
230
+ */
231
+ export function listAllSystemsFlat(model: OrganizationModel): ReturnType<typeof listAllSystems> {
232
+ return listAllSystems(model)
233
+ }
234
+
235
+ /**
236
+ * Returns every resource from the model as a flat array, sorted by id.
237
+ *
238
+ * @param model - The resolved OrganizationModel.
239
+ */
240
+ export function listAllResources(model: OrganizationModel): NonNullable<OrganizationModel['resources']>[string][] {
241
+ return Object.values(model.resources ?? {}).sort((a, b) => a.id.localeCompare(b.id))
242
+ }
243
+
244
+ /**
245
+ * Returns every role from the model as a flat array, sorted by id.
246
+ *
247
+ * @param model - The resolved OrganizationModel.
248
+ */
249
+ export function listAllRoles(model: OrganizationModel): NonNullable<OrganizationModel['roles']>[string][] {
250
+ return Object.values(model.roles ?? {}).sort((a, b) => a.id.localeCompare(b.id))
251
+ }
252
+
220
253
  // ---------------------------------------------------------------------------
221
254
  // Path parser
222
255
  // ---------------------------------------------------------------------------
223
256
 
224
257
  /** The recognized mount axes for Knowledge Map paths. */
225
- export type KnowledgeMount = 'by-system' | 'by-ontology' | 'by-kind' | 'by-owner' | 'graph' | 'node'
258
+ export type KnowledgeMount =
259
+ | 'by-system'
260
+ | 'by-ontology'
261
+ | 'by-kind'
262
+ | 'by-owner'
263
+ | 'graph'
264
+ | 'node'
265
+ | 'all-systems'
266
+ | 'all-resources'
267
+ | 'all-roles'
226
268
 
227
269
  /**
228
270
  * The result of parsing a Knowledge Map path string.
@@ -230,12 +272,15 @@ export type KnowledgeMount = 'by-system' | 'by-ontology' | 'by-kind' | 'by-owner
230
272
  * Shape: `{ mount: KnowledgeMount, args: string[] }`
231
273
  *
232
274
  * Per-mount arg arrays:
233
- * - `by-system`: `[systemId]` (e.g. `['sales.crm']`)
234
- * - `by-ontology`:`[ontologyId]` (e.g. `['sales.crm:object/deal']`)
235
- * - `by-kind`: `[kind]` (e.g. `['playbook']`)
236
- * - `by-owner`: `[ownerId]` (e.g. `['role.ops-lead']`)
237
- * - `graph`: `[nodeId, verb]` where verb is `'governs'` or `'governed-by'`
238
- * - `node`: `[nodeId]` (single node lookup, no sub-path)
275
+ * - `by-system`: `[systemId]` (e.g. `['sales.crm']`)
276
+ * - `by-ontology`: `[ontologyId]` (e.g. `['sales.crm:object/deal']`)
277
+ * - `by-kind`: `[kind]` (e.g. `['playbook']`)
278
+ * - `by-owner`: `[ownerId]` (e.g. `['role.ops-lead']`)
279
+ * - `graph`: `[nodeId, verb]` where verb is `'governs'` or `'governed-by'`
280
+ * - `node`: `[nodeId]` (single node lookup, no sub-path)
281
+ * - `all-systems`: `[]` (no args — returns all systems flattened)
282
+ * - `all-resources`:`[]` (no args — returns all resources)
283
+ * - `all-roles`: `[]` (no args — returns all roles)
239
284
  */
240
285
  export interface ParsedKnowledgePath {
241
286
  mount: KnowledgeMount
@@ -246,13 +291,16 @@ export interface ParsedKnowledgePath {
246
291
  * Parses a Knowledge Map path string into a `{ mount, args }` descriptor.
247
292
  *
248
293
  * Supported path patterns:
249
- * `/by-system/<systemId>` -> `{ mount: 'by-system', args: ['<systemId>'] }`
250
- * `/by-ontology/<ontologyId>` -> `{ mount: 'by-ontology', args: ['<ontologyId>'] }`
251
- * `/by-kind/<kind>` → `{ mount: 'by-kind', args: ['<kind>'] }`
252
- * `/by-owner/<ownerId>` → `{ mount: 'by-owner', args: ['<ownerId>'] }`
253
- * `/graph/<nodeId>/governs` → `{ mount: 'graph', args: ['<nodeId>', 'governs'] }`
254
- * `/graph/<nodeId>/governed-by` → `{ mount: 'graph', args: ['<nodeId>', 'governed-by'] }`
255
- * `/<nodeId>` → `{ mount: 'node', args: ['<nodeId>'] }`
294
+ * `/by-system/<systemId>` -> `{ mount: by-system’, args: [‘<systemId>’] }`
295
+ * `/by-ontology/<ontologyId>` -> `{ mount: by-ontology’, args: [‘<ontologyId>’] }`
296
+ * `/by-kind/<kind>` -> `{ mount: by-kind’, args: [‘<kind>’] }`
297
+ * `/by-owner/<ownerId>` -> `{ mount: by-owner’, args: [‘<ownerId>’] }`
298
+ * `/graph/<nodeId>/governs` -> `{ mount: graph’, args: [‘<nodeId>’, governs] }`
299
+ * `/graph/<nodeId>/governed-by` -> `{ mount: graph’, args: [‘<nodeId>’, governed-by] }`
300
+ * `/<nodeId>` -> `{ mount: node’, args: [‘<nodeId>’] }`
301
+ * `/all-systems` -> `{ mount: ‘all-systems’, args: [] }`
302
+ * `/all-resources` -> `{ mount: ‘all-resources’,args: [] }`
303
+ * `/all-roles` -> `{ mount: ‘all-roles’, args: [] }`
256
304
  *
257
305
  * The path MUST start with `/`. Trailing slashes are stripped before parsing.
258
306
  *
@@ -325,6 +373,17 @@ export function parsePath(pathString: string): ParsedKnowledgePath {
325
373
  return { mount: 'graph', args: [graphNodeId, verb] }
326
374
  }
327
375
 
376
+ // /all-systems /all-resources /all-roles (no-arg enumeration mounts)
377
+ if (first === 'all-systems' && rest.length === 0) {
378
+ return { mount: 'all-systems', args: [] }
379
+ }
380
+ if (first === 'all-resources' && rest.length === 0) {
381
+ return { mount: 'all-resources', args: [] }
382
+ }
383
+ if (first === 'all-roles' && rest.length === 0) {
384
+ return { mount: 'all-roles', args: [] }
385
+ }
386
+
328
387
  // /<nodeId> (single node)
329
388
  // first must not be a recognized mount prefix
330
389
  if (segments.length === 1) {
@@ -332,7 +391,7 @@ export function parsePath(pathString: string): ParsedKnowledgePath {
332
391
  }
333
392
 
334
393
  throw new Error(
335
- `parsePath: unrecognized path pattern "${pathString}". Supported: /by-system/<id>, /by-kind/<kind>, /by-owner/<id>, /graph/<nodeId>/governs, /graph/<nodeId>/governed-by, /<nodeId>`
394
+ `parsePath: unrecognized path pattern "${pathString}". Supported: /by-system/<id>, /by-kind/<kind>, /by-owner/<id>, /graph/<nodeId>/governs, /graph/<nodeId>/governed-by, /<nodeId>, /all-systems, /all-resources, /all-roles`
336
395
  )
337
396
  }
338
397
 
@@ -0,0 +1,82 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { canonicalStringifyOrganizationModel } from '../snapshot-hash'
3
+ import type { OrganizationModel } from '../types'
4
+ import { resolveOrganizationModel } from '../resolve'
5
+
6
+ // Minimal valid resolved model for hash-stability tests.
7
+ function makeModel(overrides: Partial<OrganizationModel> = {}): OrganizationModel {
8
+ return resolveOrganizationModel(overrides)
9
+ }
10
+
11
+ describe('canonicalStringifyOrganizationModel', () => {
12
+ it('produces the same string regardless of key insertion order', () => {
13
+ // Two equivalent objects with different key ordering in domainMetadata.
14
+ const model = makeModel()
15
+
16
+ // Produce two copies with the same semantic content but different metadata key order.
17
+ const a: OrganizationModel = { ...model }
18
+ const b: OrganizationModel = {
19
+ ...model,
20
+ // Re-assign domainMetadata entries in a different key order.
21
+ domainMetadata: Object.fromEntries(
22
+ Object.entries(model.domainMetadata).reverse()
23
+ ) as OrganizationModel['domainMetadata']
24
+ }
25
+
26
+ expect(canonicalStringifyOrganizationModel(a)).toBe(canonicalStringifyOrganizationModel(b))
27
+ })
28
+
29
+ it('excludes the snapshotHash field from the serialized output', () => {
30
+ const base = makeModel()
31
+ const withHash: OrganizationModel = { ...base, snapshotHash: 'abc123' }
32
+ const withOtherHash: OrganizationModel = { ...base, snapshotHash: 'xyz789' }
33
+ const withoutHash: OrganizationModel = { ...base, snapshotHash: undefined }
34
+
35
+ // All three produce the same canonical string — snapshotHash is excluded.
36
+ expect(canonicalStringifyOrganizationModel(withHash)).toBe(canonicalStringifyOrganizationModel(withoutHash))
37
+ expect(canonicalStringifyOrganizationModel(withHash)).toBe(canonicalStringifyOrganizationModel(withOtherHash))
38
+ })
39
+
40
+ it('excludes domainMetadata lastModified from the serialized output', () => {
41
+ const base = makeModel()
42
+
43
+ // Two models differing only in lastModified.
44
+ const earlyDate: OrganizationModel = {
45
+ ...base,
46
+ domainMetadata: {
47
+ ...base.domainMetadata,
48
+ branding: { version: 1, lastModified: '2026-01-01' }
49
+ }
50
+ }
51
+ const lateDate: OrganizationModel = {
52
+ ...base,
53
+ domainMetadata: {
54
+ ...base.domainMetadata,
55
+ branding: { version: 1, lastModified: '2026-12-31' }
56
+ }
57
+ }
58
+
59
+ expect(canonicalStringifyOrganizationModel(earlyDate)).toBe(canonicalStringifyOrganizationModel(lateDate))
60
+ })
61
+
62
+ it('produces different strings for different semantic content', () => {
63
+ const modelA = makeModel({ version: 1 })
64
+ const modelB = makeModel({
65
+ version: 1,
66
+ branding: { organizationName: 'AlphaOrg', productName: 'AlphaPlatform', shortName: 'Alpha' }
67
+ })
68
+
69
+ // Different semantic content → different canonical strings.
70
+ expect(canonicalStringifyOrganizationModel(modelA)).not.toBe(canonicalStringifyOrganizationModel(modelB))
71
+ })
72
+
73
+ it('is deterministic across multiple calls with the same input', () => {
74
+ const model = makeModel()
75
+ const first = canonicalStringifyOrganizationModel(model)
76
+ const second = canonicalStringifyOrganizationModel(model)
77
+ const third = canonicalStringifyOrganizationModel(model)
78
+
79
+ expect(first).toBe(second)
80
+ expect(first).toBe(third)
81
+ })
82
+ })
@@ -1,5 +1,6 @@
1
1
  export * from './cross-ref'
2
2
  export * from './schema'
3
+ export * from './snapshot-hash'
3
4
  export * from './types'
4
5
  export * from './ontology'
5
6
  export * from './contracts'
@@ -91,6 +91,18 @@ export const OrganizationModelDomainMetadataByDomainSchema = z
91
91
  // Surfaces are derived from navigation.sidebar routeable leaves.
92
92
  const OrganizationModelSchemaBase = z.object({
93
93
  version: z.literal(1).default(1),
94
+ /**
95
+ * Deterministic SHA-256 hex hash of the full resolved model, excluding this
96
+ * field itself and volatile domainMetadata.lastModified values.
97
+ *
98
+ * Stamped at deploy time by the platform OM assembly and persisted alongside
99
+ * the snapshot in the DB. Compared at API boot to detect stale deployed
100
+ * snapshots (primary gate: deploy; secondary backstop: boot).
101
+ *
102
+ * Optional — absent on models that predate Step 2 versioning or that have
103
+ * not been stamped (e.g. tenant partial overrides before re-deploy).
104
+ */
105
+ snapshotHash: z.string().optional(),
94
106
  domainMetadata: OrganizationModelDomainMetadataByDomainSchema,
95
107
  branding: OrganizationModelBrandingSchema.default(DEFAULT_ORGANIZATION_MODEL_BRANDING),
96
108
  navigation: OrganizationModelNavigationSchema,
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Server-only: compute a deterministic SHA-256 hash for an OrganizationModel snapshot.
3
+ *
4
+ * Uses node:crypto — never import this file from browser-reachable code.
5
+ * For the browser-safe canonical serializer, use `canonicalStringifyOrganizationModel`
6
+ * from `@repo/core/organization-model`.
7
+ */
8
+
9
+ import { createHash } from 'node:crypto'
10
+ import type { OrganizationModel } from '../types'
11
+ import { canonicalStringifyOrganizationModel } from '../snapshot-hash'
12
+
13
+ /**
14
+ * Compute a deterministic SHA-256 hex hash for an OrganizationModel.
15
+ *
16
+ * Delegates serialization to `canonicalStringifyOrganizationModel` (key-sorted,
17
+ * excludes `snapshotHash` and `domainMetadata[*].lastModified`) then hashes with SHA-256.
18
+ *
19
+ * @returns 64-character lowercase hex string
20
+ */
21
+ export function computeOrganizationModelSnapshotHash(model: OrganizationModel): string {
22
+ return createHash('sha256').update(canonicalStringifyOrganizationModel(model)).digest('hex')
23
+ }
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Deterministic canonical serialization helper for OrganizationModel snapshot hashing.
3
+ *
4
+ * Provides a stable JSON stringify that produces identical output for equivalent objects
5
+ * regardless of key insertion order. Used by deploy-time and boot-time snapshot hash
6
+ * verification to detect stale deployed OM snapshots.
7
+ *
8
+ * Only the serialization is here (browser-safe). Callers that need a hash compute it
9
+ * themselves using their preferred crypto implementation (e.g. node:crypto sha256).
10
+ */
11
+
12
+ import type { OrganizationModel } from './types'
13
+
14
+ /**
15
+ * Recursively serialize a value to a deterministic JSON string with sorted keys.
16
+ * Arrays preserve order; objects sort keys lexicographically.
17
+ */
18
+ function deterministicStringify(value: unknown): string {
19
+ if (Array.isArray(value)) {
20
+ return `[${value.map(deterministicStringify).join(',')}]`
21
+ }
22
+
23
+ if (value !== null && typeof value === 'object') {
24
+ return `{${Object.entries(value as Record<string, unknown>)
25
+ .sort(([a], [b]) => a.localeCompare(b))
26
+ .map(([key, entry]) => `${JSON.stringify(key)}:${deterministicStringify(entry)}`)
27
+ .join(',')}}`
28
+ }
29
+
30
+ return JSON.stringify(value)
31
+ }
32
+
33
+ /**
34
+ * Produce a deterministic canonical string for an OrganizationModel suitable for hashing.
35
+ *
36
+ * Exclusions (volatile / self-referential fields that must not be part of the hash input):
37
+ * - `snapshotHash` — the field being populated from this hash; must not be circular
38
+ * - `domainMetadata[*].lastModified` — wall-clock date string; changes on every edit
39
+ * independent of semantic content, causing spurious hash mismatches
40
+ *
41
+ * Same model content → same canonical string, regardless of key insertion order.
42
+ */
43
+ export function canonicalStringifyOrganizationModel(model: OrganizationModel): string {
44
+ // Shallow-clone to strip snapshotHash without mutating the caller's object.
45
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
46
+ const { snapshotHash: _snapshotHash, domainMetadata, ...rest } = model
47
+
48
+ // Strip lastModified from each domain metadata entry so volatile dates do not
49
+ // pollute the hash. The domain version numbers are still included.
50
+ const strippedDomainMetadata = domainMetadata
51
+ ? Object.fromEntries(
52
+ Object.entries(domainMetadata).map(([domain, meta]) => {
53
+ if (meta === null || typeof meta !== 'object') return [domain, meta]
54
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
55
+ const { lastModified: _lastModified, ...metaRest } = meta as Record<string, unknown>
56
+ return [domain, metaRest]
57
+ })
58
+ )
59
+ : undefined
60
+
61
+ const forHashing = strippedDomainMetadata !== undefined ? { ...rest, domainMetadata: strippedDomainMetadata } : rest
62
+
63
+ return deterministicStringify(forHashing)
64
+ }
@@ -1,3 +1,3 @@
1
1
  export const VERSION = {
2
- CURRENT: '1.12.14'
2
+ CURRENT: '1.12.16'
3
3
  }