@openlup/core 0.14.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 +2 -0
- package/CHANGELOG.md +16 -0
- package/CONTRIBUTING.md +2 -1
- package/README.md +19 -5
- package/dist/readiness/contracts.d.ts +12 -0
- package/dist/readiness/declarations.d.ts +14 -0
- package/dist/readiness/declarations.js +11 -1
- package/dist/readiness/index.js +74 -14
- package/package.json +1 -1
- package/release-gates.json +4 -4
- package/src/readiness/contracts.ts +7 -2
- package/src/readiness/declarations.ts +9 -1
- package/src/readiness/index.ts +60 -12
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,22 @@ 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
|
+
|
|
20
|
+
## [0.15.0]
|
|
21
|
+
|
|
22
|
+
- Publishable on the npm `latest` dist-tag as `0.15.0`, from tag
|
|
23
|
+
`openlup-core-v0.15.0`.
|
|
24
|
+
|
|
25
|
+
- Republished unchanged.
|
|
26
|
+
|
|
11
27
|
## [0.14.0]
|
|
12
28
|
|
|
13
29
|
- Publishable on the npm `latest` dist-tag as `0.14.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
|
|
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
|
@@ -21,7 +21,14 @@ package together.
|
|
|
21
21
|
each change is recorded in [the changelog](CHANGELOG.md). Pin an exact
|
|
22
22
|
version and read the changelog when you upgrade.
|
|
23
23
|
|
|
24
|
-
The experimental `./outbox`, `./standard-schema` and `./readiness` subpaths
|
|
24
|
+
The experimental `./outbox`, `./standard-schema` and `./readiness` subpaths
|
|
25
|
+
provide infrastructure contracts. `checkReadiness` checks inert contributions,
|
|
26
|
+
complete host inventory, actual schedule/route function bindings and a bounded
|
|
27
|
+
schema probe. It never migrates, leases or sends. Only `ready` admits a candidate;
|
|
28
|
+
`unknown` and `unsatisfied` retain the serving candidate. The host owns immutable
|
|
29
|
+
artifact/configuration/schema identity and admission at every work entrance.
|
|
30
|
+
Legacy artifacts require one contribution per non-kernel package; selected logical
|
|
31
|
+
modules use [module readiness](#module-readiness).
|
|
25
32
|
|
|
26
33
|
For PostgreSQL catalog inspection, import `buildGroupedPostgresSchemaProbe`,
|
|
27
34
|
`decodeGroupedPostgresSchemaRows` and `PostgresSchemaCatalogEntry` from
|
|
@@ -69,8 +76,8 @@ standalone readiness suite. Actual installed-tarball SQL and shape-order proof
|
|
|
69
76
|
runs in the existing Outbox disposable PostgreSQL tier in the source repository.
|
|
70
77
|
|
|
71
78
|
Contributor package verification keeps the existing code/docs class and file
|
|
72
|
-
ceilings.
|
|
73
|
-
|
|
79
|
+
ceilings. Its aggregate budgets in `release-gates.json` follow the measured
|
|
80
|
+
decision its `$budgetReason` records; later growth still needs its own
|
|
74
81
|
measured budget decision. Passing a local pack does not authorize publication.
|
|
75
82
|
|
|
76
83
|
## Package Surface Maturity
|
|
@@ -199,9 +206,16 @@ neutrality evidence, not package activation or consumer compatibility evidence.
|
|
|
199
206
|
|
|
200
207
|
## Kernel Documentation
|
|
201
208
|
|
|
202
|
-
<!-- openlup-doc-impact {"unit":"core","digest":"sha256-09e3df57d5021bc7edebe553c8811ffd94436bf3e0c70d981b9cedbd36782e3a","reason":"The manifest and own lockfile move from 0.13.1 to 0.14.0 for the coherent MINOR set. API, runtime, dependency resolutions and package gates are unchanged in this preparation delta; the grouped schema helpers are already documented here."} -->
|
|
203
|
-
|
|
204
209
|
- [`docs/SUBSCRIPTION_ENGINE.md`](docs/SUBSCRIPTION_ENGINE.md) — what the
|
|
205
210
|
`./subscription` kernel owns, its deterministic-clock contract, the
|
|
206
211
|
late-payment cycle-shift rule and its monotonic clamp, and how a host
|
|
207
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");
|
package/dist/readiness/index.js
CHANGED
|
@@ -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.
|
|
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
|
|
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) ?? []),
|
|
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) ?? []),
|
|
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?.[`${
|
|
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?.[`${
|
|
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?.[`${
|
|
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
|
-
|
|
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
|
-
|
|
83
|
-
|
|
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
package/release-gates.json
CHANGED
|
@@ -262,17 +262,17 @@
|
|
|
262
262
|
}
|
|
263
263
|
},
|
|
264
264
|
"pack": {
|
|
265
|
-
"$budgetReason": "2026-10-
|
|
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":
|
|
267
|
+
"maxPackedBytes": 135168,
|
|
268
268
|
"maxFiles": 198
|
|
269
269
|
},
|
|
270
270
|
"docs": {
|
|
271
271
|
"maxPackedBytes": 24576,
|
|
272
272
|
"maxFiles": 12
|
|
273
273
|
},
|
|
274
|
-
"maxPackedBytes":
|
|
275
|
-
"maxUnpackedBytes":
|
|
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");
|
package/src/readiness/index.ts
CHANGED
|
@@ -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.
|
|
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
|
|
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) ?? []),
|
|
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) ?? []),
|
|
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?.[`${
|
|
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?.[`${
|
|
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?.[`${
|
|
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
|
|
59
|
-
|
|
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; }
|