@cullet/erp-core 1.0.10 → 1.1.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.
- package/KIT_CONTEXT.md +67 -0
- package/README.md +128 -1
- package/dist/app-error.d.ts +108 -0
- package/dist/application/index.d.ts +2 -0
- package/dist/application/index.js +2 -0
- package/dist/errors/index.d.ts +2 -1
- package/dist/errors/index.js +4 -2
- package/dist/gate-engine-registry.d.ts +80 -0
- package/dist/gate-v1-payload.schema.js +2 -28
- package/dist/gate-v1-payload.schema.js.map +1 -1
- package/dist/hashing.js +49 -0
- package/dist/hashing.js.map +1 -0
- package/dist/index.d.ts +122 -45
- package/dist/index.js +104 -85
- package/dist/index.js.map +1 -1
- package/dist/not-found-error.js +54 -0
- package/dist/not-found-error.js.map +1 -0
- package/dist/outcome.js +1 -1
- package/dist/parse-gate-payload.d.ts +3 -78
- package/dist/path.d.ts +89 -0
- package/dist/policies/engines/index.d.ts +2 -1
- package/dist/policies/index.d.ts +4 -2
- package/dist/policy-service.d.ts +102 -86
- package/dist/policy-service.js +78 -16
- package/dist/policy-service.js.map +1 -1
- package/dist/temporal-guards.js +30 -0
- package/dist/temporal-guards.js.map +1 -0
- package/dist/temporal-use-case.d.ts +309 -0
- package/dist/temporal-use-case.js +284 -0
- package/dist/temporal-use-case.js.map +1 -0
- package/dist/validation-code.js +34 -47
- package/dist/validation-code.js.map +1 -1
- package/dist/validation-error.d.ts +171 -74
- package/dist/validation-error.js +152 -38
- package/dist/validation-error.js.map +1 -1
- package/dist/validation-exception.js +18 -0
- package/dist/validation-exception.js.map +1 -0
- package/meta.json +4 -2
- package/package.json +5 -1
- package/src/application/index.ts +1 -0
- package/src/core/application/commands/index.ts +1 -0
- package/src/core/application/index.ts +12 -3
- package/src/core/application/ports/index.ts +2 -2
- package/src/core/application/ports/policy-port.ts +13 -3
- package/src/core/application/ports/repository.port.ts +37 -1
- package/src/core/application/temporal/temporal-use-case.ts +14 -2
- package/src/core/application/use-case.ts +133 -2
- package/src/core/domain/entity.ts +71 -0
- package/src/core/domain/value-object.ts +41 -0
- package/src/core/errors/app-error.ts +33 -0
- package/src/core/errors/authorization-error.ts +43 -0
- package/src/core/errors/conflict-error.ts +69 -0
- package/src/core/errors/integration-error.ts +25 -0
- package/src/core/errors/not-found-error.ts +14 -0
- package/src/core/errors/validation-error.ts +18 -0
- package/src/core/index.ts +1 -10
- package/src/core/policies/catalog/policy-catalog.ts +36 -0
- package/src/core/policies/resolver/policy-resolver.ts +10 -0
- package/src/core/policies/service/policy-service.ts +53 -0
- package/src/examples/application/cancel-order.example.ts +124 -0
- package/src/examples/application/in-memory-account-repository.example.ts +73 -0
- package/src/version.ts +1 -1
package/dist/policy-service.d.ts
CHANGED
|
@@ -1,36 +1,6 @@
|
|
|
1
|
-
import { C as TenantId, I as Result, S as SchoolId, b as PolicyDecisionId,
|
|
2
|
-
import {
|
|
1
|
+
import { C as TenantId, I as Result, S as SchoolId, b as PolicyDecisionId, t as GateOutcome, u as GatePayload, x as PolicyDefinitionId, y as PolicyReporter } from "./gate-types.js";
|
|
2
|
+
import { a as ComputeOutcome, l as ComputePayload, n as ComputeRegistry, t as GateEngineRegistry } from "./gate-engine-registry.js";
|
|
3
3
|
|
|
4
|
-
//#region src/core/config/core-config.instance.d.ts
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Shared instance for the application composition root.
|
|
8
|
-
* Use `new CoreConfig()` for isolated runtimes, tests, or multi-tenant hosts.
|
|
9
|
-
*
|
|
10
|
-
* The default reporter is silent so the core has no runtime logging dependency.
|
|
11
|
-
* Hosts can swap in their own `PolicyReporter` via
|
|
12
|
-
* `coreConfig.configure({ observability: { reporter } })`.
|
|
13
|
-
*/
|
|
14
|
-
declare const coreConfig: CoreConfig;
|
|
15
|
-
//#endregion
|
|
16
|
-
//#region src/core/policies/utils/hash.d.ts
|
|
17
|
-
/**
|
|
18
|
-
* Produces a deterministic SHA-256 hex digest of a canonical JSON representation.
|
|
19
|
-
* Object keys are sorted recursively so key order does not affect the hash.
|
|
20
|
-
* Array item order is preserved on purpose; callers that treat arrays as sets
|
|
21
|
-
* must normalize or sort them before hashing.
|
|
22
|
-
*
|
|
23
|
-
* Values that JSON.stringify would silently drop or coerce (undefined,
|
|
24
|
-
* non-finite numbers, functions, symbols, bigint) are rejected up-front to
|
|
25
|
-
* prevent semantically different payloads from collapsing to the same hash.
|
|
26
|
-
*/
|
|
27
|
-
declare class PolicyHashing {
|
|
28
|
-
static sha256(input: string): string;
|
|
29
|
-
private static assertHashable;
|
|
30
|
-
static canonicalJson(value: unknown): string;
|
|
31
|
-
static computePayloadHash(payload: unknown, policyKey: string, policyVersion: string): string;
|
|
32
|
-
}
|
|
33
|
-
//#endregion
|
|
34
4
|
//#region src/core/policies/catalog/catalog-types.d.ts
|
|
35
5
|
type PolicyKind = "GATE" | "COMPUTE";
|
|
36
6
|
type PolicyOwner = "PLATFORM_OWNER" | "TENANT_ADMIN" | "SCHOOL_ADMIN";
|
|
@@ -116,9 +86,43 @@ declare class PolicyCatalogEntry {
|
|
|
116
86
|
declare class PolicyCatalog {
|
|
117
87
|
private readonly versionedEntries;
|
|
118
88
|
private readonly entriesByKey;
|
|
89
|
+
/**
|
|
90
|
+
* Indexes the given entries up front into two lookups — one per exact
|
|
91
|
+
* variant, one per policy key (the "family") — and validates them eagerly:
|
|
92
|
+
* a duplicate variant or an internally inconsistent family throws here, at
|
|
93
|
+
* wiring time, so a malformed catalog fails fast at startup rather than on
|
|
94
|
+
* the first evaluation.
|
|
95
|
+
*
|
|
96
|
+
* @throws {UnexpectedError} When two entries resolve to the same variant key.
|
|
97
|
+
*/
|
|
119
98
|
constructor(entries: readonly PolicyCatalogEntry[]);
|
|
99
|
+
/**
|
|
100
|
+
* Returns the single entry for a key. Deliberately strict: if the key has
|
|
101
|
+
* more than one variant it errs rather than guessing, steering the caller to
|
|
102
|
+
* {@link getVersioned} or {@link getFamily}. Use this only when a key is
|
|
103
|
+
* known to be unambiguous.
|
|
104
|
+
*
|
|
105
|
+
* @returns `ok` with the entry, or `err` when the key is unknown or ambiguous.
|
|
106
|
+
*/
|
|
120
107
|
get(key: string): Result<PolicyCatalogEntry, string>;
|
|
108
|
+
/**
|
|
109
|
+
* Returns every variant registered under a key — the whole "family". This is
|
|
110
|
+
* the lookup the evaluation pipeline uses, since a single key may host
|
|
111
|
+
* several engine/schema variants that the resolver then chooses between.
|
|
112
|
+
*
|
|
113
|
+
* @returns `ok` with the family (always non-empty), or `err` when the key is unknown.
|
|
114
|
+
*/
|
|
121
115
|
getFamily(key: string): Result<readonly PolicyCatalogEntry[], string>;
|
|
116
|
+
/**
|
|
117
|
+
* Resolves the one entry matching an exact variant — kind plus engine and
|
|
118
|
+
* payload-schema versions. Falls back to the sole family member when a key
|
|
119
|
+
* has exactly one entry and that entry declares no explicit version
|
|
120
|
+
* selector, so unversioned single-variant policies "just work" without the
|
|
121
|
+
* caller spelling out versions.
|
|
122
|
+
*
|
|
123
|
+
* @param params - The key and the variant coordinates to match on.
|
|
124
|
+
* @returns `ok` with the matching entry, or `err` when no variant matches.
|
|
125
|
+
*/
|
|
122
126
|
getVersioned(params: {
|
|
123
127
|
readonly key: string;
|
|
124
128
|
readonly kind: "GATE" | "COMPUTE";
|
|
@@ -126,7 +130,9 @@ declare class PolicyCatalog {
|
|
|
126
130
|
readonly computeEngineVersion?: number;
|
|
127
131
|
readonly payloadSchemaVersion?: number;
|
|
128
132
|
}): Result<PolicyCatalogEntry, string>;
|
|
133
|
+
/** Lists every registered variant across all keys — useful for introspection and diagnostics. */
|
|
129
134
|
list(): readonly PolicyCatalogEntry[];
|
|
135
|
+
/** Returns whether any variant is registered under the given key. */
|
|
130
136
|
has(key: string): boolean;
|
|
131
137
|
}
|
|
132
138
|
//#endregion
|
|
@@ -253,39 +259,6 @@ interface PolicyDefinitionRepository {
|
|
|
253
259
|
findCandidates(params: FindCandidatesParams): PolicyDefinition[];
|
|
254
260
|
}
|
|
255
261
|
//#endregion
|
|
256
|
-
//#region src/core/policies/defs/in-memory-policy-definition-repo.d.ts
|
|
257
|
-
declare class InMemoryPolicyDefinitionRepository implements PolicyDefinitionRepository {
|
|
258
|
-
private readonly definitions;
|
|
259
|
-
private readonly definitionsByPolicyKey;
|
|
260
|
-
constructor(definitions: readonly PolicyDefinition[]);
|
|
261
|
-
findCandidates(params: FindCandidatesParams): PolicyDefinition[];
|
|
262
|
-
}
|
|
263
|
-
//#endregion
|
|
264
|
-
//#region src/core/policies/package/policy-package.d.ts
|
|
265
|
-
/**
|
|
266
|
-
* Protocol of contribution: a module declares what it wants to add to the
|
|
267
|
-
* policy system without the core knowing which module it is.
|
|
268
|
-
*
|
|
269
|
-
* - catalogEntries: static policy descriptors the catalog will register.
|
|
270
|
-
* - definitions: default policy definitions (rules/parameters) to seed.
|
|
271
|
-
* - evaluators: compute evaluator registrations keyed by policy/version.
|
|
272
|
-
*/
|
|
273
|
-
interface PolicyPackage {
|
|
274
|
-
readonly catalogEntries: readonly PolicyCatalogEntryProps[];
|
|
275
|
-
readonly definitions: readonly PolicyDefinition[];
|
|
276
|
-
readonly evaluators: readonly ComputeEvaluatorRegistration[];
|
|
277
|
-
}
|
|
278
|
-
//#endregion
|
|
279
|
-
//#region src/core/policies/catalog/catalog.instance.d.ts
|
|
280
|
-
/**
|
|
281
|
-
* Aggregates catalog entries contributed by each module package into a single
|
|
282
|
-
* PolicyCatalog. The core knows nothing about concrete modules — callers
|
|
283
|
-
* are responsible for passing all relevant packages at composition time.
|
|
284
|
-
*/
|
|
285
|
-
declare class PolicyCatalogFactory {
|
|
286
|
-
static build(packages: readonly PolicyPackage[]): PolicyCatalog;
|
|
287
|
-
}
|
|
288
|
-
//#endregion
|
|
289
262
|
//#region src/core/policies/context/context-seed.d.ts
|
|
290
263
|
/**
|
|
291
264
|
* Seed data provided by the use case / caller.
|
|
@@ -381,26 +354,6 @@ declare class PolicyContextBuilder {
|
|
|
381
354
|
build(requirements: readonly string[], seed: ContextSeed): Promise<Result<Record<string, unknown>, string>>;
|
|
382
355
|
}
|
|
383
356
|
//#endregion
|
|
384
|
-
//#region src/core/policies/context/context-registry.instance.d.ts
|
|
385
|
-
/**
|
|
386
|
-
* Official instance of the ContextResolverRegistry.
|
|
387
|
-
* Use `new ContextResolverRegistry()` plus `registerNamespacedContextResolversIn`
|
|
388
|
-
* when each runtime/composition root needs its own isolated resolver graph.
|
|
389
|
-
*/
|
|
390
|
-
declare const contextResolverRegistry: ContextResolverRegistry;
|
|
391
|
-
declare function registerNamespacedContextResolvers(namespace: string, resolvers: readonly ContextValueResolver[]): Result<ContextResolverRegistry, string>;
|
|
392
|
-
//#endregion
|
|
393
|
-
//#region src/core/policies/context/path.d.ts
|
|
394
|
-
declare class PolicyContextPath {
|
|
395
|
-
private static readonly FORBIDDEN_SEGMENTS;
|
|
396
|
-
private static resolve;
|
|
397
|
-
private static parseSegments;
|
|
398
|
-
private static isPlainRecord;
|
|
399
|
-
static getOrAbsent(obj: Record<string, unknown>, path: string): Result<unknown, "absent">;
|
|
400
|
-
static has(obj: Record<string, unknown>, path: string): boolean;
|
|
401
|
-
static set(obj: Record<string, unknown>, path: string, value: unknown): Result<void, string>;
|
|
402
|
-
}
|
|
403
|
-
//#endregion
|
|
404
357
|
//#region src/core/policies/asof/asof.d.ts
|
|
405
358
|
interface DeriveAsOfOptions {
|
|
406
359
|
readonly maxFutureYears?: number;
|
|
@@ -422,6 +375,16 @@ declare class PolicyAsOfResolver {
|
|
|
422
375
|
* 5. policyVersion descending (semver — final deterministic tiebreaker)
|
|
423
376
|
*/
|
|
424
377
|
declare class PolicyResolver {
|
|
378
|
+
/**
|
|
379
|
+
* Picks the single winning definition from an already-filtered candidate
|
|
380
|
+
* list by applying the class's ordering criteria in priority order. The sort
|
|
381
|
+
* runs over a copy, so the caller's array is left untouched, and the final
|
|
382
|
+
* semver tiebreaker guarantees the winner is fully deterministic — it never
|
|
383
|
+
* depends on the order the repository returned the candidates in.
|
|
384
|
+
*
|
|
385
|
+
* @param candidates - Definitions already filtered to match key, scope, and as-of.
|
|
386
|
+
* @returns `ok` with the highest-ranked definition, or `err` when the list is empty.
|
|
387
|
+
*/
|
|
425
388
|
resolveBest(candidates: readonly PolicyDefinition[]): Result<PolicyDefinition, string>;
|
|
426
389
|
}
|
|
427
390
|
//#endregion
|
|
@@ -485,6 +448,13 @@ declare class PolicyEvaluationErrors {
|
|
|
485
448
|
}
|
|
486
449
|
//#endregion
|
|
487
450
|
//#region src/core/policies/service/policy-service.d.ts
|
|
451
|
+
/**
|
|
452
|
+
* Everything a single policy evaluation needs. `policyKey` names the policy,
|
|
453
|
+
* `scopeChain` narrows the search from most-specific to least-specific scope,
|
|
454
|
+
* `contextVersion` pins which context schema applies, and `seed` carries the
|
|
455
|
+
* raw facts the context builder expands. `decisionId` is the caller's
|
|
456
|
+
* idempotency/audit handle, echoed back on the result.
|
|
457
|
+
*/
|
|
488
458
|
interface EvaluateInput {
|
|
489
459
|
readonly decisionId: PolicyDecisionId;
|
|
490
460
|
readonly policyKey: string;
|
|
@@ -492,10 +462,22 @@ interface EvaluateInput {
|
|
|
492
462
|
readonly contextVersion: number;
|
|
493
463
|
readonly seed: ContextSeed;
|
|
494
464
|
}
|
|
465
|
+
/**
|
|
466
|
+
* Cross-cutting knobs for a {@link PolicyService}: how the as-of instant is
|
|
467
|
+
* derived ({@link asOf}) and where evaluation telemetry is sent
|
|
468
|
+
* ({@link reporter}). Both are optional; the service defaults to a silent
|
|
469
|
+
* reporter so telemetry is opt-in.
|
|
470
|
+
*/
|
|
495
471
|
interface PolicyServiceOptions {
|
|
496
472
|
readonly asOf?: DeriveAsOfOptions;
|
|
497
473
|
readonly reporter?: PolicyReporter;
|
|
498
474
|
}
|
|
475
|
+
/**
|
|
476
|
+
* The collaborators a {@link PolicyService} is wired with. Each is an injected
|
|
477
|
+
* port so the service stays storage- and engine-agnostic: the catalog defines
|
|
478
|
+
* the universe of policies, `defRepo` supplies their versioned definitions, the
|
|
479
|
+
* `resolver` picks the winning definition, and the engine registries execute it.
|
|
480
|
+
*/
|
|
499
481
|
interface PolicyServiceParams {
|
|
500
482
|
readonly catalog: PolicyCatalog;
|
|
501
483
|
readonly contextBuilder: PolicyContextBuilder;
|
|
@@ -507,6 +489,13 @@ interface PolicyServiceParams {
|
|
|
507
489
|
}
|
|
508
490
|
/** Union of all possible policy decision Outcomes. */
|
|
509
491
|
type PolicyDecision = GateOutcome | ComputeOutcome;
|
|
492
|
+
/**
|
|
493
|
+
* The full record of one successful evaluation. Beyond the business
|
|
494
|
+
* {@link decision}, it pins the exact definition that produced it — id, key,
|
|
495
|
+
* version, payload hash, and engine version — together with the `asOf` instant
|
|
496
|
+
* the policy was selected for and the wall-clock `evaluatedAt`. That provenance
|
|
497
|
+
* is what makes a decision reproducible and auditable after the fact.
|
|
498
|
+
*/
|
|
510
499
|
interface PolicyEvaluationResult {
|
|
511
500
|
readonly decisionId: PolicyDecisionId;
|
|
512
501
|
readonly ref: {
|
|
@@ -524,6 +513,18 @@ interface PolicyEvaluationResult {
|
|
|
524
513
|
/** Business decision expressed as an Outcome — not a technical Result. */
|
|
525
514
|
readonly decision: PolicyDecision;
|
|
526
515
|
}
|
|
516
|
+
/**
|
|
517
|
+
* Orchestrates the end-to-end evaluation of a policy: it parses and validates
|
|
518
|
+
* the context seed, derives the as-of instant, resolves the best matching
|
|
519
|
+
* definition for the requested key/scope/version, builds the context the policy
|
|
520
|
+
* needs, and runs it through the right engine (gate or compute).
|
|
521
|
+
*
|
|
522
|
+
* The service never throws for an expected failure: every step returns a
|
|
523
|
+
* {@link Result}, and the failures are folded into a typed
|
|
524
|
+
* {@link PolicyEvaluationError} so callers branch on `isErr()` rather than
|
|
525
|
+
* catching. Telemetry is best-effort — a throwing reporter can never abort an
|
|
526
|
+
* evaluation — which keeps observability strictly side-band.
|
|
527
|
+
*/
|
|
527
528
|
declare class PolicyService {
|
|
528
529
|
#private;
|
|
529
530
|
private readonly catalog;
|
|
@@ -535,8 +536,23 @@ declare class PolicyService {
|
|
|
535
536
|
private readonly options;
|
|
536
537
|
constructor(params: PolicyServiceParams);
|
|
537
538
|
private resolveEvaluationNow;
|
|
539
|
+
/**
|
|
540
|
+
* Evaluates a policy and returns its decision.
|
|
541
|
+
*
|
|
542
|
+
* This is the single public entry point. It delegates the actual work to the
|
|
543
|
+
* private pipeline and wraps it with telemetry: a completed or failed event
|
|
544
|
+
* is reported either way, but reporting is guarded so it can never change the
|
|
545
|
+
* outcome. The returned {@link Result} is `ok` with a
|
|
546
|
+
* {@link PolicyEvaluationResult} on success, or `err` with a typed
|
|
547
|
+
* {@link PolicyEvaluationError} describing which stage rejected the input —
|
|
548
|
+
* the method itself does not throw for expected failures.
|
|
549
|
+
*
|
|
550
|
+
* @param input - The policy key, scope chain, context version, and seed to
|
|
551
|
+
* evaluate, plus the caller's `decisionId`.
|
|
552
|
+
* @returns A `Result` carrying the decision or the evaluation error.
|
|
553
|
+
*/
|
|
538
554
|
evaluate(input: EvaluateInput): Promise<Result<PolicyEvaluationResult, PolicyEvaluationError>>;
|
|
539
555
|
}
|
|
540
556
|
//#endregion
|
|
541
|
-
export {
|
|
557
|
+
export { PolicyScope as A, BasePolicyDefinitionProps as C, PolicyDefinition as D, GatePolicyDefinitionProps as E, PolicyKey as F, AsOfSource as I, PolicyKind as L, ScopeChain as M, PolicyCatalog as N, PolicyDefinitionProps as O, PolicyCatalogEntryProps as P, PolicyOwner as R, PolicyDefinitionRepository as S, FindCandidatesParams as T, ContextResolverResilienceOptions as _, PolicyServiceOptions as a, ContextSeed as b, PolicyEvaluationErrors as c, PolicyAsOfResolver as d, PolicyContextBuilder as f, ContextResolverCircuitBreakerOptions as g, registerNamespacedContextResolversIn as h, PolicyService as i, PolicyScopeMatcher as j, PolicyDefinitionStatus as k, PolicyResolver as l, ContextResolverRegistry as m, PolicyDecision as n, PolicyServiceParams as o, PolicyContextBuilderOptions as p, PolicyEvaluationResult as r, PolicyEvaluationError as s, EvaluateInput as t, DeriveAsOfOptions as u, ContextResolverRetryOptions as v, ComputePolicyDefinitionProps as w, ContextSeedValidator as x, ContextValueResolver as y, PolicyScopeLevel as z };
|
|
542
558
|
//# sourceMappingURL=policy-service.d.ts.map
|
package/dist/policy-service.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { n as UnexpectedError, t as ValidationCode } from "./validation-code.js";
|
|
2
|
+
import { n as sha256Hex, r as stableStringify } from "./hashing.js";
|
|
2
3
|
import { t as ValidationField } from "./validation-field.js";
|
|
3
|
-
import {
|
|
4
|
+
import { i as InvariantViolationException, r as isValidDate } from "./temporal-guards.js";
|
|
5
|
+
import { t as InvalidValueException } from "./validation-exception.js";
|
|
6
|
+
import { f as SilentPolicyReporter, l as Result, o as PolicyContextPath } from "./gate-v1-payload.schema.js";
|
|
4
7
|
//#region src/core/policies/utils/hash.ts
|
|
5
8
|
/**
|
|
6
9
|
* Produces a deterministic SHA-256 hex digest of a canonical JSON representation.
|
|
@@ -49,20 +52,6 @@ var PolicyHashing = class PolicyHashing {
|
|
|
49
52
|
}
|
|
50
53
|
};
|
|
51
54
|
//#endregion
|
|
52
|
-
//#region src/core/exceptions/validation-exception.ts
|
|
53
|
-
var ValidationException = class extends DomainException {
|
|
54
|
-
constructor(field, code, message) {
|
|
55
|
-
super(message);
|
|
56
|
-
this.field = field;
|
|
57
|
-
this.code = code;
|
|
58
|
-
}
|
|
59
|
-
};
|
|
60
|
-
var InvalidValueException = class extends ValidationException {
|
|
61
|
-
constructor(field, code, message) {
|
|
62
|
-
super(field, code, message);
|
|
63
|
-
}
|
|
64
|
-
};
|
|
65
|
-
//#endregion
|
|
66
55
|
//#region src/core/policies/policy-ids.ts
|
|
67
56
|
const POLICY_DEFINITION_ID_FIELD = ValidationField.of("policyDefinitionId");
|
|
68
57
|
const POLICY_DECISION_ID_FIELD = ValidationField.of("policyDecisionId");
|
|
@@ -191,6 +180,15 @@ var PolicyCatalogEntry = class PolicyCatalogEntry {
|
|
|
191
180
|
* The catalog defines the universe of policies; the database cannot invent new ones.
|
|
192
181
|
*/
|
|
193
182
|
var PolicyCatalog = class {
|
|
183
|
+
/**
|
|
184
|
+
* Indexes the given entries up front into two lookups — one per exact
|
|
185
|
+
* variant, one per policy key (the "family") — and validates them eagerly:
|
|
186
|
+
* a duplicate variant or an internally inconsistent family throws here, at
|
|
187
|
+
* wiring time, so a malformed catalog fails fast at startup rather than on
|
|
188
|
+
* the first evaluation.
|
|
189
|
+
*
|
|
190
|
+
* @throws {UnexpectedError} When two entries resolve to the same variant key.
|
|
191
|
+
*/
|
|
194
192
|
constructor(entries) {
|
|
195
193
|
const families = /* @__PURE__ */ new Map();
|
|
196
194
|
const versionedMap = /* @__PURE__ */ new Map();
|
|
@@ -208,6 +206,14 @@ var PolicyCatalog = class {
|
|
|
208
206
|
this.versionedEntries = versionedMap;
|
|
209
207
|
this.entriesByKey = families;
|
|
210
208
|
}
|
|
209
|
+
/**
|
|
210
|
+
* Returns the single entry for a key. Deliberately strict: if the key has
|
|
211
|
+
* more than one variant it errs rather than guessing, steering the caller to
|
|
212
|
+
* {@link getVersioned} or {@link getFamily}. Use this only when a key is
|
|
213
|
+
* known to be unambiguous.
|
|
214
|
+
*
|
|
215
|
+
* @returns `ok` with the entry, or `err` when the key is unknown or ambiguous.
|
|
216
|
+
*/
|
|
211
217
|
get(key) {
|
|
212
218
|
const familyResult = this.getFamily(key);
|
|
213
219
|
if (familyResult.isErr()) return Result.err(familyResult.errorOrNull());
|
|
@@ -217,11 +223,28 @@ var PolicyCatalog = class {
|
|
|
217
223
|
if (!entry) return Result.err(`Policy not found in catalog: "${key}"`);
|
|
218
224
|
return Result.ok(entry);
|
|
219
225
|
}
|
|
226
|
+
/**
|
|
227
|
+
* Returns every variant registered under a key — the whole "family". This is
|
|
228
|
+
* the lookup the evaluation pipeline uses, since a single key may host
|
|
229
|
+
* several engine/schema variants that the resolver then chooses between.
|
|
230
|
+
*
|
|
231
|
+
* @returns `ok` with the family (always non-empty), or `err` when the key is unknown.
|
|
232
|
+
*/
|
|
220
233
|
getFamily(key) {
|
|
221
234
|
const family = this.entriesByKey.get(key);
|
|
222
235
|
if (!family) return Result.err(`Policy not found in catalog: "${key}"`);
|
|
223
236
|
return Result.ok(family);
|
|
224
237
|
}
|
|
238
|
+
/**
|
|
239
|
+
* Resolves the one entry matching an exact variant — kind plus engine and
|
|
240
|
+
* payload-schema versions. Falls back to the sole family member when a key
|
|
241
|
+
* has exactly one entry and that entry declares no explicit version
|
|
242
|
+
* selector, so unversioned single-variant policies "just work" without the
|
|
243
|
+
* caller spelling out versions.
|
|
244
|
+
*
|
|
245
|
+
* @param params - The key and the variant coordinates to match on.
|
|
246
|
+
* @returns `ok` with the matching entry, or `err` when no variant matches.
|
|
247
|
+
*/
|
|
225
248
|
getVersioned(params) {
|
|
226
249
|
const family = this.entriesByKey.get(params.key);
|
|
227
250
|
if (!family || family.length === 0) return Result.err(`Policy not found in catalog: "${params.key}"`);
|
|
@@ -236,9 +259,11 @@ var PolicyCatalog = class {
|
|
|
236
259
|
].filter((part) => part !== null).join(", ");
|
|
237
260
|
return Result.err(`Policy variant not found in catalog: "${params.key}" (${versionDetails})`);
|
|
238
261
|
}
|
|
262
|
+
/** Lists every registered variant across all keys — useful for introspection and diagnostics. */
|
|
239
263
|
list() {
|
|
240
264
|
return Array.from(this.versionedEntries.values());
|
|
241
265
|
}
|
|
266
|
+
/** Returns whether any variant is registered under the given key. */
|
|
242
267
|
has(key) {
|
|
243
268
|
return this.entriesByKey.has(key);
|
|
244
269
|
}
|
|
@@ -734,6 +759,16 @@ var PolicyAsOfResolver = class PolicyAsOfResolver {
|
|
|
734
759
|
* 5. policyVersion descending (semver — final deterministic tiebreaker)
|
|
735
760
|
*/
|
|
736
761
|
var PolicyResolver = class {
|
|
762
|
+
/**
|
|
763
|
+
* Picks the single winning definition from an already-filtered candidate
|
|
764
|
+
* list by applying the class's ordering criteria in priority order. The sort
|
|
765
|
+
* runs over a copy, so the caller's array is left untouched, and the final
|
|
766
|
+
* semver tiebreaker guarantees the winner is fully deterministic — it never
|
|
767
|
+
* depends on the order the repository returned the candidates in.
|
|
768
|
+
*
|
|
769
|
+
* @param candidates - Definitions already filtered to match key, scope, and as-of.
|
|
770
|
+
* @returns `ok` with the highest-ranked definition, or `err` when the list is empty.
|
|
771
|
+
*/
|
|
737
772
|
resolveBest(candidates) {
|
|
738
773
|
if (candidates.length === 0) return Result.err("No matching policy definition found (candidates list is empty)");
|
|
739
774
|
const sorted = [...candidates].sort((a, b) => {
|
|
@@ -815,6 +850,18 @@ var PolicyEvaluationErrors = class {
|
|
|
815
850
|
};
|
|
816
851
|
//#endregion
|
|
817
852
|
//#region src/core/policies/service/policy-service.ts
|
|
853
|
+
/**
|
|
854
|
+
* Orchestrates the end-to-end evaluation of a policy: it parses and validates
|
|
855
|
+
* the context seed, derives the as-of instant, resolves the best matching
|
|
856
|
+
* definition for the requested key/scope/version, builds the context the policy
|
|
857
|
+
* needs, and runs it through the right engine (gate or compute).
|
|
858
|
+
*
|
|
859
|
+
* The service never throws for an expected failure: every step returns a
|
|
860
|
+
* {@link Result}, and the failures are folded into a typed
|
|
861
|
+
* {@link PolicyEvaluationError} so callers branch on `isErr()` rather than
|
|
862
|
+
* catching. Telemetry is best-effort — a throwing reporter can never abort an
|
|
863
|
+
* evaluation — which keeps observability strictly side-band.
|
|
864
|
+
*/
|
|
818
865
|
var PolicyService = class {
|
|
819
866
|
#reporter;
|
|
820
867
|
constructor(params) {
|
|
@@ -838,6 +885,21 @@ var PolicyService = class {
|
|
|
838
885
|
if (!isValidDate(candidate)) return Result.err("seed.fields.now must be a valid Date when provided");
|
|
839
886
|
return Result.ok(candidate);
|
|
840
887
|
}
|
|
888
|
+
/**
|
|
889
|
+
* Evaluates a policy and returns its decision.
|
|
890
|
+
*
|
|
891
|
+
* This is the single public entry point. It delegates the actual work to the
|
|
892
|
+
* private pipeline and wraps it with telemetry: a completed or failed event
|
|
893
|
+
* is reported either way, but reporting is guarded so it can never change the
|
|
894
|
+
* outcome. The returned {@link Result} is `ok` with a
|
|
895
|
+
* {@link PolicyEvaluationResult} on success, or `err` with a typed
|
|
896
|
+
* {@link PolicyEvaluationError} describing which stage rejected the input —
|
|
897
|
+
* the method itself does not throw for expected failures.
|
|
898
|
+
*
|
|
899
|
+
* @param input - The policy key, scope chain, context version, and seed to
|
|
900
|
+
* evaluate, plus the caller's `decisionId`.
|
|
901
|
+
* @returns A `Result` carrying the decision or the evaluation error.
|
|
902
|
+
*/
|
|
841
903
|
async evaluate(input) {
|
|
842
904
|
const result = await this.#evaluate(input);
|
|
843
905
|
if (result.isErr()) {
|