@openlup/core 0.15.0 → 0.16.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,15 @@ SemVer promise.
8
8
 
9
9
  ## [Unreleased]
10
10
 
11
+ ## [0.16.0]
12
+
13
+ - Publishable on the npm `latest` dist-tag as `0.16.0`, from tag
14
+ `openlup-core-v0.16.0`.
15
+
16
+ - 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.
17
+
18
+ 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.
19
+
11
20
  ## [0.15.0]
12
21
 
13
22
  - 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
@@ -206,9 +206,16 @@ neutrality evidence, not package activation or consumer compatibility evidence.
206
206
 
207
207
  ## Kernel Documentation
208
208
 
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
209
  - [`docs/SUBSCRIPTION_ENGINE.md`](docs/SUBSCRIPTION_ENGINE.md) — what the
212
210
  `./subscription` kernel owns, its deterministic-clock contract, the
213
211
  late-payment cycle-shift rule and its monotonic clamp, and how a host
214
212
  application consumes it.
213
+
214
+ ### Module readiness
215
+
216
+ Selected `./readiness` modules retain independent schema requirements.
217
+ Dependencies use the same or an earlier layer:
218
+ `kernel`, `rail`, `capability`, `implementation`.
219
+ See [CHANGELOG migration guidance](CHANGELOG.md#0160) for selection,
220
+ bindings, refusals and diagnostics, and the source checkout's
221
+ `api/readiness.api.md` snapshot for declarations; the host owns inventory.
@@ -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.16.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,17 +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.",
265
+ "$budgetReason": "2026-10-18 M03 bounded decision: base code 129991 packed, aggregate 158715 packed/667249 unpacked bytes; B0 before trimming 132250, 162482/681852; final 132250, 161344/678531. B0 is core's only growth for programme module M03; identity ships in @openlup/platform. Code 132KiB, aggregates 162KiB/672KiB; docs class, file ceilings, required files and other controls unchanged. Set bumps grow only the changelog; docs stays the first binding limit for set notes. Later growth still requires a measured budget decision. Grants no release authority.",
266
266
  "code": {
267
- "maxPackedBytes": 131072,
267
+ "maxPackedBytes": 135168,
268
268
  "maxFiles": 198
269
269
  },
270
270
  "docs": {
271
271
  "maxPackedBytes": 24576,
272
272
  "maxFiles": 12
273
273
  },
274
- "maxPackedBytes": 158720,
275
- "maxUnpackedBytes": 667648,
274
+ "maxPackedBytes": 165888,
275
+ "maxUnpackedBytes": 688128,
276
276
  "maxFiles": 211,
277
277
  "requiredFiles": [
278
278
  "AGENTS.md",
@@ -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; }