@classytic/repo-core 0.23.0 → 0.24.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.
@@ -85,10 +85,27 @@ interface CleanupStepExecuteContext extends CleanupStepContext {
85
85
  * One preview line — maps 1:1 onto a host plan item. A business record class
86
86
  * (`'sales facts'`, `'journal entries'`), never a collection name.
87
87
  */
88
+ /**
89
+ * What a step's `estimated` COUNTS. Absent ⇒ `'remove'`.
90
+ *
91
+ * `destructive: false` is NOT a substitute: it is true of both a protective
92
+ * guard (counts records it defends) and a projection rebuild (counts records it
93
+ * recomputes), and those mean opposite things in a "records to remove" headline.
94
+ * A guard reporting 173 protected journal entries once pushed that headline to
95
+ * 540 on a plan that removed 367 — a plausible, internally consistent, wrong
96
+ * number shown at the exact moment an operator authorises destruction.
97
+ */
98
+ type CleanupStepDisposition = 'remove' | 'protect' | 'rebuild';
88
99
  interface CleanupStepEstimate {
89
100
  readonly resource: string;
90
101
  /** Estimated records this step would affect. */
91
102
  readonly estimated: number;
103
+ /**
104
+ * Whether `estimated` counts records REMOVED, PROTECTED, or REBUILT.
105
+ * Defaults to `'remove'`, so every existing purge step is unchanged and only
106
+ * a step that means something else has to say so.
107
+ */
108
+ readonly disposition?: CleanupStepDisposition | undefined;
92
109
  /** What this step RETAINS (e.g. `'measures kept, PII redacted'`). */
93
110
  readonly retained?: string | undefined;
94
111
  /**
@@ -147,6 +164,22 @@ interface CleanupStep {
147
164
  * recipe is destructive iff ANY of its steps is.
148
165
  */
149
166
  readonly destructive: boolean;
167
+ /**
168
+ * What this step IS, declared once — not merely what one estimate counted.
169
+ *
170
+ * Needed at the STEP because a caller has to know a step is protective
171
+ * WITHOUT running it. A host resolving an operator's exclusion list must
172
+ * refuse to switch a guard off, and asking `estimate()` to find out would mean
173
+ * running the counting queries for a line that is being taken out — and
174
+ * surfacing that line's blockers, so excluding a domain could still be refused
175
+ * because of it.
176
+ *
177
+ * `destructive: false` is not the same question: it is true of a guard AND of
178
+ * a projection rebuild, and a rebuild is perfectly reasonable to exclude.
179
+ *
180
+ * An estimate may restate it; absent everywhere ⇒ `'remove'`.
181
+ */
182
+ readonly disposition?: CleanupStepDisposition | undefined;
150
183
  /**
151
184
  * Projection / scaffolding rebuilds this step performs AFTER its cleanup —
152
185
  * surfaced in the preview's `rebuildActions` (e.g. `'rebuild sales rollup'`).
@@ -164,4 +197,4 @@ interface CleanupStep {
164
197
  verify?(ctx: CleanupStepContext): Promise<readonly CleanupStepCheck[]>;
165
198
  }
166
199
  //#endregion
167
- export { CleanupStep, CleanupStepCheck, CleanupStepContext, CleanupStepEstimate, CleanupStepExecuteContext, CleanupStepLogger, CleanupStepOutcome, CleanupStepProgress };
200
+ export { CleanupStep, CleanupStepCheck, CleanupStepContext, CleanupStepDisposition, CleanupStepEstimate, CleanupStepExecuteContext, CleanupStepLogger, CleanupStepOutcome, CleanupStepProgress };
@@ -1,3 +1,3 @@
1
1
  import { ResolvedTenantConfig, TenantConfig, TenantFieldType, TenantStrategy } from "./types.mjs";
2
- import { DEFAULT_TENANT_CONFIG, resolveTenantConfig, resolveTenantField } from "./resolve.mjs";
3
- export { DEFAULT_TENANT_CONFIG, type ResolvedTenantConfig, type TenantConfig, type TenantFieldType, type TenantStrategy, resolveTenantConfig, resolveTenantField };
2
+ import { DEFAULT_TENANT_CONFIG, assertNoLegacyTenantKeys, resolveTenantConfig, resolveTenantField } from "./resolve.mjs";
3
+ export { DEFAULT_TENANT_CONFIG, type ResolvedTenantConfig, type TenantConfig, type TenantFieldType, type TenantStrategy, assertNoLegacyTenantKeys, resolveTenantConfig, resolveTenantField };
@@ -1,2 +1,2 @@
1
- import { DEFAULT_TENANT_CONFIG, resolveTenantConfig, resolveTenantField } from "./resolve.mjs";
2
- export { DEFAULT_TENANT_CONFIG, resolveTenantConfig, resolveTenantField };
1
+ import { DEFAULT_TENANT_CONFIG, assertNoLegacyTenantKeys, resolveTenantConfig, resolveTenantField } from "./resolve.mjs";
2
+ export { DEFAULT_TENANT_CONFIG, assertNoLegacyTenantKeys, resolveTenantConfig, resolveTenantField };
@@ -28,5 +28,36 @@ declare function resolveTenantConfig(config?: TenantConfig | boolean): ResolvedT
28
28
  * scoping" and all return `false` — callers get one thing to branch on.
29
29
  */
30
30
  declare function resolveTenantField(config?: TenantConfig | boolean): string | false;
31
+ /**
32
+ * Refuse a config that still carries a pre-consolidation tenant key.
33
+ *
34
+ * ## Why this belongs in repo-core and not in each kernel
35
+ *
36
+ * Kernels are migrating from a per-package tenant shape (`multiTenant`, plus a
37
+ * sibling `tenantFieldType` in some) onto {@link TenantConfig} under the key
38
+ * `tenant`. `@classytic/ledger` has landed; catalog, order, cart, crm, flow,
39
+ * party, review, transfer and yard have adopted the TYPE but still read
40
+ * `multiTenant`. Each of those is a future rename.
41
+ *
42
+ * The rename itself is trivial. What is not trivial is the failure mode when a
43
+ * CALLER misses it, because every one of these resolvers reads
44
+ * `resolveTenantConfig(config.tenant ?? false)`: an absent `tenant` resolves to
45
+ * `strategy: 'none'` — no tenant field, no tenant filter, every read spanning
46
+ * ALL tenants, no error, and figures that look plausible. A host that asked for
47
+ * tenancy gets none, silently.
48
+ *
49
+ * Nine packages each remembering to hand-roll that check is nine chances to
50
+ * forget, and the one that forgets is the one that ships the leak. So the guard
51
+ * lives beside the resolver every one of them already calls.
52
+ *
53
+ * Additive and non-breaking: nothing calls it until a package renames.
54
+ *
55
+ * @param config the raw, unresolved shape as the host supplied it
56
+ * @param pkg package name for the message (e.g. `'defineOrder'`)
57
+ * @param extra additional legacy keys this package is retiring, as
58
+ * `[oldKey, newPath]` — pass `['tenantFieldType', 'tenant.fieldType']` when
59
+ * the package carried a sibling field-type option.
60
+ */
61
+ declare function assertNoLegacyTenantKeys(config: unknown, pkg: string, extra?: ReadonlyArray<readonly [string, string]>): void;
31
62
  //#endregion
32
- export { DEFAULT_TENANT_CONFIG, resolveTenantConfig, resolveTenantField };
63
+ export { DEFAULT_TENANT_CONFIG, assertNoLegacyTenantKeys, resolveTenantConfig, resolveTenantField };
@@ -71,5 +71,44 @@ function resolveTenantField(config) {
71
71
  if (!resolved.enabled || resolved.strategy === "none") return false;
72
72
  return resolved.tenantField;
73
73
  }
74
+ /**
75
+ * Refuse a config that still carries a pre-consolidation tenant key.
76
+ *
77
+ * ## Why this belongs in repo-core and not in each kernel
78
+ *
79
+ * Kernels are migrating from a per-package tenant shape (`multiTenant`, plus a
80
+ * sibling `tenantFieldType` in some) onto {@link TenantConfig} under the key
81
+ * `tenant`. `@classytic/ledger` has landed; catalog, order, cart, crm, flow,
82
+ * party, review, transfer and yard have adopted the TYPE but still read
83
+ * `multiTenant`. Each of those is a future rename.
84
+ *
85
+ * The rename itself is trivial. What is not trivial is the failure mode when a
86
+ * CALLER misses it, because every one of these resolvers reads
87
+ * `resolveTenantConfig(config.tenant ?? false)`: an absent `tenant` resolves to
88
+ * `strategy: 'none'` — no tenant field, no tenant filter, every read spanning
89
+ * ALL tenants, no error, and figures that look plausible. A host that asked for
90
+ * tenancy gets none, silently.
91
+ *
92
+ * Nine packages each remembering to hand-roll that check is nine chances to
93
+ * forget, and the one that forgets is the one that ships the leak. So the guard
94
+ * lives beside the resolver every one of them already calls.
95
+ *
96
+ * Additive and non-breaking: nothing calls it until a package renames.
97
+ *
98
+ * @param config the raw, unresolved shape as the host supplied it
99
+ * @param pkg package name for the message (e.g. `'defineOrder'`)
100
+ * @param extra additional legacy keys this package is retiring, as
101
+ * `[oldKey, newPath]` — pass `['tenantFieldType', 'tenant.fieldType']` when
102
+ * the package carried a sibling field-type option.
103
+ */
104
+ function assertNoLegacyTenantKeys(config, pkg, extra = []) {
105
+ if (config === null || typeof config !== "object") return;
106
+ const record = config;
107
+ const retired = [["multiTenant", "tenant"], ...extra];
108
+ for (const [key, became] of retired) {
109
+ if (record[key] === void 0) continue;
110
+ throw new Error(`${pkg}: \`${key}\` was renamed to \`${became}\`. It is REFUSED rather than ignored because ignoring it disables tenancy SILENTLY — no tenant field, no tenant filter, and every read spanning all tenants while returning plausible numbers. Move the value to \`tenant\` (\`tenant: false\` for a single-tenant deployment).`);
111
+ }
112
+ }
74
113
  //#endregion
75
- export { DEFAULT_TENANT_CONFIG, resolveTenantConfig, resolveTenantField };
114
+ export { DEFAULT_TENANT_CONFIG, assertNoLegacyTenantKeys, resolveTenantConfig, resolveTenantField };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@classytic/repo-core",
3
- "version": "0.23.0",
3
+ "version": "0.24.0",
4
4
  "description": "Driver-agnostic repository primitives: hooks, Filter IR, operations, pagination, cache contract. Foundation for mongokit, sqlitekit, pgkit, and prismakit. Lean by design — no plugins ship here; each kit owns its own.",
5
5
  "type": "module",
6
6
  "sideEffects": false,