@openlup/core 0.15.0 → 0.17.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/AGENTS.md CHANGED
@@ -89,6 +89,8 @@ The checker reports `OPENLUP_E_PORT_MISSING`, `OPENLUP_E_EVENT_UNHANDLED`,
89
89
  its exact candidate. A host must provide complete installed metadata and selected-adapter
90
90
  observations; a report does not prove ACL, function bodies or recovery.
91
91
 
92
+ See [module readiness](README.md#module-readiness) for logical modules.
93
+
92
94
  ## Using this package in an application
93
95
 
94
96
  - **Do not edit the installed package.** Do not change files under `node_modules/@openlup/`, and
package/CHANGELOG.md CHANGED
@@ -8,6 +8,27 @@ SemVer promise.
8
8
 
9
9
  ## [Unreleased]
10
10
 
11
+ ## [0.17.0]
12
+
13
+ - Publishable on the npm `latest` dist-tag as `0.17.0`, from tag
14
+ `openlup-core-v0.17.0`.
15
+
16
+ - Republish unchanged APIs, runtime behavior and shipped SQL as part of the
17
+ complete `0.17.0` package set.
18
+
19
+ Baseline evidence (2026-10-20): published `0.16.0` tarball 161,232 packed bytes,
20
+ SHA256 `48abf4ea8ddc446b8d6e2c610801e2cfcbe5575743bd5a61fbb8b8b50a2b36a3`.
21
+ The initial `0.16.0` release-gates baseline is retained under S1 option B.
22
+
23
+ ## [0.16.0]
24
+
25
+ - Publishable on the npm `latest` dist-tag as `0.16.0`, from tag
26
+ `openlup-core-v0.16.0`.
27
+
28
+ - Add optional logical module selection, dependency/entry contracts, module-qualified bindings and module diagnostics to experimental `./readiness`. Each selected module retains an independent schema requirement within one artifact.
29
+
30
+ Migration: additive API; existing manifests and loaded packages that omit module fields keep byte-identical readiness reports and binding keys. To opt an artifact in, observe `LoadedPackage.modules` and supply exactly one manifest `module` per selected id, with its logical kind, provided entries and in-artifact dependencies. Bind ports, triggers and routes as `package/module:id`; keep artifact kind/version and environment keys unchanged. Mixed modes, missing dependencies/entries and duplicate module/event/schedule/route ownership refuse using existing codes. Empty selections need no contributions; disabled modules need no bindings or schema probe. Inspect optional issue `module` when diagnosing module/schema failures. No release, publication or activation is performed by this change.
31
+
11
32
  ## [0.15.0]
12
33
 
13
34
  - Publishable on the npm `latest` dist-tag as `0.15.0`, from tag
package/CONTRIBUTING.md CHANGED
@@ -17,7 +17,8 @@ npm run ci
17
17
 
18
18
  The gate builds every export, runs standalone tests and isolated package smoke,
19
19
  verifies declaration snapshots, and checks license, SBOM, audit, pack, and
20
- publish controls. Root Vitest excludes this suite; CI's required test job runs it separately.
20
+ publish controls. Root Vitest excludes this suite; CI's required `install-proof` job runs it separately through the shared package gate,
21
+ independently of the required root `test` job.
21
22
 
22
23
  ## Scope
23
24
 
package/README.md CHANGED
@@ -27,8 +27,8 @@ complete host inventory, actual schedule/route function bindings and a bounded
27
27
  schema probe. It never migrates, leases or sends. Only `ready` admits a candidate;
28
28
  `unknown` and `unsatisfied` retain the serving candidate. The host owns immutable
29
29
  artifact/configuration/schema identity and admission at every work entrance.
30
- Exactly one contribution per non-kernel package is required today; multiple
31
- capabilities per distribution need a future separately reviewed adaptation.
30
+ Legacy artifacts require one contribution per non-kernel package; selected logical
31
+ modules use [module readiness](#module-readiness).
32
32
 
33
33
  For PostgreSQL catalog inspection, import `buildGroupedPostgresSchemaProbe`,
34
34
  `decodeGroupedPostgresSchemaRows` and `PostgresSchemaCatalogEntry` from
@@ -76,8 +76,8 @@ standalone readiness suite. Actual installed-tarball SQL and shape-order proof
76
76
  runs in the existing Outbox disposable PostgreSQL tier in the source repository.
77
77
 
78
78
  Contributor package verification keeps the existing code/docs class and file
79
- ceilings. The grouped catalog addition uses the measured, owner-approved
80
- aggregate budgets in `release-gates.json`; later growth still needs its own
79
+ ceilings. Its aggregate budgets in `release-gates.json` follow the measured
80
+ decision its `$budgetReason` records; later growth still needs its own
81
81
  measured budget decision. Passing a local pack does not authorize publication.
82
82
 
83
83
  ## Package Surface Maturity
@@ -196,9 +196,8 @@ The repository checker also calls the UI neutrality counting process with
196
196
  `--counts-json` to count its existing patterns over the supplied sources. Running that checker without
197
197
  arguments still scans the UI source directory and refuses any forbidden hit.
198
198
  Published Tree CI runs the repository ratchet in required `self-check` and its
199
- CLI refusal tests separately in required `test`; unrestricted root diagnostics
200
- do not replace either gate. The independent core CI step retains this package's
201
- complete verification when root diagnostics are red.
199
+ CLI refusal tests in required root `test`. This package's independent proof
200
+ belongs to `install-proof`; see [repository testing](https://github.com/openlup/openlup/blob/main/CONTRIBUTING.md#test-scope-and-diagnostics).
202
201
 
203
202
  Together these interfaces let the repository measure existing findings without
204
203
  making one package import another package's internals. They provide source
@@ -206,9 +205,27 @@ neutrality evidence, not package activation or consumer compatibility evidence.
206
205
 
207
206
  ## Kernel Documentation
208
207
 
209
- <!-- openlup-doc-impact {"unit":"core","digest":"sha256-8e51804713ed34cae5286093c8036e24b5cf502a4e20bec243ce3a1481382b02","reason":"The manifest and own lockfile move from 0.14.0 to 0.15.0 for the set that first publishes @openlup/platform. API, runtime, dependency resolutions and package gates are unchanged in this preparation delta."} -->
210
-
211
208
  - [`docs/SUBSCRIPTION_ENGINE.md`](docs/SUBSCRIPTION_ENGINE.md) — what the
212
209
  `./subscription` kernel owns, its deterministic-clock contract, the
213
210
  late-payment cycle-shift rule and its monotonic clamp, and how a host
214
211
  application consumes it.
212
+
213
+ ### Module readiness
214
+
215
+ Selected `./readiness` modules retain independent schema requirements.
216
+ Dependencies use the same or an earlier layer:
217
+ `kernel`, `rail`, `capability`, `implementation`.
218
+ See [CHANGELOG migration guidance](CHANGELOG.md#0160) for selection,
219
+ bindings, refusals and diagnostics, and the source checkout's
220
+ `api/readiness.api.md` snapshot for declarations; the host owns inventory.
221
+
222
+ ## Package artifact limits
223
+
224
+ The monorepo's package artifact checker enforces this package's
225
+ `release-gates.json` caps and packed content allowlist. Only
226
+ `--release-set <version>` checks compressed growth against the last released
227
+ baseline, with explanations in the new set's changelog section and release body. See the
228
+ [package artifact policy](https://github.com/openlup/openlup/blob/main/.github/VERSIONING_AND_EOL.md#package-artifact-limits)
229
+ for measured sizes, current caps, the S1 baseline refresh and refusal remedies.
230
+ Artifact checks require a clean package directory; `--manifest-only` supplies no
231
+ artifact evidence.
@@ -11,6 +11,7 @@ export interface ReadinessIssue {
11
11
  subject: string;
12
12
  fix: string;
13
13
  observation?: string;
14
+ module?: string;
14
15
  }
15
16
  /** @beta */
16
17
  export interface SchemaObject {
@@ -38,6 +39,16 @@ export interface PackageManifest {
38
39
  routes: readonly string[];
39
40
  requiredPorts: readonly string[];
40
41
  env: readonly string[];
42
+ /** Selected logical module within this artifact; kind above remains the artifact kind. */
43
+ module?: {
44
+ id: string;
45
+ kind: PackageManifest["kind"];
46
+ provides: readonly string[];
47
+ dependsOn: readonly {
48
+ module: string;
49
+ entries: readonly string[];
50
+ }[];
51
+ };
41
52
  requiredSchema?: RequiredSchema;
42
53
  }
43
54
  /** @beta Run-bound handlers are represented by their shared inert descriptors. */
@@ -59,6 +70,7 @@ export interface LoadedPackage {
59
70
  name: string;
60
71
  version: string;
61
72
  kind: PackageManifest["kind"];
73
+ modules?: readonly string[];
62
74
  }
63
75
  /** @beta Candidate identity must change when configuration, schema or artifact changes. */
64
76
  export interface CandidateIdentity {
@@ -27,6 +27,20 @@ export declare const manifestShape: z.ZodObject<{
27
27
  routes: z.ZodArray<z.ZodString>;
28
28
  requiredPorts: z.ZodArray<z.ZodString>;
29
29
  env: z.ZodArray<z.ZodString>;
30
+ module: z.ZodOptional<z.ZodObject<{
31
+ id: z.ZodString;
32
+ kind: z.ZodEnum<{
33
+ kernel: "kernel";
34
+ rail: "rail";
35
+ capability: "capability";
36
+ implementation: "implementation";
37
+ }>;
38
+ provides: z.ZodArray<z.ZodString>;
39
+ dependsOn: z.ZodArray<z.ZodObject<{
40
+ module: z.ZodString;
41
+ entries: z.ZodArray<z.ZodString>;
42
+ }, z.core.$strict>>;
43
+ }, z.core.$strict>>;
30
44
  requiredSchema: z.ZodOptional<z.ZodObject<{
31
45
  version: z.ZodString;
32
46
  migration: z.ZodString;
@@ -2,12 +2,18 @@ import { z } from "zod";
2
2
  const name = z.string().min(1).max(240).regex(/^[a-zA-Z0-9@_./:-]+$/);
3
3
  const version = z.string().regex(/^0\.\d+\.\d+$/);
4
4
  const list = z.array(name).refine((items) => new Set(items).size === items.length);
5
+ const moduleId = z.string().max(240).regex(/^[a-z0-9][a-z0-9-]*$/);
6
+ const modules = z.array(moduleId).refine((items) => new Set(items).size === items.length);
7
+ const entries = z.array(z.string().max(240).regex(/^(?:\.|\.(?:\/[a-zA-Z0-9_-]+)+)$/)).refine((items) => new Set(items).size === items.length);
5
8
  const kind = z.enum(["kernel", "rail", "capability", "implementation"]);
6
9
  export const schemaObjectShape = z.object({ kind: z.enum(["table", "column", "function"]), name,
7
10
  signature: z.string().max(1000).optional() }).strict().refine((o) => o.name.split(".").length === (o.kind === "column" ? 3 : 2) && (o.kind !== "function" || o.signature !== undefined));
8
11
  export const manifestShape = z.object({ name: name.regex(/^@openlup\/[a-z0-9-]+$/), version, kind,
9
12
  emits: list, handles: list, schedules: z.array(z.object({ id: name, cadenceSeconds: z.number().finite().positive() }).strict()),
10
13
  routes: list, requiredPorts: list, env: list,
14
+ module: z.object({ id: moduleId, kind, provides: entries,
15
+ dependsOn: z.array(z.object({ module: moduleId, entries }).strict()).refine((items) => new Set(items.map((item) => item.module)).size === items.length),
16
+ }).strict().optional(),
11
17
  requiredSchema: z.object({ version: name, migration: name, objects: z.array(schemaObjectShape) }).strict().optional(),
12
18
  }).strict();
13
19
  /** Validate declarations without invoking a handler, schedule, route or probe. */
@@ -27,13 +33,14 @@ export function declarationIssues(input) {
27
33
  return issues;
28
34
  }
29
35
  for (const pkg of input.loadedPackages)
30
- if (!z.object({ name, version, kind }).strict().safeParse(pkg).success)
36
+ if (!z.object({ name, version, kind, modules: modules.optional() }).strict().safeParse(pkg).success)
31
37
  issue("loaded-package", "Supply a valid observed package name, version and kind.");
32
38
  for (const c of input.contributions) {
33
39
  if (!manifestShape.safeParse(c?.manifest).success) {
34
40
  issue("manifest", "Supply a valid complete manifest with unique declarations.");
35
41
  continue;
36
42
  }
43
+ const start = issues.length;
37
44
  if (!Array.isArray(c.handlers) || c.handlers.some((h) => !name.safeParse(h?.eventType).success || !Number.isFinite(h?.timeoutMs) || h?.timeoutMs <= 0))
38
45
  issue(c.manifest.name, "Supply valid shared handler descriptors.", "OPENLUP_E_EVENT_UNHANDLED");
39
46
  if (!Array.isArray(c.schedules) || c.schedules.some((s) => !name.safeParse(s?.id).success || !Number.isFinite(s?.cadenceSeconds) || s?.cadenceSeconds <= 0 || typeof s?.run !== "function"))
@@ -43,6 +50,9 @@ export function declarationIssues(input) {
43
50
  const objects = c.manifest.requiredSchema?.objects ?? [];
44
51
  if (new Set(objects.map((o) => `${o.kind}:${o.name}:${o.signature ?? ""}`)).size !== objects.length)
45
52
  issue(c.manifest.name, "Remove duplicate schema requirements.", "OPENLUP_E_SCHEMA_BEHIND");
53
+ if (c.manifest.module)
54
+ for (const diagnostic of issues.slice(start))
55
+ diagnostic.module = c.manifest.module.id;
46
56
  }
47
57
  if (!Array.isArray(input.vocabulary) || input.vocabulary.some((v) => !name.safeParse(v?.eventType).success || !name.safeParse(v?.owner).success))
48
58
  issue("vocabulary", "Supply explicit vocabulary entries with owners.", "OPENLUP_E_EVENT_UNHANDLED");
@@ -6,9 +6,10 @@ export { schemaObjectKey, buildSchemaObjectProbe, createSchemaProbe, buildGroupe
6
6
  export async function checkReadiness(input) {
7
7
  const issues = declarationIssues(input);
8
8
  let unknown = issues.some((i) => ["input", "candidate", "inventory", "loaded-package", "schemaProbe", "probeTimeoutMs"].includes(i.subject));
9
- const add = (code, pkg, subject, fix, observation) => {
10
- issues.push({ code, package: pkg, subject, fix, ...(observation ? { observation } : {}) });
9
+ const add = (code, pkg, subject, fix, observation, module) => {
10
+ issues.push({ code, package: pkg, subject, fix, ...(observation ? { observation } : {}), ...(module ? { module } : {}) });
11
11
  };
12
+ const addIssue = add;
12
13
  const report = () => ({ state: unknown ? "unknown" : issues.length ? "unsatisfied" : "ready", candidate: input?.candidate,
13
14
  loadedPackages: input?.loadedPackages ?? [], issues: issues.sort((a, b) => [a.code, a.package, a.subject, a.observation ?? ""].join("|").localeCompare([b.code, b.package, b.subject, b.observation ?? ""].join("|"))) });
14
15
  if (!input || !Array.isArray(input.loadedPackages) || !Array.isArray(input.contributions))
@@ -28,9 +29,27 @@ export async function checkReadiness(input) {
28
29
  for (const p of loaded) {
29
30
  if (p.version !== set || (p.kind === "kernel" && p.name !== "@openlup/core"))
30
31
  add("OPENLUP_E_SET_MISMATCH", p.name, "version", install);
31
- if (p.kind !== "kernel" && contributions.filter((c) => c.manifest.name === p.name).length !== 1)
32
+ if (p.modules !== undefined) {
33
+ for (const id of (Array.isArray(p.modules) ? p.modules : [])) {
34
+ if (contributions.filter((c) => c.manifest.name === p.name && c.manifest.module?.id === id).length !== 1)
35
+ add("OPENLUP_E_SET_MISMATCH", p.name, "contribution", "Compose exactly one contribution for each selected module.", undefined, id);
36
+ }
37
+ }
38
+ else if (p.kind !== "kernel" && contributions.filter((c) => c.manifest.name === p.name).length !== 1)
32
39
  add("OPENLUP_E_SET_MISMATCH", p.name, "contribution", "Compose exactly one contribution for each loaded non-kernel package.");
33
40
  }
41
+ // Selection is observed per artifact. Refused mixed/unselected contributions
42
+ // do not introduce adapter, environment or schema obligations.
43
+ const selected = contributions.filter((c) => {
44
+ const m = c.manifest, p = loaded.find((p) => p.name === m.name);
45
+ if (m.module ? !Array.isArray(p?.modules) || !p.modules.includes(m.module.id) : p?.modules !== undefined) {
46
+ add("OPENLUP_E_SET_MISMATCH", m.name, "module-selection", "Match module contributions to the observed artifact selection.", undefined, m.module?.id);
47
+ return false;
48
+ }
49
+ return true;
50
+ });
51
+ const layers = { kernel: 0, rail: 1, capability: 2, implementation: 3 };
52
+ const bindings = new Map();
34
53
  const handlers = new Map();
35
54
  const emitters = new Map();
36
55
  const events = new Set();
@@ -41,46 +60,87 @@ export async function checkReadiness(input) {
41
60
  events.add(v.eventType);
42
61
  }
43
62
  }
44
- for (const c of contributions) {
63
+ for (const c of selected) {
45
64
  const m = c.manifest;
65
+ const add = (code, pkg, subject, fix, observation) => addIssue(code, pkg, subject, fix, observation, m.module?.id);
66
+ const owner = m.module ? `${m.name}/${m.module.id}` : m.name;
67
+ if (m.module) {
68
+ for (const dependency of m.module.dependsOn) {
69
+ const target = selected.find((other) => other.manifest.name === m.name && other.manifest.module?.id === dependency.module)?.manifest.module;
70
+ if (!target)
71
+ add("OPENLUP_E_SET_MISMATCH", m.name, `dependency:${dependency.module}`, "Select and compose the required module in the same artifact.");
72
+ else {
73
+ if (layers[target.kind] > layers[m.module.kind])
74
+ add("OPENLUP_E_SET_MISMATCH", m.name, `dependency:${dependency.module}`, "Depend only on modules in the same or an earlier layer.");
75
+ for (const entry of dependency.entries)
76
+ if (!target.provides.includes(entry))
77
+ add("OPENLUP_E_SET_MISMATCH", m.name, `dependency:${dependency.module}:${entry}`, "Use an entry declared by the dependency's provides contract.");
78
+ }
79
+ }
80
+ for (const [type, ids] of [["schedule", m.schedules.map((s) => s.id)], ["route", m.routes]])
81
+ for (const id of ids) {
82
+ const key = `${m.name}:${type}:${id}`, previous = bindings.get(key);
83
+ if (previous !== undefined)
84
+ add("OPENLUP_E_SET_MISMATCH", m.name, `${type}:${id}`, "Declare one module owner for each artifact schedule or route.", previous);
85
+ else
86
+ bindings.set(key, m.module.id);
87
+ }
88
+ }
46
89
  const p = loaded.filter((p) => p.name === m.name);
47
90
  if (p.length !== 1 || p[0]?.version !== m.version || p[0]?.kind !== m.kind)
48
91
  add("OPENLUP_E_SET_MISMATCH", m.name, "manifest", "Match the contribution to its observed installed package.");
49
92
  for (const type of m.emits) {
50
93
  events.add(type);
51
94
  if (m.kind !== "kernel")
52
- emitters.set(type, [...(emitters.get(type) ?? []), m.name]);
95
+ emitters.set(type, [...(emitters.get(type) ?? []), owner]);
53
96
  }
54
97
  const actual = c.handlers.map((h) => h?.eventType);
55
98
  for (const type of new Set([...m.handles, ...actual])) {
56
99
  if (m.handles.filter((x) => x === type).length !== 1 || actual.filter((x) => x === type).length !== 1)
57
100
  add("OPENLUP_E_EVENT_DUPLICATE", m.name, type, "Match each declared consumer to exactly one composed handler descriptor.");
58
101
  else
59
- handlers.set(type, [...(handlers.get(type) ?? []), m.name]);
102
+ handlers.set(type, [...(handlers.get(type) ?? []), owner]);
60
103
  events.add(type);
61
104
  }
62
105
  for (const port of m.requiredPorts)
63
- if (!isPort(input.bindings?.ports?.[`${m.name}:${port}`]))
106
+ if (!isPort(input.bindings?.ports?.[`${owner}:${port}`]))
64
107
  add("OPENLUP_E_PORT_MISSING", m.name, port, "Bind the selected adapter port in composition.");
65
108
  for (const env of m.env)
66
109
  if (typeof input.bindings?.env?.[env] !== "string" || !input.bindings.env[env]?.trim())
67
110
  add("OPENLUP_E_ENV_MISSING", m.name, env, "Supply the named environment variable without exposing its value.");
68
111
  for (const id of new Set([...m.schedules.map((s) => s.id), ...c.schedules.map((s) => s?.id)])) {
69
112
  const declarations = m.schedules.filter((s) => s.id === id), actual = c.schedules.filter((s) => s?.id === id);
70
- if (declarations.length !== 1 || actual.length !== 1 || declarations[0]?.cadenceSeconds !== actual[0]?.cadenceSeconds || typeof actual[0]?.run !== "function" || input.bindings?.triggers?.[`${m.name}:${id}`] !== actual[0]?.run)
113
+ if (declarations.length !== 1 || actual.length !== 1 || declarations[0]?.cadenceSeconds !== actual[0]?.cadenceSeconds || typeof actual[0]?.run !== "function" || input.bindings?.triggers?.[`${owner}:${id}`] !== actual[0]?.run)
71
114
  add("OPENLUP_E_SCHEDULE_UNBOUND", m.name, id, "Bind the actual schedule with its declared cadence to one host trigger.");
72
115
  }
73
116
  for (const id of new Set([...m.routes, ...c.routes.map((r) => r?.id)]))
74
- if (m.routes.filter((r) => r === id).length !== 1 || c.routes.filter((r) => r?.id === id && typeof r.handle === "function").length !== 1 || input.bindings?.routes?.[`${m.name}:${id}`] !== c.routes.find((r) => r.id === id)?.handle)
117
+ if (m.routes.filter((r) => r === id).length !== 1 || c.routes.filter((r) => r?.id === id && typeof r.handle === "function").length !== 1 || input.bindings?.routes?.[`${owner}:${id}`] !== c.routes.find((r) => r.id === id)?.handle)
75
118
  add("OPENLUP_E_PORT_MISSING", m.name, id, "Bind the declared route to its actual host entry.");
76
119
  }
77
120
  for (const [type, owners] of [...emitters, ...handlers])
78
- if (owners.length > 1)
79
- add("OPENLUP_E_EVENT_DUPLICATE", owners.sort().join(","), type, "Use one non-kernel producer owner and one composed consumer per event.");
121
+ if (owners.length > 1) {
122
+ const fix = "Use one non-kernel producer owner and one composed consumer per event.";
123
+ if (owners.some((owner) => selected.some((c) => c.manifest.module && `${c.manifest.name}/${c.manifest.module.id}` === owner))) {
124
+ for (const owner of new Set(owners)) {
125
+ const m = selected.find((c) => c.manifest.module && `${c.manifest.name}/${c.manifest.module.id}` === owner)?.manifest;
126
+ add("OPENLUP_E_EVENT_DUPLICATE", m?.name ?? owner, type, fix, [...owners].sort().join(","), m?.module?.id);
127
+ }
128
+ }
129
+ else
130
+ add("OPENLUP_E_EVENT_DUPLICATE", owners.sort().join(","), type, fix);
131
+ }
80
132
  for (const type of events)
81
- if (!handlers.has(type) && !(Array.isArray(input.exemptions) ? input.exemptions : []).some((e) => e && typeof e.reason === "string" && e.reason.trim() && typeof e.owner === "string" && typeof e.pattern === "string" && ["ignored", "dormant"].includes(e.state) && typeof e.prefix === "boolean" && (e.prefix ? type.startsWith(e.pattern) : type === e.pattern)))
82
- add("OPENLUP_E_EVENT_UNHANDLED", "@openlup/core", type, "Register a handler or an explicit owned ignored/dormant declaration.");
83
- for (const c of contributions) {
133
+ if (!handlers.has(type) && !(Array.isArray(input.exemptions) ? input.exemptions : []).some((e) => e && typeof e.reason === "string" && e.reason.trim() && typeof e.owner === "string" && typeof e.pattern === "string" && ["ignored", "dormant"].includes(e.state) && typeof e.prefix === "boolean" && (e.prefix ? type.startsWith(e.pattern) : type === e.pattern))) {
134
+ const owners = selected.filter((c) => c.manifest.module && c.manifest.emits.includes(type));
135
+ const fix = "Register a handler or an explicit owned ignored/dormant declaration.";
136
+ if (owners.length)
137
+ for (const c of owners)
138
+ add("OPENLUP_E_EVENT_UNHANDLED", c.manifest.name, type, fix, undefined, c.manifest.module?.id);
139
+ else
140
+ add("OPENLUP_E_EVENT_UNHANDLED", "@openlup/core", type, fix);
141
+ }
142
+ for (const c of selected) {
143
+ const add = (code, pkg, subject, fix, observation) => addIssue(code, pkg, subject, fix, observation, c.manifest.module?.id);
84
144
  const req = c.manifest.requiredSchema;
85
145
  if (!req)
86
146
  continue;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openlup/core",
3
- "version": "0.15.0",
3
+ "version": "0.17.0",
4
4
  "description": "Neutral D2C subscription and bundle commerce kernels; a development preview with no stable API.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -262,18 +262,17 @@
262
262
  }
263
263
  },
264
264
  "pack": {
265
- "$budgetReason": "2026-10-12 owner-approved bounded decision: additive grouped PostgreSQL catalog probes preserve the native jsonb text contract. Public base380edbd measures154118 packed/652616 unpacked bytes; corrected contribution measures158572/666908. Aggregate155KiB packed/652KiB unpacked is the smallest KiB-aligned headroom that fits the measured contribution; code/docs class ceilings, all file ceilings, required artifacts and other controls remain unchanged. Re-measure the exact approved candidate before DCO/delivery; this proposal grants no release authority. Later growth still requires a measured budget decision.",
266
265
  "code": {
267
- "maxPackedBytes": 131072,
268
- "maxFiles": 198
266
+ "maxPackedBytes": 276480,
267
+ "maxFiles": 400
269
268
  },
270
269
  "docs": {
271
- "maxPackedBytes": 24576,
272
- "maxFiles": 12
270
+ "maxPackedBytes": 49152,
271
+ "maxFiles": 20
273
272
  },
274
- "maxPackedBytes": 158720,
275
- "maxUnpackedBytes": 667648,
276
- "maxFiles": 211,
273
+ "maxPackedBytes": 327680,
274
+ "maxUnpackedBytes": 1392640,
275
+ "maxFiles": 420,
277
276
  "requiredFiles": [
278
277
  "AGENTS.md",
279
278
  "CHANGELOG.md",
@@ -285,7 +284,12 @@
285
284
  "SECURITY.md",
286
285
  "package.json",
287
286
  "release-gates.json"
288
- ]
287
+ ],
288
+ "baseline": {
289
+ "version": "0.16.0",
290
+ "packedBytes": 161371
291
+ },
292
+ "browserEntries": {}
289
293
  },
290
294
  "publish": {
291
295
  "prepublishOnly": "node --experimental-strip-types ./scripts/refuse-publish.ts",
@@ -5,7 +5,7 @@ export const READINESS_CODES = ["OPENLUP_E_PORT_MISSING", "OPENLUP_E_EVENT_UNHAN
5
5
  /** @beta */
6
6
  export type ReadinessCode = typeof READINESS_CODES[number];
7
7
  /** @beta */
8
- export interface ReadinessIssue { code: ReadinessCode; package: string; subject: string; fix: string; observation?: string }
8
+ export interface ReadinessIssue { code: ReadinessCode; package: string; subject: string; fix: string; observation?: string; module?: string }
9
9
  /** @beta */
10
10
  export interface SchemaObject { kind: "table" | "column" | "function"; name: string; signature?: string }
11
11
  /** @beta Version is an adapter-specific migration identity, not inferred from object existence. */
@@ -16,6 +16,11 @@ export interface PackageManifest {
16
16
  emits: readonly string[]; handles: readonly string[];
17
17
  schedules: readonly { id: string; cadenceSeconds: number }[];
18
18
  routes: readonly string[]; requiredPorts: readonly string[]; env: readonly string[];
19
+ /** Selected logical module within this artifact; kind above remains the artifact kind. */
20
+ module?: {
21
+ id: string; kind: PackageManifest["kind"]; provides: readonly string[];
22
+ dependsOn: readonly { module: string; entries: readonly string[] }[];
23
+ };
19
24
  requiredSchema?: RequiredSchema;
20
25
  }
21
26
  /** @beta Run-bound handlers are represented by their shared inert descriptors. */
@@ -26,7 +31,7 @@ export interface Contribution {
26
31
  routes: readonly { id: string; handle: (...args: never[]) => unknown }[];
27
32
  }
28
33
  /** @beta Host-observed inventory, including core. The loader owns completeness. */
29
- export interface LoadedPackage { name: string; version: string; kind: PackageManifest["kind"] }
34
+ export interface LoadedPackage { name: string; version: string; kind: PackageManifest["kind"]; modules?: readonly string[] }
30
35
  /** @beta Candidate identity must change when configuration, schema or artifact changes. */
31
36
  export interface CandidateIdentity { artifact: string; configuration: string; environment: string; database: string; schema: string }
32
37
  /** @beta */
@@ -3,6 +3,9 @@ import type { ReadinessInput, ReadinessIssue, Contribution } from "./contracts.j
3
3
  const name = z.string().min(1).max(240).regex(/^[a-zA-Z0-9@_./:-]+$/);
4
4
  const version = z.string().regex(/^0\.\d+\.\d+$/);
5
5
  const list = z.array(name).refine((items) => new Set(items).size === items.length);
6
+ const moduleId = z.string().max(240).regex(/^[a-z0-9][a-z0-9-]*$/);
7
+ const modules = z.array(moduleId).refine((items) => new Set(items).size === items.length);
8
+ const entries = z.array(z.string().max(240).regex(/^(?:\.|\.(?:\/[a-zA-Z0-9_-]+)+)$/)).refine((items) => new Set(items).size === items.length);
6
9
  const kind = z.enum(["kernel", "rail", "capability", "implementation"]);
7
10
  export const schemaObjectShape = z.object({ kind: z.enum(["table", "column", "function"]), name,
8
11
  signature: z.string().max(1000).optional() }).strict().refine((o) =>
@@ -10,6 +13,9 @@ export const schemaObjectShape = z.object({ kind: z.enum(["table", "column", "fu
10
13
  export const manifestShape = z.object({ name: name.regex(/^@openlup\/[a-z0-9-]+$/), version, kind,
11
14
  emits: list, handles: list, schedules: z.array(z.object({ id: name, cadenceSeconds: z.number().finite().positive() }).strict()),
12
15
  routes: list, requiredPorts: list, env: list,
16
+ module: z.object({ id: moduleId, kind, provides: entries,
17
+ dependsOn: z.array(z.object({ module: moduleId, entries }).strict()).refine((items) => new Set(items.map((item) => item.module)).size === items.length),
18
+ }).strict().optional(),
13
19
  requiredSchema: z.object({ version: name, migration: name, objects: z.array(schemaObjectShape) }).strict().optional(),
14
20
  }).strict();
15
21
  /** Validate declarations without invoking a handler, schedule, route or probe. */
@@ -20,14 +26,16 @@ export function declarationIssues(input: ReadinessInput): ReadinessIssue[] {
20
26
  if (!input.candidate || ![input.candidate.artifact, input.candidate.configuration, input.candidate.environment, input.candidate.database, input.candidate.schema].every((v) => typeof v === "string" && /^[a-zA-Z0-9_.:/-]{1,240}$/.test(v))) issue("candidate", "Supply sanitized immutable artifact, configuration, environment and database identities.");
21
27
  if (input.inventoryComplete !== true) issue("inventory", "Observe the complete installed package inventory, including core.");
22
28
  if (!Array.isArray(input.loadedPackages) || !Array.isArray(input.contributions)) { issue("inventory", "Supply loaded packages and contributions as arrays."); return issues; }
23
- for (const pkg of input.loadedPackages) if (!z.object({ name, version, kind }).strict().safeParse(pkg).success) issue("loaded-package", "Supply a valid observed package name, version and kind.");
29
+ for (const pkg of input.loadedPackages) if (!z.object({ name, version, kind, modules: modules.optional() }).strict().safeParse(pkg).success) issue("loaded-package", "Supply a valid observed package name, version and kind.");
24
30
  for (const c of input.contributions as readonly Contribution[]) {
25
31
  if (!manifestShape.safeParse(c?.manifest).success) { issue("manifest", "Supply a valid complete manifest with unique declarations."); continue; }
32
+ const start = issues.length;
26
33
  if (!Array.isArray(c.handlers) || c.handlers.some((h) => !name.safeParse(h?.eventType).success || !Number.isFinite(h?.timeoutMs) || h?.timeoutMs <= 0)) issue(c.manifest.name, "Supply valid shared handler descriptors.", "OPENLUP_E_EVENT_UNHANDLED");
27
34
  if (!Array.isArray(c.schedules) || c.schedules.some((s) => !name.safeParse(s?.id).success || !Number.isFinite(s?.cadenceSeconds) || s?.cadenceSeconds <= 0 || typeof s?.run !== "function")) issue(c.manifest.name, "Supply actual runnable schedules.", "OPENLUP_E_SCHEDULE_UNBOUND");
28
35
  if (!Array.isArray(c.routes) || c.routes.some((r) => !name.safeParse(r?.id).success || typeof r?.handle !== "function")) issue(c.manifest.name, "Supply actual route registrations.", "OPENLUP_E_PORT_MISSING");
29
36
  const objects = c.manifest.requiredSchema?.objects ?? [];
30
37
  if (new Set(objects.map((o) => `${o.kind}:${o.name}:${o.signature ?? ""}`)).size !== objects.length) issue(c.manifest.name, "Remove duplicate schema requirements.", "OPENLUP_E_SCHEMA_BEHIND");
38
+ if (c.manifest.module) for (const diagnostic of issues.slice(start)) diagnostic.module = c.manifest.module.id;
31
39
  }
32
40
  if (!Array.isArray(input.vocabulary) || input.vocabulary.some((v) => !name.safeParse(v?.eventType).success || !name.safeParse(v?.owner).success)) issue("vocabulary", "Supply explicit vocabulary entries with owners.", "OPENLUP_E_EVENT_UNHANDLED");
33
41
  if (!Array.isArray(input.exemptions) || input.exemptions.some((e) => !name.safeParse(e?.pattern).success || typeof e?.prefix !== "boolean" || !name.safeParse(e?.owner).success || typeof e?.reason !== "string" || !e?.reason.trim() || !["ignored", "dormant"].includes(e?.state))) issue("exemptions", "Supply explicit owned exemptions with reasons.", "OPENLUP_E_EVENT_UNHANDLED");
@@ -7,9 +7,10 @@ export { schemaObjectKey, buildSchemaObjectProbe, createSchemaProbe, buildGroupe
7
7
  export async function checkReadiness(input: ReadinessInput): Promise<ReadinessReport> {
8
8
  const issues = declarationIssues(input);
9
9
  let unknown = issues.some((i) => ["input", "candidate", "inventory", "loaded-package", "schemaProbe", "probeTimeoutMs"].includes(i.subject));
10
- const add = (code: ReadinessIssue["code"], pkg: string, subject: string, fix: string, observation?: string) => {
11
- issues.push({ code, package: pkg, subject, fix, ...(observation ? { observation } : {}) });
10
+ const add = (code: ReadinessIssue["code"], pkg: string, subject: string, fix: string, observation?: string, module?: string) => {
11
+ issues.push({ code, package: pkg, subject, fix, ...(observation ? { observation } : {}), ...(module ? { module } : {}) });
12
12
  };
13
+ const addIssue = add;
13
14
  const report = (): ReadinessReport => ({ state: unknown ? "unknown" : issues.length ? "unsatisfied" : "ready", candidate: input?.candidate,
14
15
  loadedPackages: input?.loadedPackages ?? [], issues: issues.sort((a,b) => [a.code,a.package,a.subject,a.observation ?? ""].join("|").localeCompare([b.code,b.package,b.subject,b.observation ?? ""].join("|"))) });
15
16
  if (!input || !Array.isArray(input.loadedPackages) || !Array.isArray(input.contributions)) return report();
@@ -23,8 +24,24 @@ export async function checkReadiness(input: ReadinessInput): Promise<ReadinessRe
23
24
  for (const [name,count] of packages) if (count > 1) add("OPENLUP_E_SET_MISMATCH", name, "inventory", "Observe each loaded package exactly once.");
24
25
  for (const p of loaded) {
25
26
  if (p.version !== set || (p.kind === "kernel" && p.name !== "@openlup/core")) add("OPENLUP_E_SET_MISMATCH", p.name, "version", install);
26
- if (p.kind !== "kernel" && contributions.filter((c) => c.manifest.name === p.name).length !== 1) add("OPENLUP_E_SET_MISMATCH", p.name, "contribution", "Compose exactly one contribution for each loaded non-kernel package.");
27
+ if (p.modules !== undefined) {
28
+ for (const id of (Array.isArray(p.modules) ? p.modules : [])) {
29
+ if (contributions.filter((c) => c.manifest.name === p.name && c.manifest.module?.id === id).length !== 1) add("OPENLUP_E_SET_MISMATCH", p.name, "contribution", "Compose exactly one contribution for each selected module.", undefined, id);
30
+ }
31
+ } else if (p.kind !== "kernel" && contributions.filter((c) => c.manifest.name === p.name).length !== 1) add("OPENLUP_E_SET_MISMATCH", p.name, "contribution", "Compose exactly one contribution for each loaded non-kernel package.");
27
32
  }
33
+ // Selection is observed per artifact. Refused mixed/unselected contributions
34
+ // do not introduce adapter, environment or schema obligations.
35
+ const selected = contributions.filter((c) => {
36
+ const m = c.manifest, p = loaded.find((p) => p.name === m.name);
37
+ if (m.module ? !Array.isArray(p?.modules) || !p.modules.includes(m.module.id) : p?.modules !== undefined) {
38
+ add("OPENLUP_E_SET_MISMATCH", m.name, "module-selection", "Match module contributions to the observed artifact selection.", undefined, m.module?.id);
39
+ return false;
40
+ }
41
+ return true;
42
+ });
43
+ const layers = { kernel: 0, rail: 1, capability: 2, implementation: 3 };
44
+ const bindings = new Map<string, string>();
28
45
  const handlers = new Map<string,string[]>(); const emitters = new Map<string,string[]>();
29
46
  const events = new Set<string>();
30
47
  for (const v of (Array.isArray(input.vocabulary) ? input.vocabulary : [])) {
@@ -33,31 +50,62 @@ export async function checkReadiness(input: ReadinessInput): Promise<ReadinessRe
33
50
  events.add(v.eventType);
34
51
  }
35
52
  }
36
- for (const c of contributions) {
53
+ for (const c of selected) {
37
54
  const m = c.manifest;
55
+ const add: typeof addIssue = (code, pkg, subject, fix, observation) => addIssue(code, pkg, subject, fix, observation, m.module?.id);
56
+ const owner = m.module ? `${m.name}/${m.module.id}` : m.name;
57
+ if (m.module) {
58
+ for (const dependency of m.module.dependsOn) {
59
+ const target = selected.find((other) => other.manifest.name === m.name && other.manifest.module?.id === dependency.module)?.manifest.module;
60
+ if (!target) add("OPENLUP_E_SET_MISMATCH", m.name, `dependency:${dependency.module}`, "Select and compose the required module in the same artifact.");
61
+ else {
62
+ if (layers[target.kind] > layers[m.module.kind]) add("OPENLUP_E_SET_MISMATCH", m.name, `dependency:${dependency.module}`, "Depend only on modules in the same or an earlier layer.");
63
+ for (const entry of dependency.entries) if (!target.provides.includes(entry)) add("OPENLUP_E_SET_MISMATCH", m.name, `dependency:${dependency.module}:${entry}`, "Use an entry declared by the dependency's provides contract.");
64
+ }
65
+ }
66
+ for (const [type, ids] of [["schedule", m.schedules.map((s) => s.id)], ["route", m.routes]] as const) for (const id of ids) {
67
+ const key = `${m.name}:${type}:${id}`, previous = bindings.get(key);
68
+ if (previous !== undefined) add("OPENLUP_E_SET_MISMATCH", m.name, `${type}:${id}`, "Declare one module owner for each artifact schedule or route.", previous);
69
+ else bindings.set(key, m.module.id);
70
+ }
71
+ }
38
72
  const p = loaded.filter((p) => p.name === m.name);
39
73
  if (p.length !== 1 || p[0]?.version !== m.version || p[0]?.kind !== m.kind) add("OPENLUP_E_SET_MISMATCH", m.name, "manifest", "Match the contribution to its observed installed package.");
40
74
  for (const type of m.emits) {
41
75
  events.add(type);
42
- if (m.kind !== "kernel") emitters.set(type, [...(emitters.get(type) ?? []), m.name]);
76
+ if (m.kind !== "kernel") emitters.set(type, [...(emitters.get(type) ?? []), owner]);
43
77
  }
44
78
  const actual = c.handlers.map((h) => h?.eventType);
45
79
  for (const type of new Set([...m.handles,...actual])) {
46
80
  if (m.handles.filter((x) => x === type).length !== 1 || actual.filter((x) => x === type).length !== 1) add("OPENLUP_E_EVENT_DUPLICATE", m.name, type, "Match each declared consumer to exactly one composed handler descriptor.");
47
- else handlers.set(type, [...(handlers.get(type) ?? []), m.name]);
81
+ else handlers.set(type, [...(handlers.get(type) ?? []), owner]);
48
82
  events.add(type);
49
83
  }
50
- for (const port of m.requiredPorts) if (!isPort(input.bindings?.ports?.[`${m.name}:${port}`])) add("OPENLUP_E_PORT_MISSING", m.name, port, "Bind the selected adapter port in composition.");
84
+ for (const port of m.requiredPorts) if (!isPort(input.bindings?.ports?.[`${owner}:${port}`])) add("OPENLUP_E_PORT_MISSING", m.name, port, "Bind the selected adapter port in composition.");
51
85
  for (const env of m.env) if (typeof input.bindings?.env?.[env] !== "string" || !input.bindings.env[env]?.trim()) add("OPENLUP_E_ENV_MISSING", m.name, env, "Supply the named environment variable without exposing its value.");
52
86
  for (const id of new Set([...m.schedules.map((s) => s.id),...c.schedules.map((s) => s?.id)])) {
53
87
  const declarations = m.schedules.filter((s) => s.id === id), actual = c.schedules.filter((s) => s?.id === id);
54
- if (declarations.length !== 1 || actual.length !== 1 || declarations[0]?.cadenceSeconds !== actual[0]?.cadenceSeconds || typeof actual[0]?.run !== "function" || input.bindings?.triggers?.[`${m.name}:${id}`] !== actual[0]?.run) add("OPENLUP_E_SCHEDULE_UNBOUND", m.name, id, "Bind the actual schedule with its declared cadence to one host trigger.");
88
+ if (declarations.length !== 1 || actual.length !== 1 || declarations[0]?.cadenceSeconds !== actual[0]?.cadenceSeconds || typeof actual[0]?.run !== "function" || input.bindings?.triggers?.[`${owner}:${id}`] !== actual[0]?.run) add("OPENLUP_E_SCHEDULE_UNBOUND", m.name, id, "Bind the actual schedule with its declared cadence to one host trigger.");
55
89
  }
56
- for (const id of new Set([...m.routes,...c.routes.map((r) => r?.id)])) if (m.routes.filter((r) => r === id).length !== 1 || c.routes.filter((r) => r?.id === id && typeof r.handle === "function").length !== 1 || input.bindings?.routes?.[`${m.name}:${id}`] !== c.routes.find((r) => r.id === id)?.handle) add("OPENLUP_E_PORT_MISSING", m.name, id, "Bind the declared route to its actual host entry.");
90
+ for (const id of new Set([...m.routes,...c.routes.map((r) => r?.id)])) if (m.routes.filter((r) => r === id).length !== 1 || c.routes.filter((r) => r?.id === id && typeof r.handle === "function").length !== 1 || input.bindings?.routes?.[`${owner}:${id}`] !== c.routes.find((r) => r.id === id)?.handle) add("OPENLUP_E_PORT_MISSING", m.name, id, "Bind the declared route to its actual host entry.");
91
+ }
92
+ for (const [type,owners] of [...emitters,...handlers]) if (owners.length > 1) {
93
+ const fix = "Use one non-kernel producer owner and one composed consumer per event.";
94
+ if (owners.some((owner) => selected.some((c) => c.manifest.module && `${c.manifest.name}/${c.manifest.module.id}` === owner))) {
95
+ for (const owner of new Set(owners)) {
96
+ const m = selected.find((c) => c.manifest.module && `${c.manifest.name}/${c.manifest.module.id}` === owner)?.manifest;
97
+ add("OPENLUP_E_EVENT_DUPLICATE", m?.name ?? owner, type, fix, [...owners].sort().join(","), m?.module?.id);
98
+ }
99
+ } else add("OPENLUP_E_EVENT_DUPLICATE", owners.sort().join(","), type, fix);
100
+ }
101
+ for (const type of events) if (!handlers.has(type) && !(Array.isArray(input.exemptions) ? input.exemptions : []).some((e) => e && typeof e.reason === "string" && e.reason.trim() && typeof e.owner === "string" && typeof e.pattern === "string" && ["ignored", "dormant"].includes(e.state) && typeof e.prefix === "boolean" && (e.prefix ? type.startsWith(e.pattern) : type === e.pattern))) {
102
+ const owners = selected.filter((c) => c.manifest.module && c.manifest.emits.includes(type));
103
+ const fix = "Register a handler or an explicit owned ignored/dormant declaration.";
104
+ if (owners.length) for (const c of owners) add("OPENLUP_E_EVENT_UNHANDLED", c.manifest.name, type, fix, undefined, c.manifest.module?.id);
105
+ else add("OPENLUP_E_EVENT_UNHANDLED", "@openlup/core", type, fix);
57
106
  }
58
- for (const [type,owners] of [...emitters,...handlers]) if (owners.length > 1) add("OPENLUP_E_EVENT_DUPLICATE", owners.sort().join(","), type, "Use one non-kernel producer owner and one composed consumer per event.");
59
- for (const type of events) if (!handlers.has(type) && !(Array.isArray(input.exemptions) ? input.exemptions : []).some((e) => e && typeof e.reason === "string" && e.reason.trim() && typeof e.owner === "string" && typeof e.pattern === "string" && ["ignored", "dormant"].includes(e.state) && typeof e.prefix === "boolean" && (e.prefix ? type.startsWith(e.pattern) : type === e.pattern))) add("OPENLUP_E_EVENT_UNHANDLED", "@openlup/core", type, "Register a handler or an explicit owned ignored/dormant declaration.");
60
- for (const c of contributions) {
107
+ for (const c of selected) {
108
+ const add: typeof addIssue = (code, pkg, subject, fix, observation) => addIssue(code, pkg, subject, fix, observation, c.manifest.module?.id);
61
109
  const req = c.manifest.requiredSchema;
62
110
  if (!req) continue;
63
111
  if (issues.some((i) => ["schemaProbe", "probeTimeoutMs"].includes(i.subject))) { unknown = true; continue; }