@x1a0f3n9/dsh-typert-loader 0.1.5-rc.2

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 DeepSeek
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,6 @@
1
+ # Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
2
+ # side as of the last confirmed-consistent state. Both languages carry equal authority;
3
+ # after editing either side, bring the other along and re-record with:
4
+ # pnpm run verify-translation-pairing --write packages/typert/loader/README.md
5
+ README.md: 378be98dea5f47e4d6b0a0c70600e2a4c221c80b
6
+ README.zh.md: e598127bba12e4057405231fe09199db18f9604d
package/README.md ADDED
@@ -0,0 +1,126 @@
1
+ ---
2
+ description: "Loader integration for generated Typert artifacts: how mounted packages automatically contribute their host-face reflection and schemas to the runtime registry."
3
+ kind: "package-reference"
4
+ ---
5
+
6
+ # @x1a0f3n9/dsh-typert-loader
7
+
8
+ English | [中文](README.zh.md)
9
+
10
+ ## Summary
11
+
12
+ With `dsh-typert-loader` mounted, every package that mounts in a Loader composition automatically contributes its generated Typert reflection and schemas to the runtime registry — and withdraws them when the package or the plugin unmounts. Packages without the generated export are skipped, so adding the plugin to any composition is safe. An explicit `packages` list covers plugins nested behind another Loader entry, whose fibers carry no resolvable package specifier. It is a Node-only plugin and needs the config-tree resolution anchor to resolve packages.
13
+
14
+ ## Table of Contents
15
+
16
+ - [Use this package](#use-this-package)
17
+ - [Understand the implementation](#understand-the-implementation)
18
+ - [Further Exploration](#further-exploration)
19
+ - [Model Experience](#model-experience)
20
+ - [Known Limitations and Deferred Work](#known-limitations-and-deferred-work)
21
+ - [Dev Note](#dev-note)
22
+
23
+ -----
24
+
25
+ <a id="use-this-package"></a>
26
+ ## Use this package
27
+
28
+ Mount this plugin in a Host Loader composition that loads packages publishing generated Typert artifacts. The registry itself comes from `dsh-typert-registry`; this plugin only discovers and registers.
29
+
30
+ ### Minimal configuration
31
+
32
+ Load the registry and the loader; the loader defaults to discovering every Loader entry:
33
+
34
+ ```yaml
35
+ - name: '@x1a0f3n9/dsh-typert-registry'
36
+ - name: '@x1a0f3n9/dsh-typert-loader'
37
+ ```
38
+
39
+ | Field | Default | Meaning |
40
+ |---|---|---|
41
+ | `packages` | `[]` | Additional package artifacts to register for plugins nested behind another Loader entry; each must resolve from the config tree and export `./typert` |
42
+
43
+ The generated [configuration catalog](../../../docs/config-catalog.md#deepseek-aidsh-typert-loader) is the exhaustive source for every accepted field.
44
+
45
+ ### What gets registered
46
+
47
+ Each qualifying Loader entry contributes its generated host-face reflection and schemas to the runtime registry. Registration follows the entry lifecycle: it is withdrawn when the entry or the plugin unmounts, and an import that settles after either owner is gone is discarded.
48
+
49
+ ### Observable behavior and failures
50
+
51
+ Packages without the export are skipped silently. Resolution verdicts and imported manifests are cached for the process lifetime, so adding a `./typert` export requires a restart. A malformed artifact among already-mounted entries fails activation loudly; a later failure is logged per package without preventing unrelated packages from registering. An explicit `packages` entry that cannot be resolved from the config tree, or that lacks the export, fails loudly and names the package.
52
+
53
+ -----
54
+
55
+ <a id="understand-the-implementation"></a>
56
+ ## Understand the implementation
57
+
58
+ <details>
59
+ <summary>Implementation internals — click to expand</summary>
60
+
61
+ This section explains how the loader scans, validates, and registers; the observable behavior is covered in [Use this package](#use-this-package).
62
+
63
+ ### Design concept
64
+
65
+ The plugin is an incremental scanner mirroring the client-modules node half: every Cordis `internal/plugin` emission marks the fiber's entry name dirty, and a microtask flush reconciles each dirty name against the live Loader entries; the activation pass seeds the same dirty set with all current entries.
66
+
67
+ ### Manifest validation
68
+
69
+ `validateTypertManifest()` is the module/file boundary: the manifest crosses from a build artifact into the typed registry, so every field is checked. The manifest must name the package that exports it, carry face `host`, hold zod v4 schema instances, and keep well-formed service, event, object, member, type, and documentation records; invocation descriptors must use strict codecs. Every failure names the package and the defect.
70
+
71
+ ### Caching and ownership
72
+
73
+ Verdicts (resolvable specifier, export presence) and imported manifests are cached per package name and never expire. Registrations are keyed by entry name and withdrawn through the exact `ctx.typert.register()` disposer; in-flight tasks are tracked per entry so a late import cannot register a contribution after its owner is gone.
74
+
75
+ ### Source map
76
+
77
+ | File | Role |
78
+ |---|---|
79
+ | [`src/index.ts`](src/index.ts) | Plugin entry: `Config`, scanner, manifest validation, registration wiring |
80
+ | — | No runtime invariant companion is published; the Loader entry lifecycle directly owns each exact registry disposer, and integration tests observe registration and removal. |
81
+
82
+ </details>
83
+
84
+ -----
85
+
86
+ <a id="further-exploration"></a>
87
+ ## Further Exploration
88
+
89
+ Read these pages when the package-level contract is not enough; they move from the loader to what it registers and what produces it.
90
+
91
+ - [Typert registry](../registry/README.md) — the service this plugin feeds.
92
+ - [Typert generator](../generator/README.md) — what produces the artifacts the loader imports.
93
+ - [Generated configuration catalog](../../../docs/config-catalog.md#deepseek-aidsh-typert-loader) — the `packages` field declaration and JSDoc.
94
+ - [Typert group map](../README.md) — the full type-reflection pipeline.
95
+
96
+ -----
97
+
98
+ <a id="model-experience"></a>
99
+ ## Model Experience
100
+
101
+ None, as loader integration only registers generated artifacts; consumers own any model-visible projection.
102
+
103
+ #### KV Cache effect
104
+
105
+ No direct effect; registration changes reach a request only through a consumer that reads the registry.
106
+
107
+ ## Known Limitations and Deferred Work
108
+
109
+ <a id="known-limitations-and-deferred-work"></a>
110
+
111
+
112
+ These limits define what the loader does not discover or register; they are current package constraints, not a task backlog.
113
+
114
+ - **Host face only** — discovery imports only the host `./typert` artifact; client runtimes need a separate composition owner before equivalent discovery is added.
115
+ - **Explicit entries for nested plugins** — Loader entries are discovered automatically, but plugins nested behind another entry, or not loaded by the Loader at all, need an explicit `packages` entry or direct `ctx.typert.register()` ownership.
116
+ - **Cached verdicts never expire** — a package that gains a `./typert` export mid-process needs a restart before the loader registers it.
117
+
118
+ <a id="dev-note"></a>
119
+ ### Dev Note
120
+
121
+ <details>
122
+ <summary>Working context for maintainers — click to expand</summary>
123
+
124
+ None.
125
+
126
+ </details>
package/README.zh.md ADDED
@@ -0,0 +1,126 @@
1
+ ---
2
+ description: "生成的 Typert 产物所用的 Loader 集成:已挂载的包如何自动把宿主侧反射与 schema 贡献给运行时注册表。"
3
+ kind: "package-reference"
4
+ ---
5
+
6
+ # @x1a0f3n9/dsh-typert-loader
7
+
8
+ [English](README.md) | 中文
9
+
10
+ ## 概述
11
+
12
+ 挂载 `dsh-typert-loader` 后,Loader 组合中每个挂载的包都会自动把其生成的 Typert 反射与 schema 贡献给运行时注册表——并在包或本插件卸载时自动撤销。没有该导出的包会被跳过,因此在任何 Loader 组合中挂载它都是安全的。显式 `packages` 用于覆盖嵌套在另一 Loader 配置项之下的插件,这些插件的 fiber 不携带可解析的包说明符。它是仅支持 Node 的插件,需要配置树解析锚点才能解析包。
13
+
14
+ ## 目录
15
+
16
+ - [使用本包](#use-this-package)
17
+ - [理解实现](#understand-the-implementation)
18
+ - [进一步探索](#further-exploration)
19
+ - [模型体验](#model-experience)
20
+ - [已知限制与延期工作](#known-limitations-and-deferred-work)
21
+ - [开发备注](#dev-note)
22
+
23
+ -----
24
+
25
+ <a id="use-this-package"></a>
26
+ ## 使用本包
27
+
28
+ 在加载发布生成 Typert 产物的包的 Host Loader 组合中挂载本插件。注册表本身来自 `dsh-typert-registry`;本插件只负责发现与注册。
29
+
30
+ ### 最小配置
31
+
32
+ 加载注册表与 loader;loader 默认发现每一个 Loader 配置项:
33
+
34
+ ```yaml
35
+ - name: '@x1a0f3n9/dsh-typert-registry'
36
+ - name: '@x1a0f3n9/dsh-typert-loader'
37
+ ```
38
+
39
+ | 字段 | 默认值 | 含义 |
40
+ |---|---|---|
41
+ | `packages` | `[]` | 为嵌套在另一 Loader 配置项下的插件额外注册的包产物;每个包都必须能从配置树解析,并导出 `./typert` |
42
+
43
+ 生成的[配置目录](../../../docs/config-catalog.zh.md#deepseek-aidsh-typert-loader)是所有受支持字段的完整真源。
44
+
45
+ ### 注册什么
46
+
47
+ 每个符合条件的 Loader 配置项都会把其生成的宿主侧反射与 schema 贡献给运行时注册表。注册跟随配置项生命周期:配置项或本插件卸载时撤销;在配置项或本插件任一方卸载后才结束的导入操作会被丢弃。
48
+
49
+ ### 可观察行为与失败
50
+
51
+ 没有该导出的包会被静默跳过。解析结论与已导入的 manifest(元数据清单)会在整个进程生命周期内缓存,因此新增 `./typert` 导出后必须重启。已挂载配置项对应的产物格式错误时,激活会明确报错;之后才发生的失败按包记录日志,不会阻止无关包完成注册。无法从配置树解析、或缺少该导出的显式 `packages` 条目会明确报错并指名该包。
52
+
53
+ -----
54
+
55
+ <a id="understand-the-implementation"></a>
56
+ ## 理解实现
57
+
58
+ <details>
59
+ <summary>实现细节——点击展开</summary>
60
+
61
+ 本节解释 loader 如何扫描、校验与注册;可观察行为已在[使用本包](#use-this-package)中说明。
62
+
63
+ ### 设计理念
64
+
65
+ 本插件是一个增量扫描器,与 client-modules 的 Node 侧实现对称:每次 Cordis `internal/plugin` 事件都会把该 fiber 的配置项名称标记为脏,微任务 flush 会针对实时 Loader 配置项逐一调和每个脏名称;激活阶段用所有当前配置项填充同一脏集合。
66
+
67
+ ### Manifest 校验
68
+
69
+ `validateTypertManifest()` 是模块/文件边界:manifest 从构建产物进入类型化注册表,因此每个字段都会被检查。manifest 必须指名导出它的包、携带 `host` face、持有 zod v4 schema 实例,并保持服务、事件、对象、成员、类型与文档记录格式正确;调用描述符必须使用严格编解码器。每次失败都会指名包与缺陷。
70
+
71
+ ### 缓存与归属
72
+
73
+ 结论(可解析说明符、是否导出)与已导入的 manifest 按包名缓存且永不过期。注册按配置项名称键控,并通过 `ctx.typert.register()` 返回的同一资源释放函数撤销;进行中的任务按配置项跟踪,因此迟到的导入不可能在其所有者消失后注册贡献。
74
+
75
+ ### 源码地图
76
+
77
+ | 文件 | 职责 |
78
+ |---|---|
79
+ | [`src/index.ts`](src/index.ts) | 插件入口:`Config`、扫描器、manifest 校验、注册装配逻辑 |
80
+ | — | 不发布运行时不变式伴生入口;Loader 配置项的生命周期直接持有每个对应的注册表资源释放函数,集成测试会观察注册与移除。 |
81
+
82
+ </details>
83
+
84
+ -----
85
+
86
+ <a id="further-exploration"></a>
87
+ ## 进一步探索
88
+
89
+ 当包级约定不够用时阅读以下页面;这些页面从 loader 开始,依次介绍它注册的内容及其生成方。
90
+
91
+ - [Typert 注册表](../registry/README.zh.md)——本插件所供给的服务。
92
+ - [Typert 生成器](../generator/README.zh.md)——产生 loader 所导入产物的包。
93
+ - [生成配置目录](../../../docs/config-catalog.zh.md#deepseek-aidsh-typert-loader)——`packages` 字段声明及其 JSDoc。
94
+ - [Typert 组地图](../README.zh.md)——完整的类型反射流水线。
95
+
96
+ -----
97
+
98
+ <a id="model-experience"></a>
99
+ ## 模型体验
100
+
101
+ 无,因为 loader 集成只注册生成的产物;任何模型可见投影均由消费方负责。
102
+
103
+ #### KV Cache 影响
104
+
105
+ 无直接影响;注册变更只有通过读取注册表的消费方才会影响请求。
106
+
107
+ ## 已知限制与延期工作
108
+
109
+ <a id="known-limitations-and-deferred-work"></a>
110
+
111
+
112
+ 这些限制说明 loader 不会发现或注册什么;它们是当前包约束,不是任务积压。
113
+
114
+ - **仅宿主侧**——发现机制只会导入宿主侧 `./typert` 产物;在为客户端运行时添加等价的发现机制之前,需要先有独立的组合所有者。
115
+ - **嵌套插件需要显式条目**——Loader 配置项会被自动发现,但嵌套在另一配置项之下、或完全不经 Loader 加载的插件,需要显式加入 `packages`,或由其所有者直接调用 `ctx.typert.register()`。
116
+ - **缓存结论永不过期**——进程中途新增 `./typert` 导出的包需要重启后 loader 才会注册它。
117
+
118
+ <a id="dev-note"></a>
119
+ ### 开发备注
120
+
121
+ <details>
122
+ <summary>维护者的工作上下文——点击展开</summary>
123
+
124
+ 无。
125
+
126
+ </details>
package/lib/index.js ADDED
@@ -0,0 +1,333 @@
1
+ import { createRequire } from "node:module";
2
+ import { readFileSync } from "node:fs";
3
+ import { dirname, join } from "node:path";
4
+ import { pathToFileURL } from "node:url";
5
+ import z from "@deepseek-ai/schemastery";
6
+ //#region lib/types/index.js
7
+ /**
8
+ * Typert Loader integration: automatic registration for mounted plugin packages.
9
+ *
10
+ * When a loader entry mounts, this plugin resolves the entry's package.json; a
11
+ * package exporting `./typert` has its host face imported and its
12
+ * `TYPERT` manifest registered into `ctx.typert`, and the registration is
13
+ * withdrawn when the entry unmounts. Explicit `packages` cover plugins nested
14
+ * behind another Loader entry, whose Cordis fibers carry no resolvable package
15
+ * specifier. Packages without the export are skipped silently when discovered
16
+ * from Loader entries; an explicit package or declared artifact that is broken
17
+ * fails loud — aggregated into this plugin's activation throw for existing
18
+ * entries, contained to a logged error per package in steady state.
19
+ *
20
+ * Scanning is incremental per entry name. Every cordis `internal/plugin`
21
+ * emission marks the fiber's entry name dirty, and a microtask flush
22
+ * reconciles each dirty name against the live
23
+ * loader entries; the activation pass seeds the same dirty set with all
24
+ * current entries. Package verdicts and imported manifests are cached per
25
+ * package name and never expire — plugin-set changes take effect on restart.
26
+ *
27
+ * Manual `ctx.typert.register()` remains available for contributions
28
+ * that do not use a `./typert` artifact (hand-written wire schemas,
29
+ * tests, non-loader compositions).
30
+ *
31
+ * @module @x1a0f3n9/dsh-typert-loader
32
+ */
33
+ var __rewriteRelativeImportExtension = function(path, preserveJsx) {
34
+ if (typeof path === "string" && /^\.\.?\//.test(path)) return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function(m, tsx, d, ext, cm) {
35
+ return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : d + ext + "." + cm.toLowerCase() + "js";
36
+ });
37
+ return path;
38
+ };
39
+ /** The package.json exports key naming a package's host-face typert artifact. */
40
+ const TYPERT_HOST_EXPORT = "./typert";
41
+ /** Cordis plugin name. */
42
+ const name = "typert-loader";
43
+ /** Services required before registration: the registry this plugin feeds and the Loader it observes. */
44
+ const inject = ["typert", "loader"];
45
+ /** Validate explicit package names and default to Loader-entry discovery only. */
46
+ const Config = z.object({ packages: z.array(z.string().min(1)).default([]) });
47
+ const MEMBER_KINDS = new Set([
48
+ "property",
49
+ "method",
50
+ "getter",
51
+ "setter",
52
+ "call",
53
+ "construct",
54
+ "index"
55
+ ]);
56
+ /** Resolve the `./typert` export to a relative path, accepting the string and one-level conditional forms. */
57
+ function typertExportOf(pkgName, exportsField) {
58
+ if (typeof exportsField !== "object" || exportsField === null) return void 0;
59
+ const target = exportsField[TYPERT_HOST_EXPORT];
60
+ if (target === void 0) return void 0;
61
+ if (typeof target === "string") return target;
62
+ if (typeof target === "object" && target !== null) {
63
+ const fallback = target.default;
64
+ if (typeof fallback === "string") return fallback;
65
+ }
66
+ throw new Error(`typert-loader: ${pkgName} exports["${TYPERT_HOST_EXPORT}"] must be a string or an object with a string default`);
67
+ }
68
+ /**
69
+ * Narrow a dynamically imported typert module's `TYPERT` export to a
70
+ * contribution owned by `pkgName`. This is the module/file boundary: the
71
+ * manifest crosses from a build artifact into the typed registry, so every
72
+ * field is checked and every failure names the package and the defect.
73
+ * @param pkgName - the package whose typert face was imported.
74
+ * @param exported - the module's `TYPERT` export.
75
+ * @returns the validated contribution.
76
+ */
77
+ function validateTypertManifest(pkgName, exported) {
78
+ if (typeof exported !== "object" || exported === null) throw new Error(`typert-loader: ${pkgName} exports "${TYPERT_HOST_EXPORT}" but its module has no TYPERT manifest object`);
79
+ const manifest = exported;
80
+ if (manifest.package !== pkgName) throw new Error(`typert-loader: ${pkgName} TYPERT manifest names package ${JSON.stringify(manifest.package)} — the manifest must be owned by the package that exports it`);
81
+ if (manifest.face !== "host") throw new Error(`typert-loader: ${pkgName} exports "${TYPERT_HOST_EXPORT}" but TYPERT.face is not "host"`);
82
+ if (!Array.isArray(manifest.schemas)) throw new Error(`typert-loader: ${pkgName} TYPERT.schemas must be an array`);
83
+ for (const value of manifest.schemas) {
84
+ if (typeof value !== "object" || value === null) throw new Error(`typert-loader: ${pkgName} TYPERT.schemas contains a non-object schema`);
85
+ const schema = value;
86
+ requireString(pkgName, schema, "name", "schema");
87
+ if (typeof schema.schema !== "object" || schema.schema === null || !("_zod" in schema.schema)) throw new Error(`typert-loader: ${pkgName} TYPERT schema "${schema.name}" is not a zod v4 schema instance`);
88
+ }
89
+ const model = requireObject(pkgName, manifest.model, "TYPERT.model");
90
+ const services = requireArray(pkgName, model.services, "TYPERT.model.services");
91
+ const events = requireArray(pkgName, model.events, "TYPERT.model.events");
92
+ const objects = requireArray(pkgName, model.objects, "TYPERT.model.objects");
93
+ for (const value of services) {
94
+ const service = requireObject(pkgName, value, "service");
95
+ requireDocumentation(pkgName, service, "service");
96
+ requireString(pkgName, service, "key", "service");
97
+ requireString(pkgName, service, "exportName", "service");
98
+ requireMembers(pkgName, service.members, `service "${service.key}"`);
99
+ requireTypes(pkgName, service.types, `service "${service.key}"`);
100
+ }
101
+ for (const value of events) {
102
+ const event = requireObject(pkgName, value, "event");
103
+ requireDocumentation(pkgName, event, "event");
104
+ requireString(pkgName, event, "name", "event");
105
+ requireString(pkgName, event, "signature", `event "${event.name}"`);
106
+ if (event.mode !== void 0 && typeof event.mode !== "string") throw new Error(`typert-loader: ${pkgName} event "${event.name}" mode must be a string`);
107
+ }
108
+ for (const value of objects) {
109
+ const object = requireObject(pkgName, value, "object");
110
+ requireDocumentation(pkgName, object, "object");
111
+ requireString(pkgName, object, "name", "object");
112
+ requireString(pkgName, object, "exportName", "object");
113
+ requireMembers(pkgName, object.members, `object "${object.name}"`);
114
+ requireTypes(pkgName, object.types, `object "${object.name}"`);
115
+ }
116
+ for (const value of requireArray(pkgName, manifest.invocations, "TYPERT.invocations")) requireInvocation(pkgName, value);
117
+ return manifest;
118
+ }
119
+ function requireObject(pkgName, value, subject) {
120
+ if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error(`typert-loader: ${pkgName} ${subject} must be an object`);
121
+ return value;
122
+ }
123
+ function requireArray(pkgName, value, subject) {
124
+ if (!Array.isArray(value)) throw new Error(`typert-loader: ${pkgName} ${subject} must be an array`);
125
+ return value;
126
+ }
127
+ function requireString(pkgName, value, key, subject) {
128
+ if (typeof value[key] !== "string" || value[key].length === 0) throw new Error(`typert-loader: ${pkgName} ${subject} has a missing or empty ${key}`);
129
+ }
130
+ function requireDocumentation(pkgName, value, subject) {
131
+ requireArray(pkgName, value.tags, `${subject}.tags`);
132
+ for (const key of [
133
+ "description",
134
+ "summary",
135
+ "jsDoc"
136
+ ]) if (value[key] !== void 0 && typeof value[key] !== "string") throw new Error(`typert-loader: ${pkgName} ${subject}.${key} must be a string`);
137
+ }
138
+ function requireMembers(pkgName, value, subject) {
139
+ for (const item of requireArray(pkgName, value, `${subject}.members`)) {
140
+ const member = requireObject(pkgName, item, `${subject} member`);
141
+ requireString(pkgName, member, "name", `${subject} member`);
142
+ requireString(pkgName, member, "signature", `${subject} member`);
143
+ if (typeof member.kind !== "string" || !MEMBER_KINDS.has(member.kind)) throw new Error(`typert-loader: ${pkgName} ${subject} member "${member.name}" has invalid kind`);
144
+ }
145
+ }
146
+ function requireTypes(pkgName, value, subject) {
147
+ for (const item of requireArray(pkgName, value, `${subject}.types`)) {
148
+ const type = requireObject(pkgName, item, `${subject} type`);
149
+ requireString(pkgName, type, "name", `${subject} type`);
150
+ requireString(pkgName, type, "declaration", `${subject} type`);
151
+ }
152
+ }
153
+ function requireInvocation(pkgName, value) {
154
+ const invocation = requireObject(pkgName, value, "invocation");
155
+ for (const key of [
156
+ "id",
157
+ "service",
158
+ "namespace",
159
+ "method"
160
+ ]) requireString(pkgName, invocation, key, "invocation");
161
+ const id = invocation.id;
162
+ const receiver = requireObject(pkgName, invocation.invocation, `invocation "${id}" receiver`);
163
+ if (receiver.kind === "context") {
164
+ requireString(pkgName, receiver, "context", `invocation "${id}" Context receiver`);
165
+ requireString(pkgName, receiver, "wire", `invocation "${id}" Context receiver`);
166
+ requireStrictCodec(pkgName, receiver.codec, `invocation "${id}" Context codec`);
167
+ } else if (receiver.kind !== "direct") throw new Error(`typert-loader: ${pkgName} invocation "${id}" receiver kind must be "direct" or "context"`);
168
+ const wires = /* @__PURE__ */ new Set();
169
+ const parameters = /* @__PURE__ */ new Map();
170
+ let lookupCount = 0;
171
+ for (const valueParameter of requireArray(pkgName, invocation.parameters, `invocation "${id}" parameters`)) {
172
+ const parameter = requireObject(pkgName, valueParameter, `invocation "${id}" parameter`);
173
+ requireString(pkgName, parameter, "name", `invocation "${id}" parameter`);
174
+ requireString(pkgName, parameter, "wire", `invocation "${id}" parameter`);
175
+ const wire = parameter.wire;
176
+ if (wires.has(wire)) throw new Error(`typert-loader: ${pkgName} invocation "${id}" repeats wire field "${wire}"`);
177
+ wires.add(wire);
178
+ if (parameter.source === "lookup") {
179
+ lookupCount += 1;
180
+ requireString(pkgName, parameter, "lookup", `invocation "${id}" lookup parameter`);
181
+ } else if (parameter.source === "json") {
182
+ if (parameter.lookup !== void 0) throw new Error(`typert-loader: ${pkgName} invocation "${id}" JSON parameter declares a lookup`);
183
+ } else throw new Error(`typert-loader: ${pkgName} invocation "${id}" parameter source must be "json" or "lookup"`);
184
+ parameters.set(wire, parameter);
185
+ requireStrictCodec(pkgName, parameter.codec, `invocation "${id}" parameter codec`);
186
+ }
187
+ if (invocation.cancellation !== void 0) {
188
+ if (requireObject(pkgName, invocation.cancellation, `invocation "${id}" cancellation`).parameter !== "signal") throw new Error(`typert-loader: ${pkgName} invocation "${id}" cancellation parameter must be "signal"`);
189
+ }
190
+ if (invocation.scope !== void 0) {
191
+ if (receiver.kind !== "direct") throw new Error(`typert-loader: ${pkgName} invocation "${id}" Context receiver cannot declare a direct scope projection`);
192
+ const scope = requireObject(pkgName, invocation.scope, `invocation "${id}" scope`);
193
+ requireString(pkgName, scope, "context", `invocation "${id}" scope`);
194
+ requireString(pkgName, scope, "wire", `invocation "${id}" scope`);
195
+ const parameter = parameters.get(scope.wire);
196
+ if (lookupCount !== 1 || parameter?.source !== "lookup" || parameter.lookup !== scope.context) throw new Error(`typert-loader: ${pkgName} invocation "${id}" scope wire "${scope.wire}" must select its only lookup parameter`);
197
+ }
198
+ if (receiver.kind === "context" && wires.has(receiver.wire)) throw new Error(`typert-loader: ${pkgName} invocation "${id}" repeats Context wire field "${receiver.wire}"`);
199
+ requireStrictCodec(pkgName, invocation.result, `invocation "${id}" result codec`);
200
+ if (invocation.sourceLocation !== void 0) {
201
+ const location = requireObject(pkgName, invocation.sourceLocation, `invocation "${id}" sourceLocation`);
202
+ requireString(pkgName, location, "file", `invocation "${id}" sourceLocation`);
203
+ for (const key of ["line", "column"]) if (!Number.isInteger(location[key]) || location[key] < 1) throw new Error(`typert-loader: ${pkgName} invocation "${id}" sourceLocation.${key} must be a positive integer`);
204
+ }
205
+ }
206
+ function requireStrictCodec(pkgName, value, subject) {
207
+ const codec = requireObject(pkgName, value, subject);
208
+ if (codec.mode !== "strict") throw new Error(`typert-loader: ${pkgName} ${subject} must use a strict codec`);
209
+ requireString(pkgName, codec, "typeSymbol", subject);
210
+ if (typeof codec.schema !== "object" || codec.schema === null || !("_zod" in codec.schema) || typeof codec.schema.parse !== "function") throw new Error(`typert-loader: ${pkgName} ${subject} is not backed by a zod v4 schema`);
211
+ }
212
+ /**
213
+ * Scan current Loader entries during activation, then follow entry mounts and
214
+ * unmounts for this plugin's lifetime.
215
+ * @param ctx - plugin context carrying `typert` and `loader`.
216
+ * @param config - explicit package artifacts in addition to Loader entries.
217
+ */
218
+ async function apply(ctx, config) {
219
+ if (ctx.baseUrl === void 0) throw new Error("typert-loader: ctx.baseUrl is unset — the loader needs the config-tree anchor to resolve plugin packages");
220
+ const require = createRequire(ctx.baseUrl);
221
+ const configured = new Set(config.packages);
222
+ const registered = /* @__PURE__ */ new Map();
223
+ const pending = /* @__PURE__ */ new Map();
224
+ const artifactPath = /* @__PURE__ */ new Map();
225
+ const manifests = /* @__PURE__ */ new Map();
226
+ const dirty = /* @__PURE__ */ new Set();
227
+ let flushQueued = false;
228
+ let active = true;
229
+ ctx.effect(function* () {
230
+ yield () => {
231
+ active = false;
232
+ dirty.clear();
233
+ };
234
+ }, "typert loader lifetime");
235
+ const resolveArtifact = (pkgName) => {
236
+ const cached = artifactPath.get(pkgName);
237
+ if (cached !== void 0) return cached;
238
+ let pkgPath;
239
+ try {
240
+ pkgPath = require.resolve(`${pkgName}/package.json`);
241
+ } catch (cause) {
242
+ if (configured.has(pkgName)) throw new Error(`typert-loader: configured package "${pkgName}" cannot be resolved from the config tree — add it to the composition package dependencies or remove it from packages`, { cause });
243
+ artifactPath.set(pkgName, null);
244
+ return null;
245
+ }
246
+ const rel = typertExportOf(pkgName, JSON.parse(readFileSync(pkgPath, "utf8")).exports);
247
+ if (rel === void 0 && configured.has(pkgName)) throw new Error(`typert-loader: configured package "${pkgName}" does not export "${TYPERT_HOST_EXPORT}"`);
248
+ const resolved = rel === void 0 ? null : join(dirname(pkgPath), rel);
249
+ artifactPath.set(pkgName, resolved);
250
+ return resolved;
251
+ };
252
+ const loadManifest = (pkgName, path) => {
253
+ let loading = manifests.get(pkgName);
254
+ if (loading === void 0) {
255
+ loading = import(__rewriteRelativeImportExtension(pathToFileURL(path).href)).then((mod) => validateTypertManifest(pkgName, mod.TYPERT), (cause) => {
256
+ throw new Error(`typert-loader: ${pkgName} exports "${TYPERT_HOST_EXPORT}" but importing ${path} failed: ${String(cause)}`);
257
+ });
258
+ manifests.set(pkgName, loading);
259
+ }
260
+ return loading;
261
+ };
262
+ const qualifies = (entryName) => {
263
+ if (configured.has(entryName)) return true;
264
+ for (const entry of ctx.loader.entries()) if (entry.options.name === entryName && entry.fiber !== void 0 && !entry.disabled) return true;
265
+ return false;
266
+ };
267
+ /** Reconcile one entry name against the live loader entries; a mount returns its async task. */
268
+ const processOne = (entryName) => {
269
+ if (!qualifies(entryName)) {
270
+ const dispose = registered.get(entryName);
271
+ if (dispose !== void 0) {
272
+ registered.delete(entryName);
273
+ return dispose();
274
+ }
275
+ return;
276
+ }
277
+ if (registered.has(entryName) || pending.has(entryName)) return void 0;
278
+ const path = resolveArtifact(entryName);
279
+ if (path === null) return void 0;
280
+ const task = loadManifest(entryName, path).then((manifest) => {
281
+ if (!active || !qualifies(entryName) || registered.has(entryName)) return;
282
+ registered.set(entryName, ctx.typert.register(manifest));
283
+ });
284
+ pending.set(entryName, task);
285
+ const settle = () => {
286
+ pending.delete(entryName);
287
+ };
288
+ task.then(settle, settle);
289
+ return task;
290
+ };
291
+ const flush = (onError) => {
292
+ const tasks = [];
293
+ for (const entryName of [...dirty]) {
294
+ dirty.delete(entryName);
295
+ try {
296
+ const task = processOne(entryName);
297
+ if (task !== void 0) tasks.push(task.catch((error) => {
298
+ onError(toError(error));
299
+ }));
300
+ } catch (error) {
301
+ onError(toError(error));
302
+ }
303
+ }
304
+ return tasks;
305
+ };
306
+ ctx.on("internal/plugin", (fiber) => {
307
+ const entryName = fiber.entry?.options.name;
308
+ if (entryName === void 0) return;
309
+ dirty.add(entryName);
310
+ if (flushQueued) return;
311
+ flushQueued = true;
312
+ queueMicrotask(() => {
313
+ flushQueued = false;
314
+ if (!active) return;
315
+ for (const task of flush((err) => {
316
+ ctx.logger.error(err);
317
+ }));
318
+ });
319
+ });
320
+ for (const packageName of configured) dirty.add(packageName);
321
+ for (const entry of ctx.loader.entries()) dirty.add(entry.options.name);
322
+ const failures = [];
323
+ await Promise.all(flush((err) => {
324
+ failures.push(err);
325
+ }));
326
+ if (failures.length > 0) throw new AggregateError(failures, `typert-loader: ${String(failures.length)} typert contributor(s) failed to register:\n${failures.map((e) => ` - ${e.message}`).join("\n")}`);
327
+ }
328
+ /** Normalize an arbitrary import or manifest failure to an Error. */
329
+ function toError(error) {
330
+ return error instanceof Error ? error : new Error(String(error));
331
+ }
332
+ //#endregion
333
+ export { Config, TYPERT_HOST_EXPORT, apply, inject, name, validateTypertManifest };
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Typert Loader integration: automatic registration for mounted plugin packages.
3
+ *
4
+ * When a loader entry mounts, this plugin resolves the entry's package.json; a
5
+ * package exporting `./typert` has its host face imported and its
6
+ * `TYPERT` manifest registered into `ctx.typert`, and the registration is
7
+ * withdrawn when the entry unmounts. Explicit `packages` cover plugins nested
8
+ * behind another Loader entry, whose Cordis fibers carry no resolvable package
9
+ * specifier. Packages without the export are skipped silently when discovered
10
+ * from Loader entries; an explicit package or declared artifact that is broken
11
+ * fails loud — aggregated into this plugin's activation throw for existing
12
+ * entries, contained to a logged error per package in steady state.
13
+ *
14
+ * Scanning is incremental per entry name. Every cordis `internal/plugin`
15
+ * emission marks the fiber's entry name dirty, and a microtask flush
16
+ * reconciles each dirty name against the live
17
+ * loader entries; the activation pass seeds the same dirty set with all
18
+ * current entries. Package verdicts and imported manifests are cached per
19
+ * package name and never expire — plugin-set changes take effect on restart.
20
+ *
21
+ * Manual `ctx.typert.register()` remains available for contributions
22
+ * that do not use a `./typert` artifact (hand-written wire schemas,
23
+ * tests, non-loader compositions).
24
+ *
25
+ * @module @x1a0f3n9/dsh-typert-loader
26
+ */
27
+ import type { Context } from '@deepseek-ai/cordis';
28
+ import z from '@deepseek-ai/schemastery';
29
+ import type { TypertContribution } from '@x1a0f3n9/dsh-typert-registry/types';
30
+ /** The package.json exports key naming a package's host-face typert artifact. */
31
+ export declare const TYPERT_HOST_EXPORT = "./typert";
32
+ /** Cordis plugin name. */
33
+ export declare const name = "typert-loader";
34
+ /** Services required before registration: the registry this plugin feeds and the Loader it observes. */
35
+ export declare const inject: string[];
36
+ /** Additional package artifacts whose owning plugins are nested behind another Loader entry. */
37
+ export interface Config {
38
+ /** Exact npm package names that must resolve and export `./typert`. */
39
+ packages?: string[];
40
+ }
41
+ /** Validate explicit package names and default to Loader-entry discovery only. */
42
+ export declare const Config: z<Config>;
43
+ /**
44
+ * Narrow a dynamically imported typert module's `TYPERT` export to a
45
+ * contribution owned by `pkgName`. This is the module/file boundary: the
46
+ * manifest crosses from a build artifact into the typed registry, so every
47
+ * field is checked and every failure names the package and the defect.
48
+ * @param pkgName - the package whose typert face was imported.
49
+ * @param exported - the module's `TYPERT` export.
50
+ * @returns the validated contribution.
51
+ */
52
+ export declare function validateTypertManifest(pkgName: string, exported: unknown): TypertContribution;
53
+ /**
54
+ * Scan current Loader entries during activation, then follow entry mounts and
55
+ * unmounts for this plugin's lifetime.
56
+ * @param ctx - plugin context carrying `typert` and `loader`.
57
+ * @param config - explicit package artifacts in addition to Loader entries.
58
+ */
59
+ export declare function apply(ctx: Context, config: Config): Promise<void>;
60
+ //# sourceMappingURL=index.d.ts.map
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@x1a0f3n9/dsh-typert-loader",
3
+ "description": "Loader integration for generated Typert package contributions",
4
+ "version": "0.1.5-rc.2",
5
+ "publishConfig": {
6
+ "access": "public"
7
+ },
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
11
+ "directory": "packages/typert/loader"
12
+ },
13
+ "type": "module",
14
+ "main": "lib/index.js",
15
+ "types": "lib/types/index.d.ts",
16
+ "exports": {
17
+ ".": {
18
+ "types": "./lib/types/index.d.ts",
19
+ "default": "./lib/index.js"
20
+ },
21
+ "./src/*": "./src/*",
22
+ "./package.json": "./package.json"
23
+ },
24
+ "files": [
25
+ "lib/index.js",
26
+ "lib/types/**/*.d.ts"
27
+ ],
28
+ "license": "MIT",
29
+ "peerDependencies": {
30
+ "@deepseek-ai/cordis-plugin-loader": "^1.0.3",
31
+ "@x1a0f3n9/dsh-typert-registry": "^0.1.5-rc.2",
32
+ "@deepseek-ai/cordis": "^4.0.2"
33
+ },
34
+ "dependencies": {
35
+ "@deepseek-ai/schemastery": "^3.18.2"
36
+ },
37
+ "devDependencies": {
38
+ "zod": "^4.4.3",
39
+ "@x1a0f3n9/dsh-typert-registry": "^0.1.5-rc.2",
40
+ "@deepseek-ai/cordis-plugin-loader": "^1.0.3",
41
+ "@deepseek-ai/cordis": "^4.0.2"
42
+ }
43
+ }