@classytic/party 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,65 @@
3
3
  All notable changes to `@classytic/party` are documented here. Adheres to
4
4
  [Keep a Changelog](https://keepachangelog.com/) and [SemVer](https://semver.org/).
5
5
 
6
+ ## [0.2.0] — 2026-08-17
7
+
8
+ ### Added
9
+ - `defineParty(shape)` / `blueprint.bind(connection, runtime)` — describe/bind construction API replacing `createParty`; describe phase is pure and safe at import time
10
+ - `SubjectRef` / `PARTY_MODEL_NAME` exported from `./domain/subject` — canonical polymorphic subject seam for cross-kernel refs (`@classytic/access`, `@classytic/muster`, etc.)
11
+ - `src/maintenance/phone-identity-repair.ts` — repair utility for phone-identity drift
12
+
13
+ ### Removed
14
+ - `createParty` (replaced by `defineParty`)
15
+
16
+ ## 1.0.0 - 2026-08-07 (unpublished)
17
+
18
+ ### Requirements
19
+
20
+ - **New peer dependency: `@classytic/contact >=0.1.0`** — install it alongside
21
+ this package. It owns contact-point canonicalisation (E.164 phones, emails),
22
+ which `resolveOrCreate` / `syncIdentity` derive their `identityKeys` from.
23
+ A PEER rather than a regular dependency on purpose: npm may nest a regular one
24
+ under `party/node_modules`, giving the host a second module instance, and then
25
+ `err instanceof PhoneFormatError` is `false` at a host catch site that imported
26
+ its own copy — a guard that compiles, runs, and matches nothing.
27
+
28
+ ### Breaking
29
+
30
+ - **Contact input that cannot be canonicalised now THROWS** instead of becoming
31
+ an identity key. A phone that is not a valid, dialable number raises
32
+ `PartyPhoneFormatError` (400 / `party.phone_invalid`); an unparseable email
33
+ raises `EmailFormatError`. `0.1.0` degraded both — a rejected phone became
34
+ `+${digits}`, producing keys like `phone:+01309000993` that are neither a
35
+ national number nor E.164, so two spellings of one person keyed differently and
36
+ the identity forked silently. There is no lenient variant.
37
+ - **Phone canonicalisation requires a region for national-format input.**
38
+ `PartyShape.defaultPhoneRegion` (ISO 3166-1 alpha-2, supplied by the
39
+ deployment — this kernel names no country). Unset ⇒ E.164-only input is
40
+ accepted and a national format is refused rather than guessed at. An explicit
41
+ `+…` number is never re-read through the region.
42
+ - **`ContactPoint.value` stores the canonical E.164**, with the caller's original
43
+ spelling preserved as `ContactPoint.input` (never keyed, never matched on).
44
+
45
+ ### Changed
46
+
47
+ - Added `PartyShape.identityContactTypes` (`email | phone`, both by default).
48
+ A host can exclude shared contact channels from automatic identity resolution
49
+ while retaining normalized contact points. Tax identifiers and external
50
+ references remain authoritative. Existing defaults are unchanged.
51
+ - The unique `identityKeys` index now explicitly includes only non-empty arrays.
52
+ This permits multiple name-only or contact-only parties when contact channels
53
+ are non-authoritative, without weakening uniqueness for real identity keys.
54
+ - Canonical email output is unchanged for every valid address — trim plus
55
+ lowercase, and still **no** Gmail-dot or `+tag` folding, since folding a
56
+ provider's rule merges two distinct inboxes into one identity. No existing key
57
+ is re-derived.
58
+ - `libphonenumber-js` is no longer a direct dependency.
59
+
60
+ ## 0.1.1 - 2026-07-22 (unpublished)
61
+
62
+ ### Changed
63
+ - Internal: `InProcess*Bus` is now a thin subclass of `@classytic/primitives/event-infra`'s shared `InProcessEventBus` (dedup / error-isolation / close semantics unchanged); `MemoryOutboxStore` re-exported from the shared runtime where applicable. No public API, event-name, or wire-envelope change. Requires `@classytic/primitives >=0.13.0`.
64
+
6
65
  ## [0.1.0] - 2026-07-13
7
66
 
8
67
  Initial release — the Party / Business-Partner identity kernel.
package/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2026 Classytic
3
+ Copyright (c) 2026 Classytic LLC
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -48,12 +48,12 @@ Peers: `@classytic/mongokit >=3.20`, `@classytic/primitives >=0.9.1`,
48
48
 
49
49
  ```ts
50
50
  import mongoose from 'mongoose';
51
- import { createParty } from '@classytic/party';
51
+ import { defineParty } from '@classytic/party';
52
52
 
53
- const party = await createParty({
54
- connection: mongoose.connection,
53
+ const party = defineParty({
55
54
  multiTenant: false, // identity is company-wide here; pass true for per-tenant SaaS
56
- });
55
+ defaultPhoneRegion: 'BD',
56
+ }).bind(mongoose.connection);
57
57
 
58
58
  // Resolve-or-create by ANY identity signal — race-safe, dedup by email/phone/vat.
59
59
  const { party: ada, created } = await party.port.resolveOrCreate(
@@ -76,6 +76,27 @@ const snap = await party.port.snapshot(String(ada._id), {});
76
76
  await party.repositories.party.merge(dupId, survivorId, {});
77
77
  ```
78
78
 
79
+ ### Contacts are not always proof of identity
80
+
81
+ Email and phone are authoritative identity keys by default, preserving the
82
+ race-safe behavior existing commerce hosts rely on. Domains where people often
83
+ share contact details can keep those values as contact data without allowing
84
+ them to collapse two parties:
85
+
86
+ ```ts
87
+ const party = defineParty({
88
+ multiTenant: true,
89
+ defaultPhoneRegion: 'GB',
90
+ identityContactTypes: [],
91
+ }).bind(mongoose.connection);
92
+ ```
93
+
94
+ Tax identifiers and explicit external references remain authoritative. A
95
+ domain-specific facet (for example a legal Client) can perform candidate-match
96
+ review and then attach its stable external reference or explicitly merge the
97
+ confirmed duplicate. This setting shapes persisted `identityKeys`; migrate
98
+ existing rows before changing it on a live deployment.
99
+
79
100
  ## The `PartyPort` — the reusable contract
80
101
 
81
102
  Consuming domains depend on `engine.port` (a `PartyPort`), not the models:
@@ -111,3 +132,7 @@ entities — that's the duplication this kernel exists to remove.
111
132
  ## License
112
133
 
113
134
  MIT
135
+
136
+ ## Trademark
137
+
138
+ Code is MIT-licensed. "Classytic"/"arc" names + logos are trademarks of Classytic LLC — see [TRADEMARK.md](TRADEMARK.md).
package/dist/events.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { z } from "zod";
2
2
  import { DomainEvent, EventHandler, EventMeta, EventTransport } from "@classytic/primitives/events";
3
+ import { InProcessEventBus } from "@classytic/primitives/event-infra";
3
4
  import { Types } from "mongoose";
4
5
  import { OperationContext } from "@classytic/primitives/context";
5
6
  import "@classytic/primitives/reference";
@@ -192,16 +193,10 @@ interface PartyLogger {
192
193
  declare function createEvent<T>(type: string, payload: T, ctx?: PartyContext, meta?: Partial<EventMeta>): DomainEvent<T>;
193
194
  //#endregion
194
195
  //#region src/events/in-process-bus.d.ts
195
- declare class InProcessPartyBus implements EventTransport {
196
- readonly name = "in-process-party";
197
- private subs;
198
- private readonly logger;
196
+ declare class InProcessPartyBus extends InProcessEventBus {
199
197
  constructor(options?: {
200
- logger?: PartyLogger | undefined;
198
+ logger?: PartyLogger;
201
199
  });
202
- publish(event: DomainEvent): Promise<void>;
203
- subscribe(pattern: string, handler: EventHandler): Promise<() => void>;
204
- close(): Promise<void>;
205
200
  }
206
201
  //#endregion
207
202
  //#region src/events/dispatch.d.ts
package/dist/events.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { z } from "zod";
2
- import { createEvent as createEvent$1, matchEventPattern } from "@classytic/primitives/events";
2
+ import { createEvent as createEvent$1 } from "@classytic/primitives/events";
3
+ import { InProcessEventBus } from "@classytic/primitives/event-infra";
3
4
  //#region src/events/event-constants.ts
4
5
  /** PACKAGE_RULES §12: `package:resource.verb`. Emit ONLY these catalog names. */
5
6
  const PARTY_EVENTS = {
@@ -158,37 +159,24 @@ function createEvent(type, payload, ctx, meta) {
158
159
  //#region src/events/in-process-bus.ts
159
160
  /**
160
161
  * In-process event bus — default fallback when no `eventTransport` is given to
161
- * `createParty`. Structurally identical to arc's `MemoryEventTransport`;
162
- * pattern-matching delegates to primitives' `matchEventPattern` (PACKAGE_RULES
163
- * §13). Handler errors are swallowed-and-logged — emission never blocks writes.
162
+ * `createParty`.
163
+ *
164
+ * A thin subclass of `@classytic/primitives/event-infra`'s shared
165
+ * `InProcessEventBus`. Prior semantics preserved: glob matching via
166
+ * `matchEventPattern`, per-handler error isolation, idempotent `close()`.
167
+ * Party historically SWALLOWED handler errors when no logger was supplied —
168
+ * that default is preserved by passing `logger: null` unless the host injects
169
+ * a `PartyLogger`, whose structured `error(msg, meta)` slot is bridged onto
170
+ * the shared error-logger contract.
164
171
  */
165
- var InProcessPartyBus = class {
166
- name = "in-process-party";
167
- subs = [];
168
- logger;
172
+ var InProcessPartyBus = class extends InProcessEventBus {
169
173
  constructor(options) {
170
- this.logger = options?.logger;
171
- }
172
- async publish(event) {
173
- const matching = this.subs.filter((s) => matchEventPattern(s.pattern, event.type));
174
- for (const sub of matching) try {
175
- await sub.handler(event);
176
- } catch (err) {
177
- this.logger?.error?.(`[party] event handler error [${event.type}]`, { error: err instanceof Error ? err.message : String(err) });
178
- }
179
- }
180
- async subscribe(pattern, handler) {
181
- const sub = {
182
- pattern,
183
- handler
184
- };
185
- this.subs.push(sub);
186
- return () => {
187
- this.subs = this.subs.filter((s) => s !== sub);
188
- };
189
- }
190
- async close() {
191
- this.subs = [];
174
+ const partyLogger = options?.logger;
175
+ super({
176
+ name: "in-process-party",
177
+ logLabel: "party",
178
+ logger: partyLogger?.error ? { error: (message, ...args) => partyLogger.error?.(message, { args }) } : null
179
+ });
192
180
  }
193
181
  };
194
182
  //#endregion
@@ -211,7 +199,9 @@ var PartyEventDispatcher = class {
211
199
  return this.transport.publish(event);
212
200
  }
213
201
  subscribe(pattern, handler) {
214
- return this.transport.subscribe(pattern, handler);
202
+ const subscribe = this.transport.subscribe?.bind(this.transport);
203
+ if (!subscribe) throw new Error("party: configured event transport does not support subscribe");
204
+ return subscribe(pattern, handler);
215
205
  }
216
206
  /**
217
207
  * Route a domain event with full §P8 discipline. Reads the pending queue and