@deepseek-ai/dsh-experimental-forge 0.0.0-stage → 0.2.2-alpha.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,37 @@
1
+ # Bilingual-pair consistency record for README.md (docs/i18n/README.md): per heading
2
+ # section, a hash of its English and Chinese blocks outside code blocks and generated regions.
3
+ # After editing either side, bring the other along and re-record with:
4
+ # pnpm run verify-translation-pairing --write packages/experimental/forge/README.md
5
+ /:
6
+ en: 58d73ee11cb9ecde
7
+ zh: 04213727345a58a2
8
+ /deepseek-ai-dsh-experimental-forge:
9
+ en: cf24b0ee1e2732e8
10
+ zh: bb4e76d2b69b84d8
11
+ /deepseek-ai-dsh-experimental-forge/summary:
12
+ en: 2d79427980c092c3
13
+ zh: ee18048c6cc0aeec
14
+ /deepseek-ai-dsh-experimental-forge/table-of-contents:
15
+ en: d152484eb41ac6b4
16
+ zh: 2f8a621b991260a1
17
+ /deepseek-ai-dsh-experimental-forge/use-this-package:
18
+ en: 5f4f14ee9aad5072
19
+ zh: 6c8606796df24a3e
20
+ /deepseek-ai-dsh-experimental-forge/understand-the-implementation:
21
+ en: e193cb547c47069b
22
+ zh: faa517522402ea41
23
+ /deepseek-ai-dsh-experimental-forge/further-exploration:
24
+ en: bb13125144d413d5
25
+ zh: a6832dad7a093bfe
26
+ /deepseek-ai-dsh-experimental-forge/model-experience:
27
+ en: a62b9af05181f74f
28
+ zh: 64b8b0aa18c64c57
29
+ /deepseek-ai-dsh-experimental-forge/model-experience/kv-cache-effect:
30
+ en: 1675c9fcae96936c
31
+ zh: 5cd2a82aeca3160a
32
+ /deepseek-ai-dsh-experimental-forge/known-limitations-and-deferred-work:
33
+ en: 0721de59f78c48e6
34
+ zh: 094ca6e3bba766ba
35
+ /deepseek-ai-dsh-experimental-forge/known-limitations-and-deferred-work/dev-note:
36
+ en: e67f633dc6fe7773
37
+ zh: 3bb0c20c93f16f98
package/README.md CHANGED
@@ -1,3 +1,100 @@
1
- # Temporary Holding Version
1
+ ---
2
+ description: "Register code host providers and poll change request status through one provider-neutral service."
3
+ kind: "package-reference"
4
+ ---
2
5
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
6
+ # @deepseek-ai/dsh-experimental-forge
7
+
8
+ English | [中文](README.zh.md)
9
+
10
+ ## Summary
11
+
12
+ Read and act on pull requests and merge requests without coupling consumers to one code host. A provider maps remote and change request URLs to provider-neutral references, finds change requests by head branch, reads their status in batches, reads their diffs and files, and marks drafts ready or merges on request. Watchers of one change request share a single polling loop whose cadence follows check, mergeability, and lifecycle state.
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 the registry with at least one provider package, such as the GitHub provider.
29
+
30
+ ```yaml
31
+ - name: '@deepseek-ai/dsh-experimental-forge'
32
+ - name: '@deepseek-ai/dsh-experimental-forge-github'
33
+ ```
34
+
35
+ `ctx.forge.register(provider)` is an effect on the calling fiber and returns an async disposer that cancels and drains that registration's calls. Results arriving after cancellation are discarded. One provider serves one forge family; registering a second provider for the same family throws. Registration changes announce `forge/providers-changed`; registering a provider polls its existing watches immediately. `repositoryFromRemote(url)` and `changeRequestFromUrl(url)` ask providers in registration order and return the first match. `changeRequestUrl(ref)`, `findByHead(repository, branch, signal)`, and `readiness(repository, signal)` delegate to the provider of the reference's forge and throw when none is registered; `readiness` reports whether the provider can reach the host with current credentials.
36
+
37
+ `markReady(ref, signal)` marks a draft ready for review, and `merge(ref, { method, headSha }, signal)` merges a change request only while its head is still `headSha`. Both delegate to the provider of the reference's forge, throw when none is registered, and resolve to undefined once the host accepted the change or to a `ForgeActionFailure`: `unauthenticated`, `stale` when the head moved past `headSha`, or `failed` with the host's reason. After either settles, a watched change request is polled at once. Snapshots carry `mergeReady`, whether the host would merge the change request now for the signed-in user, and `mergeMethods`, the methods the repository accepts with that user's usual one first.
38
+
39
+ `diff(ref, signal)` reads the change request's unified patch as the host computes it, with `a/` and `b/` path prefixes, and `fileText(ref, commit, path, signal)` reads one file of its repository at a commit. Both delegate to the provider of the reference's forge, throw when none is registered, and resolve to the text or to a `ForgeFailure`, which is `too-large` when the host or the provider refuses an answer over its size limit. `fileUrl(ref, commit, path)` returns the web page of that file at that commit.
40
+
41
+ `ctx.forge.watch(ref, listener)` is also an effect. The first watcher of a change request polls it at once; `latest(ref)` returns the most recent result. Each forge has at most one fetch in flight, and that fetch includes every due change request of the forge. An open change request is polled every `activeIntervalMs` while checks are pending or mergeability is unknown, and every `idleIntervalMs` otherwise. Closed change requests use `idleIntervalMs` to detect reopening; merged requests stop automatic polling. `refresh(ref)` polls a watched change request at once, including after a terminal state; if a fetch is already in flight for its forge, another fetch starts immediately after it settles. A failed read repeats after `activeIntervalMs` doubled for each consecutive failure, up to `maxBackoffMs`, which must be at least `activeIntervalMs`. Unregistering a provider settles its watched change requests as `unavailable`. Polling stops when the last watcher leaves. The [configuration catalog](../../../docs/config-catalog.md#deepseek-aidsh-experimental-forge) lists the defaults.
42
+
43
+ Reaching `fetchTimeoutMs` aborts the fetch signal with `FORGE_FETCH_TIMEOUT`. Providers must stop their work and settle when the signal aborts; rejected fetches become `unavailable` failures. A provider that ignores cancellation can stall polling and service disposal.
44
+
45
+ -----
46
+
47
+ <a id="understand-the-implementation"></a>
48
+ ## Understand the implementation
49
+
50
+ <details>
51
+ <summary>Implementation internals — click to expand</summary>
52
+
53
+ One timer covers all watched change requests. When it fires, the service groups due change requests by forge and starts one batched provider fetch per idle forge. A listener that throws is logged and skipped. Disposing the service aborts in-flight fetches and waits for them to settle.
54
+
55
+ | Source | Responsibility |
56
+ |---|---|
57
+ | [`src/index.ts`](src/index.ts) | Provider registration, URL mapping, watch lifetime, and batched polling |
58
+ | [`src/schedule.ts`](src/schedule.ts) | Delay before the next poll of one change request |
59
+ | [`src/keys.ts`](src/keys.ts) | Case-insensitive canonical identity of a change request |
60
+ | [`src/types.ts`](src/types.ts) | Repository, change request, status, and provider types |
61
+
62
+ </details>
63
+
64
+ -----
65
+
66
+ <a id="further-exploration"></a>
67
+ ## Further Exploration
68
+
69
+ - [Forge subsystem](../../../docs/subsystems/forge.md) — provider contract, change request types, and service reference.
70
+ - [GitHub provider](../forge-github/README.md) — the provider for GitHub hosts.
71
+ - [Session bindings](../forge-session/README.md) — the consumer that binds change requests to Sessions.
72
+ - [Binding decision](../../../.agents/notes/implemented/feature/2026-10-09-session-change-request-bindings.md) — why bindings derive from shell results and status stays on the host.
73
+
74
+ -----
75
+
76
+ <a id="model-experience"></a>
77
+ ## Model Experience
78
+
79
+ None, as providers and watchers exchange code host status without contributing model context.
80
+
81
+ #### KV Cache effect
82
+
83
+ None. No model request contains data from this package.
84
+
85
+ ## Known Limitations and Deferred Work
86
+
87
+ <a id="known-limitations-and-deferred-work"></a>
88
+
89
+ - **Polling only** — status reaches watchers on the next scheduled poll or `refresh`; there is no webhook intake.
90
+ - **Process-local results** — latest results live in memory and are lost on restart; the next watcher polls again.
91
+
92
+ <a id="dev-note"></a>
93
+ ### Dev Note
94
+
95
+ <details>
96
+ <summary>Working context for maintainers — click to expand</summary>
97
+
98
+ None.
99
+
100
+ </details>
package/README.zh.md ADDED
@@ -0,0 +1,100 @@
1
+ ---
2
+ description: "通过一个提供方无关的服务注册代码托管提供方并轮询变更请求状态。"
3
+ kind: "package-reference"
4
+ ---
5
+
6
+ # @deepseek-ai/dsh-experimental-forge
7
+
8
+ [English](README.md) | 中文
9
+
10
+ ## 概述
11
+
12
+ 读取并操作 pull request 与 merge request,而不把消费方绑定到某一个代码托管平台。提供方把远端 URL 和变更请求(change request,即 pull request 或 merge request)URL 映射为提供方无关的引用,按 head 分支查找变更请求,批量读取其状态,读取其 diff 与文件,并按请求把草稿标记为可评审或合并变更请求。同一变更请求的所有监听方共享一个轮询循环,轮询节奏随检查、可合并性与生命周期状态变化。
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
+ 将注册表与至少一个提供方包一起挂载,例如 GitHub 提供方。
29
+
30
+ ```yaml
31
+ - name: '@deepseek-ai/dsh-experimental-forge'
32
+ - name: '@deepseek-ai/dsh-experimental-forge-github'
33
+ ```
34
+
35
+ `ctx.forge.register(provider)` 是调用方 fiber 上的 effect,返回异步 disposer,取消并等待该次注册的调用结束。取消后到达的结果会被丢弃。一个提供方服务一个 forge 家族;为同一家族注册第二个提供方会抛错。注册变化会发出 `forge/providers-changed`;注册提供方会立即轮询其已有监听。`repositoryFromRemote(url)` 与 `changeRequestFromUrl(url)` 按注册顺序询问提供方并返回第一个匹配结果。`changeRequestUrl(ref)`、`findByHead(repository, branch, signal)` 与 `readiness(repository, signal)` 委托给引用所属 forge 的提供方,没有注册提供方时抛错;`readiness` 报告提供方能否用当前凭据访问该主机。
36
+
37
+ `markReady(ref, signal)` 把草稿标记为可评审;`merge(ref, { method, headSha }, signal)` 只在 head 仍是 `headSha` 时合并变更请求。两者都委托给引用所属 forge 的提供方,没有注册提供方时抛错;主机接受时 resolve 为 undefined,否则为 `ForgeActionFailure`:`unauthenticated`、head 已越过 `headSha` 时的 `stale`,或带主机原因的 `failed`。两者结束后,被监听的变更请求会立即重新轮询。快照带有 `mergeReady`(主机现在是否会为已登录用户合并该变更请求)与 `mergeMethods`(仓库接受的合并方式,该用户常用的方式排在最前)。
38
+
39
+ `diff(ref, signal)` 读取托管平台计算出的变更请求统一补丁,路径带 `a/` 与 `b/` 前缀;`fileText(ref, commit, path, signal)` 读取其仓库中某个提交下的一个文件。两者都委托给引用所属 forge 的提供方,没有注册提供方时抛错,resolve 为文本或 `ForgeFailure`;托管平台或提供方因结果超过其大小上限而拒绝时,失败为 `too-large`。`fileUrl(ref, commit, path)` 返回该文件在该提交下的网页地址。
40
+
41
+ `ctx.forge.watch(ref, listener)` 同样是 effect。变更请求的第一个监听方会立即触发轮询;`latest(ref)` 返回最近一次结果。每个 forge 同时最多只有一次读取在进行,这次读取包含该 forge 所有到期的变更请求。打开状态的变更请求在检查未完成或可合并性未知时每 `activeIntervalMs` 轮询一次,否则每 `idleIntervalMs` 轮询一次。已关闭的变更请求按 `idleIntervalMs` 轮询以检测重新打开;已合并的停止自动轮询。`refresh(ref)` 立即轮询一个被监听的变更请求,已结束的也包括在内;该 forge 已有读取在进行时,等待其结束后立即再读一次。读取失败后,重试间隔为 `activeIntervalMs` 按连续失败次数翻倍,最长 `maxBackoffMs`;该值必须大于或等于 `activeIntervalMs`。注销提供方会让其被监听的变更请求以 `unavailable` 结束。最后一个监听方离开后轮询停止。默认值见[配置目录](../../../docs/config-catalog.zh.md#deepseek-aidsh-experimental-forge)。
42
+
43
+ 达到 `fetchTimeoutMs` 时,读取信号会以 `FORGE_FETCH_TIMEOUT` 中止。提供方必须在信号中止时停止工作并结束请求;被拒绝的读取会转为 `unavailable` 失败。忽略取消的提供方可能阻塞轮询和服务销毁。
44
+
45
+ -----
46
+
47
+ <a id="understand-the-implementation"></a>
48
+ ## 了解实现
49
+
50
+ <details>
51
+ <summary>实现细节——点击展开</summary>
52
+
53
+ 一个定时器覆盖所有被监听的变更请求。定时器触发时,服务按 forge 对到期的变更请求分组,并为每个空闲的 forge 发起一次批量读取。抛错的监听方会被记录日志并跳过。销毁服务会中止进行中的读取,并等待它们结束。
54
+
55
+ | 源码 | 职责 |
56
+ |---|---|
57
+ | [`src/index.ts`](src/index.ts) | 提供方注册、URL 映射、监听生命周期与批量轮询 |
58
+ | [`src/schedule.ts`](src/schedule.ts) | 计算单个变更请求距下次轮询的延迟 |
59
+ | [`src/keys.ts`](src/keys.ts) | 变更请求不区分大小写的规范标识 |
60
+ | [`src/types.ts`](src/types.ts) | 仓库、变更请求、状态与提供方类型 |
61
+
62
+ </details>
63
+
64
+ -----
65
+
66
+ <a id="further-exploration"></a>
67
+ ## 深入探索
68
+
69
+ - [Forge 子系统](../../../docs/subsystems/forge.zh.md)——提供方契约、变更请求类型与服务参考。
70
+ - [GitHub 提供方](../forge-github/README.zh.md)——服务 GitHub 主机的提供方。
71
+ - [Session 绑定](../forge-session/README.zh.md)——把变更请求绑定到 Session 的消费方。
72
+ - [绑定决策](../../../.agents/notes/implemented/feature/2026-10-09-session-change-request-bindings.zh.md)——为何绑定从 shell 结果派生、状态只留在宿主侧。
73
+
74
+ -----
75
+
76
+ <a id="model-experience"></a>
77
+ ## 模型体验
78
+
79
+ 无:提供方与监听方只交换代码托管平台的状态,不贡献模型上下文。
80
+
81
+ #### KV Cache 影响
82
+
83
+ 无。任何模型请求都不包含本包的数据。
84
+
85
+ ## 已知限制与待办工作
86
+
87
+ <a id="known-limitations-and-deferred-work"></a>
88
+
89
+ - **仅轮询**——状态在下一次计划轮询或 `refresh` 时才送达监听方;没有 webhook 接入。
90
+ - **结果只在进程内**——最近结果保存在内存中,重启后丢失;下一个监听方会重新轮询。
91
+
92
+ <a id="dev-note"></a>
93
+ ### 开发备注
94
+
95
+ <details>
96
+ <summary>维护者的工作上下文——点击展开</summary>
97
+
98
+ 无。
99
+
100
+ </details>
package/lib/index.js ADDED
@@ -0,0 +1,495 @@
1
+ import { Service } from "@deepseek-ai/cordis";
2
+ import z from "@deepseek-ai/schemastery";
3
+ import { MAX_TIMER_DELAY_MS, deadline } from "@deepseek-ai/dsh-timeout";
4
+ import { brandString } from "@deepseek-ai/dsh-brand";
5
+ //#region lib/types/keys.js
6
+ /** Canonical identities for change requests. */
7
+ /**
8
+ * Build the canonical identity of a change request. Host, owner, and name
9
+ * compare case-insensitively, as they do on the hosts this vocabulary serves.
10
+ * @param ref - change request to identify.
11
+ * @returns `<forge>:<host>/<owner>/<name>#<number>` in lowercase.
12
+ */
13
+ function changeRequestKey(ref) {
14
+ const { forge, host, owner, name } = ref.repository;
15
+ return brandString(`${forge}:${host}/${owner}/${name}#${ref.number}`.toLowerCase());
16
+ }
17
+ //#endregion
18
+ //#region lib/types/schedule.js
19
+ /** Poll cadence for watched change requests. */
20
+ /**
21
+ * Choose the delay before the next poll of one change request. Failures back
22
+ * off exponentially from the active interval; merged change requests stop
23
+ * polling, while closed ones use the idle cadence to detect reopening.
24
+ * @param result - latest snapshot or failure.
25
+ * @param failures - consecutive failures, counting `result` when it is one.
26
+ * @param intervals - configured cadence.
27
+ * @returns the delay in milliseconds, or undefined after a terminal state.
28
+ */
29
+ function nextPollDelay(result, failures, intervals) {
30
+ if ("code" in result) return Math.min(intervals.maxBackoffMs, intervals.activeIntervalMs * 2 ** failures);
31
+ if (result.state === "merged") return void 0;
32
+ if (result.state === "closed") return intervals.idleIntervalMs;
33
+ return result.checksRollup === "pending" || result.mergeability === "unknown" ? intervals.activeIntervalMs : intervals.idleIntervalMs;
34
+ }
35
+ //#endregion
36
+ //#region lib/types/index.js
37
+ /** Registry of code-host providers with provider-neutral change requests and shared status polling. */
38
+ var __addDisposableResource = function(env, value, async) {
39
+ if (value !== null && value !== void 0) {
40
+ if (typeof value !== "object" && typeof value !== "function") throw new TypeError("Object expected.");
41
+ var dispose, inner;
42
+ if (async) {
43
+ if (!Symbol.asyncDispose) throw new TypeError("Symbol.asyncDispose is not defined.");
44
+ dispose = value[Symbol.asyncDispose];
45
+ }
46
+ if (dispose === void 0) {
47
+ if (!Symbol.dispose) throw new TypeError("Symbol.dispose is not defined.");
48
+ dispose = value[Symbol.dispose];
49
+ if (async) inner = dispose;
50
+ }
51
+ if (typeof dispose !== "function") throw new TypeError("Object not disposable.");
52
+ if (inner) dispose = function() {
53
+ try {
54
+ inner.call(this);
55
+ } catch (e) {
56
+ return Promise.reject(e);
57
+ }
58
+ };
59
+ env.stack.push({
60
+ value,
61
+ dispose,
62
+ async
63
+ });
64
+ } else if (async) env.stack.push({ async: true });
65
+ return value;
66
+ };
67
+ var __disposeResources = (function(SuppressedError) {
68
+ return function(env) {
69
+ function fail(e) {
70
+ env.error = env.hasError ? new SuppressedError(e, env.error, "An error was suppressed during disposal.") : e;
71
+ env.hasError = true;
72
+ }
73
+ var r, s = 0;
74
+ function next() {
75
+ while (r = env.stack.pop()) try {
76
+ if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next);
77
+ if (r.dispose) {
78
+ var result = r.dispose.call(r.value);
79
+ if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) {
80
+ fail(e);
81
+ return next();
82
+ });
83
+ } else s |= 1;
84
+ } catch (e) {
85
+ fail(e);
86
+ }
87
+ if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve();
88
+ if (env.hasError) throw env.error;
89
+ }
90
+ return next();
91
+ };
92
+ })(typeof SuppressedError === "function" ? SuppressedError : function(error, suppressed, message) {
93
+ var e = new Error(message);
94
+ return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
95
+ });
96
+ /**
97
+ * Registry of forge providers. Watchers of one change request share a single
98
+ * polling loop; each forge has at most one fetch in flight, which batches
99
+ * every due change request of that forge.
100
+ */
101
+ var ForgeService = class extends Service {
102
+ static Config = z.object({
103
+ activeIntervalMs: z.number().default(3e4),
104
+ idleIntervalMs: z.number().default(12e4),
105
+ maxBackoffMs: z.number().default(6e5),
106
+ fetchTimeoutMs: z.number().default(3e4)
107
+ });
108
+ config;
109
+ providers = /* @__PURE__ */ new Map();
110
+ watches = /* @__PURE__ */ new Map();
111
+ fetching = /* @__PURE__ */ new Set();
112
+ pending = /* @__PURE__ */ new Set();
113
+ lifetime = new AbortController();
114
+ timer;
115
+ /**
116
+ * Poll what is due. A field rather than a method, so the timer runs it on
117
+ * the instance itself: a polling round started through a caller's `ctx.forge`
118
+ * would otherwise run on that caller's shadow receiver, and every later round
119
+ * would nest one more shadow until a property write overflows the stack.
120
+ */
121
+ tick = () => {
122
+ this.timer = void 0;
123
+ this.poll();
124
+ };
125
+ constructor(ctx, config) {
126
+ super(ctx, "forge");
127
+ this.config = config;
128
+ for (const key of [
129
+ "activeIntervalMs",
130
+ "idleIntervalMs",
131
+ "maxBackoffMs",
132
+ "fetchTimeoutMs"
133
+ ]) {
134
+ const value = this.config[key];
135
+ if (!Number.isSafeInteger(value) || value <= 0 || value > MAX_TIMER_DELAY_MS) throw new Error(`forge: ${key} must be a positive integer no greater than ${MAX_TIMER_DELAY_MS}`);
136
+ }
137
+ if (this.config.maxBackoffMs < this.config.activeIntervalMs) throw new Error("forge: maxBackoffMs must be greater than or equal to activeIntervalMs");
138
+ ctx.effect(() => async () => {
139
+ this.lifetime.abort(/* @__PURE__ */ new Error("forge service disposed"));
140
+ clearTimeout(this.timer);
141
+ this.timer = void 0;
142
+ await Promise.allSettled(this.pending);
143
+ }, "forge: stop polling");
144
+ }
145
+ /**
146
+ * Register the provider of one forge family as an effect on the calling
147
+ * fiber. Watches of that forge receive an `unavailable` failure when the
148
+ * registration ends. Unregistering cancels and awaits its in-flight calls;
149
+ * results from the withdrawn registration are discarded.
150
+ * @param provider - provider to serve `provider.forge`.
151
+ * @returns the disposer that unregisters the provider and waits for its calls to finish.
152
+ * @throws when another provider already serves the same forge.
153
+ */
154
+ register(provider) {
155
+ const dispose = this.ctx.effect(() => {
156
+ if (this.providers.has(provider.forge)) throw new Error(`forge: a provider for "${provider.forge}" is already registered`);
157
+ const registration = {
158
+ provider,
159
+ lifetime: new AbortController(),
160
+ pending: /* @__PURE__ */ new Set()
161
+ };
162
+ this.providers.set(provider.forge, registration);
163
+ for (const watch of this.watches.values()) if (watch.ref.repository.forge === provider.forge) watch.dueAt = Date.now();
164
+ this.providersChanged(provider.forge);
165
+ this.schedule();
166
+ return async () => {
167
+ this.providers.delete(provider.forge);
168
+ registration.lifetime.abort(/* @__PURE__ */ new Error(`forge: provider "${provider.forge}" was unregistered`));
169
+ for (const watch of [...this.watches.values()]) if (watch.ref.repository.forge === provider.forge) this.settle(watch, this.missing(provider.forge), void 0);
170
+ this.providersChanged(provider.forge);
171
+ this.schedule();
172
+ await Promise.allSettled(registration.pending);
173
+ };
174
+ }, `forge.register(${provider.forge})`);
175
+ return async () => {
176
+ await dispose();
177
+ };
178
+ }
179
+ /**
180
+ * Map a Git remote URL to a repository through the first provider that serves it.
181
+ * @param url - fetch or push URL as Git reports it.
182
+ * @returns the repository, or undefined when no provider serves the URL.
183
+ */
184
+ repositoryFromRemote(url) {
185
+ for (const { provider } of this.providers.values()) {
186
+ const repository = provider.repositoryFromRemote(url);
187
+ if (repository !== void 0) return repository;
188
+ }
189
+ }
190
+ /**
191
+ * Map a change request web URL through the first provider that serves it.
192
+ * @param url - web URL of a pull or merge request.
193
+ * @returns the change request, or undefined when no provider serves the URL.
194
+ */
195
+ changeRequestFromUrl(url) {
196
+ for (const { provider } of this.providers.values()) {
197
+ const ref = provider.changeRequestFromUrl(url);
198
+ if (ref !== void 0) return ref;
199
+ }
200
+ }
201
+ /**
202
+ * Build the canonical web URL of a change request.
203
+ * @param ref - change request served by a registered provider.
204
+ * @returns the provider's canonical URL.
205
+ * @throws when no provider serves the change request's forge.
206
+ */
207
+ changeRequestUrl(ref) {
208
+ return this.provider(ref.repository.forge).changeRequestUrl(ref);
209
+ }
210
+ /**
211
+ * Find open change requests whose head is `branch`, excluding the default branch and forks.
212
+ * @param repository - repository served by a registered provider.
213
+ * @param branch - head branch name on the host.
214
+ * @param signal - cancellation for the lookup.
215
+ * @returns matching change requests, newest first as the provider reports them.
216
+ * @throws when no provider serves the repository's forge or the lookup fails.
217
+ */
218
+ async findByHead(repository, branch, signal) {
219
+ return await this.invoke(this.registration(repository.forge), signal, (provider, operation) => provider.findByHead(repository, branch, operation));
220
+ }
221
+ /**
222
+ * Report whether the provider can reach the repository's host with current credentials.
223
+ * @param repository - repository served by a registered provider.
224
+ * @param signal - cancellation for the probe.
225
+ * @returns the provider's readiness for the host.
226
+ * @throws when no provider serves the repository's forge.
227
+ */
228
+ async readiness(repository, signal) {
229
+ return await this.invoke(this.registration(repository.forge), signal, (provider, operation) => provider.readiness(repository.host, operation));
230
+ }
231
+ /**
232
+ * Mark a draft change request ready for review, then poll it at once when it is watched.
233
+ * @param ref - change request served by a registered provider.
234
+ * @param signal - cancellation of the request.
235
+ * @returns undefined once the host accepted the change, or why it did not.
236
+ * @throws when no provider serves the change request's forge, or with `signal.reason` when it aborts.
237
+ */
238
+ async markReady(ref, signal) {
239
+ const failure = await this.invoke(this.registration(ref.repository.forge), signal, (provider, operation) => provider.markReady(ref, operation));
240
+ this.refresh(ref);
241
+ return failure;
242
+ }
243
+ /**
244
+ * Merge a change request while its head is still `request.headSha`, then
245
+ * poll it at once when it is watched.
246
+ * @param ref - change request served by a registered provider.
247
+ * @param request - merge method and the head commit the user confirmed.
248
+ * @param signal - cancellation of the request.
249
+ * @returns undefined once the host merged it, or why it did not.
250
+ * @throws when no provider serves the change request's forge, or with `signal.reason` when it aborts.
251
+ */
252
+ async merge(ref, request, signal) {
253
+ const failure = await this.invoke(this.registration(ref.repository.forge), signal, (provider, operation) => provider.merge(ref, request, operation));
254
+ this.refresh(ref);
255
+ return failure;
256
+ }
257
+ /**
258
+ * Read the changes of a change request as its host computes them.
259
+ * @param ref - change request served by a registered provider.
260
+ * @param signal - cancellation of the read.
261
+ * @returns one unified patch with `a/` and `b/` path prefixes, or why the host did not provide it.
262
+ * @throws when no provider serves the change request's forge, or with `signal.reason` when it aborts.
263
+ */
264
+ async diff(ref, signal) {
265
+ return await this.invoke(this.registration(ref.repository.forge), signal, (provider, operation) => provider.diff(ref, operation));
266
+ }
267
+ /**
268
+ * Read the text of one file at a commit of a change request's repository.
269
+ * @param ref - change request served by a registered provider.
270
+ * @param commit - commit object name.
271
+ * @param path - path relative to the repository root.
272
+ * @param signal - cancellation of the read.
273
+ * @returns the text, or why the host did not provide it.
274
+ * @throws when no provider serves the change request's forge, or with `signal.reason` when it aborts.
275
+ */
276
+ async fileText(ref, commit, path, signal) {
277
+ return await this.invoke(this.registration(ref.repository.forge), signal, (provider, operation) => provider.fileText(ref, commit, path, operation));
278
+ }
279
+ /**
280
+ * Build the web page of one file at a commit of a change request's repository.
281
+ * @param ref - change request served by a registered provider.
282
+ * @param commit - commit object name.
283
+ * @param path - path relative to the repository root.
284
+ * @returns the provider's URL.
285
+ * @throws when no provider serves the change request's forge.
286
+ */
287
+ fileUrl(ref, commit, path) {
288
+ return this.provider(ref.repository.forge).fileUrl(ref, commit, path);
289
+ }
290
+ /**
291
+ * Watch one change request as an effect on the calling fiber. The first
292
+ * watcher starts polling at once; listeners receive every later result,
293
+ * and `latest` returns the current one. Polling stops when the last watcher
294
+ * leaves. Listeners must not throw; a thrown error is logged and skipped.
295
+ * @param ref - change request to poll.
296
+ * @param listener - receives each polling result.
297
+ * @returns the disposer that stops this watch.
298
+ */
299
+ watch(ref, listener) {
300
+ const key = changeRequestKey(ref);
301
+ const dispose = this.ctx.effect(() => {
302
+ let watch = this.watches.get(key);
303
+ if (watch === void 0) {
304
+ watch = {
305
+ key,
306
+ ref,
307
+ listeners: /* @__PURE__ */ new Set(),
308
+ latest: void 0,
309
+ dueAt: Date.now(),
310
+ failures: 0
311
+ };
312
+ this.watches.set(key, watch);
313
+ this.schedule();
314
+ }
315
+ const owned = watch;
316
+ owned.listeners.add(listener);
317
+ return () => {
318
+ owned.listeners.delete(listener);
319
+ if (owned.listeners.size === 0 && this.watches.get(key) === owned) {
320
+ this.watches.delete(key);
321
+ this.schedule();
322
+ }
323
+ };
324
+ }, "forge.watch()");
325
+ return () => void dispose();
326
+ }
327
+ /**
328
+ * Poll a watched change request at once, including after a terminal state.
329
+ * A fetch already in flight is followed immediately by another fetch; an
330
+ * unwatched change request is unchanged.
331
+ * @param ref - change request to poll.
332
+ */
333
+ refresh(ref) {
334
+ const watch = this.watches.get(changeRequestKey(ref));
335
+ if (watch === void 0) return;
336
+ watch.dueAt = Date.now();
337
+ this.schedule();
338
+ }
339
+ /**
340
+ * Read the latest polling result of a watched change request.
341
+ * @param ref - change request to read.
342
+ * @returns the latest result, or undefined before the first result or without a watch.
343
+ */
344
+ latest(ref) {
345
+ return this.watches.get(changeRequestKey(ref))?.latest;
346
+ }
347
+ provider(forge) {
348
+ return this.registration(forge).provider;
349
+ }
350
+ providersChanged(forge) {
351
+ if (this.lifetime.signal.aborted) return;
352
+ const task = this.ctx.parallel("forge/providers-changed", forge).catch((error) => {
353
+ this.ctx.logger.warn(`forge: provider change listener failed: ${String(error)}`);
354
+ });
355
+ this.pending.add(task);
356
+ task.then(() => this.pending.delete(task));
357
+ }
358
+ registration(forge) {
359
+ const registration = this.providers.get(forge);
360
+ if (registration === void 0) throw new Error(`forge: ${this.missing(forge).message}`);
361
+ return registration;
362
+ }
363
+ async invoke(registration, signal, action) {
364
+ const operation = AbortSignal.any([
365
+ signal,
366
+ this.lifetime.signal,
367
+ registration.lifetime.signal
368
+ ]);
369
+ operation.throwIfAborted();
370
+ const completion = Promise.withResolvers();
371
+ registration.pending.add(completion.promise);
372
+ this.pending.add(completion.promise);
373
+ try {
374
+ const result = await action(registration.provider, operation);
375
+ operation.throwIfAborted();
376
+ return result;
377
+ } finally {
378
+ registration.pending.delete(completion.promise);
379
+ this.pending.delete(completion.promise);
380
+ completion.resolve();
381
+ }
382
+ }
383
+ missing(forge) {
384
+ return {
385
+ code: "unavailable",
386
+ message: `no provider is registered for "${forge}"`
387
+ };
388
+ }
389
+ schedule() {
390
+ clearTimeout(this.timer);
391
+ this.timer = void 0;
392
+ if (this.lifetime.signal.aborted) return;
393
+ let next;
394
+ for (const watch of this.watches.values()) {
395
+ if (watch.dueAt === void 0 || this.fetching.has(watch.ref.repository.forge)) continue;
396
+ next = next === void 0 ? watch.dueAt : Math.min(next, watch.dueAt);
397
+ }
398
+ if (next === void 0) return;
399
+ this.timer = setTimeout(this.tick, Math.max(0, next - Date.now()));
400
+ }
401
+ poll() {
402
+ const now = Date.now();
403
+ const due = /* @__PURE__ */ new Map();
404
+ for (const watch of this.watches.values()) {
405
+ const forge = watch.ref.repository.forge;
406
+ if (watch.dueAt === void 0 || watch.dueAt > now || this.fetching.has(forge)) continue;
407
+ const batch = due.get(forge);
408
+ if (batch === void 0) due.set(forge, [watch]);
409
+ else batch.push(watch);
410
+ }
411
+ for (const [forge, batch] of due) {
412
+ const task = this.fetch(forge, batch);
413
+ this.pending.add(task);
414
+ task.then(() => this.pending.delete(task));
415
+ }
416
+ this.schedule();
417
+ }
418
+ /** Fetch one forge's batch; never rejects. */
419
+ async fetch(forge, batch) {
420
+ this.fetching.add(forge);
421
+ for (const watch of batch) watch.dueAt = void 0;
422
+ let results;
423
+ let failure;
424
+ const registration = this.providers.get(forge);
425
+ try {
426
+ if (registration === void 0) failure = this.missing(forge);
427
+ else {
428
+ const env_1 = {
429
+ stack: [],
430
+ error: void 0,
431
+ hasError: false
432
+ };
433
+ try {
434
+ const timeout = __addDisposableResource(env_1, deadline(this.lifetime.signal, this.config.fetchTimeoutMs, "FORGE_FETCH_TIMEOUT"), false);
435
+ results = await this.invoke(registration, timeout.signal, (provider, operation) => provider.fetch(batch.map((watch) => watch.ref), operation));
436
+ } catch (e_1) {
437
+ env_1.error = e_1;
438
+ env_1.hasError = true;
439
+ } finally {
440
+ __disposeResources(env_1);
441
+ }
442
+ }
443
+ } catch (error) {
444
+ failure = {
445
+ code: "unavailable",
446
+ message: error instanceof Error ? error.message : String(error)
447
+ };
448
+ } finally {
449
+ this.fetching.delete(forge);
450
+ }
451
+ if (this.lifetime.signal.aborted) return;
452
+ if (registration !== this.providers.get(forge)) {
453
+ this.schedule();
454
+ return;
455
+ }
456
+ for (const watch of batch) {
457
+ if (this.watches.get(watch.key) !== watch) continue;
458
+ const result = failure ?? results?.get(watch.key) ?? {
459
+ code: "unavailable",
460
+ message: `provider returned no result for ${watch.key}`
461
+ };
462
+ this.settle(watch, result, watch.dueAt);
463
+ }
464
+ this.schedule();
465
+ }
466
+ settle(watch, result, refreshAt) {
467
+ const fetchedAt = Date.now();
468
+ let update;
469
+ if ("code" in result) {
470
+ watch.failures++;
471
+ update = {
472
+ key: watch.key,
473
+ fetchedAt,
474
+ failure: result
475
+ };
476
+ } else {
477
+ watch.failures = 0;
478
+ update = {
479
+ key: watch.key,
480
+ fetchedAt,
481
+ snapshot: result
482
+ };
483
+ }
484
+ watch.latest = update;
485
+ const delay = nextPollDelay(result, watch.failures, this.config);
486
+ watch.dueAt = refreshAt ?? (delay === void 0 ? void 0 : fetchedAt + delay);
487
+ for (const listener of [...watch.listeners]) try {
488
+ listener(update);
489
+ } catch (error) {
490
+ this.ctx.logger.warn(`forge: change request listener failed: ${String(error)}`);
491
+ }
492
+ }
493
+ };
494
+ //#endregion
495
+ export { ForgeService, ForgeService as default, changeRequestKey };
@@ -0,0 +1,169 @@
1
+ /** Registry of code-host providers with provider-neutral change requests and shared status polling. */
2
+ import { Context, Service } from '@deepseek-ai/cordis';
3
+ import z from '@deepseek-ai/schemastery';
4
+ import type { ChangeRequestListener, ChangeRequestRef, ChangeRequestUpdate, Config, ForgeActionFailure, ForgeFailure, ForgeProvider, ForgeReadiness, ForgeRepository, MergeRequest } from './types.ts';
5
+ export { changeRequestKey } from './keys.ts';
6
+ export type * from './types.ts';
7
+ declare module '@deepseek-ai/cordis' {
8
+ interface Context {
9
+ forge: ForgeService;
10
+ }
11
+ interface Events {
12
+ /**
13
+ * Announces a provider registration or withdrawal after the registry changes.
14
+ * Listener failures are logged without preventing other listeners from running.
15
+ * @param forge - family whose provider changed.
16
+ * @mode parallel
17
+ */
18
+ 'forge/providers-changed'(forge: string): void | Promise<void>;
19
+ }
20
+ }
21
+ /**
22
+ * Registry of forge providers. Watchers of one change request share a single
23
+ * polling loop; each forge has at most one fetch in flight, which batches
24
+ * every due change request of that forge.
25
+ */
26
+ export declare class ForgeService extends Service {
27
+ static Config: z<Config>;
28
+ private readonly config;
29
+ private readonly providers;
30
+ private readonly watches;
31
+ private readonly fetching;
32
+ private readonly pending;
33
+ private readonly lifetime;
34
+ private timer;
35
+ /**
36
+ * Poll what is due. A field rather than a method, so the timer runs it on
37
+ * the instance itself: a polling round started through a caller's `ctx.forge`
38
+ * would otherwise run on that caller's shadow receiver, and every later round
39
+ * would nest one more shadow until a property write overflows the stack.
40
+ */
41
+ private readonly tick;
42
+ constructor(ctx: Context, config: Config);
43
+ /**
44
+ * Register the provider of one forge family as an effect on the calling
45
+ * fiber. Watches of that forge receive an `unavailable` failure when the
46
+ * registration ends. Unregistering cancels and awaits its in-flight calls;
47
+ * results from the withdrawn registration are discarded.
48
+ * @param provider - provider to serve `provider.forge`.
49
+ * @returns the disposer that unregisters the provider and waits for its calls to finish.
50
+ * @throws when another provider already serves the same forge.
51
+ */
52
+ register(provider: ForgeProvider): () => Promise<void>;
53
+ /**
54
+ * Map a Git remote URL to a repository through the first provider that serves it.
55
+ * @param url - fetch or push URL as Git reports it.
56
+ * @returns the repository, or undefined when no provider serves the URL.
57
+ */
58
+ repositoryFromRemote(url: string): ForgeRepository | undefined;
59
+ /**
60
+ * Map a change request web URL through the first provider that serves it.
61
+ * @param url - web URL of a pull or merge request.
62
+ * @returns the change request, or undefined when no provider serves the URL.
63
+ */
64
+ changeRequestFromUrl(url: string): ChangeRequestRef | undefined;
65
+ /**
66
+ * Build the canonical web URL of a change request.
67
+ * @param ref - change request served by a registered provider.
68
+ * @returns the provider's canonical URL.
69
+ * @throws when no provider serves the change request's forge.
70
+ */
71
+ changeRequestUrl(ref: ChangeRequestRef): string;
72
+ /**
73
+ * Find open change requests whose head is `branch`, excluding the default branch and forks.
74
+ * @param repository - repository served by a registered provider.
75
+ * @param branch - head branch name on the host.
76
+ * @param signal - cancellation for the lookup.
77
+ * @returns matching change requests, newest first as the provider reports them.
78
+ * @throws when no provider serves the repository's forge or the lookup fails.
79
+ */
80
+ findByHead(repository: ForgeRepository, branch: string, signal: AbortSignal): Promise<readonly ChangeRequestRef[]>;
81
+ /**
82
+ * Report whether the provider can reach the repository's host with current credentials.
83
+ * @param repository - repository served by a registered provider.
84
+ * @param signal - cancellation for the probe.
85
+ * @returns the provider's readiness for the host.
86
+ * @throws when no provider serves the repository's forge.
87
+ */
88
+ readiness(repository: ForgeRepository, signal: AbortSignal): Promise<ForgeReadiness>;
89
+ /**
90
+ * Mark a draft change request ready for review, then poll it at once when it is watched.
91
+ * @param ref - change request served by a registered provider.
92
+ * @param signal - cancellation of the request.
93
+ * @returns undefined once the host accepted the change, or why it did not.
94
+ * @throws when no provider serves the change request's forge, or with `signal.reason` when it aborts.
95
+ */
96
+ markReady(ref: ChangeRequestRef, signal: AbortSignal): Promise<ForgeActionFailure | undefined>;
97
+ /**
98
+ * Merge a change request while its head is still `request.headSha`, then
99
+ * poll it at once when it is watched.
100
+ * @param ref - change request served by a registered provider.
101
+ * @param request - merge method and the head commit the user confirmed.
102
+ * @param signal - cancellation of the request.
103
+ * @returns undefined once the host merged it, or why it did not.
104
+ * @throws when no provider serves the change request's forge, or with `signal.reason` when it aborts.
105
+ */
106
+ merge(ref: ChangeRequestRef, request: MergeRequest, signal: AbortSignal): Promise<ForgeActionFailure | undefined>;
107
+ /**
108
+ * Read the changes of a change request as its host computes them.
109
+ * @param ref - change request served by a registered provider.
110
+ * @param signal - cancellation of the read.
111
+ * @returns one unified patch with `a/` and `b/` path prefixes, or why the host did not provide it.
112
+ * @throws when no provider serves the change request's forge, or with `signal.reason` when it aborts.
113
+ */
114
+ diff(ref: ChangeRequestRef, signal: AbortSignal): Promise<string | ForgeFailure>;
115
+ /**
116
+ * Read the text of one file at a commit of a change request's repository.
117
+ * @param ref - change request served by a registered provider.
118
+ * @param commit - commit object name.
119
+ * @param path - path relative to the repository root.
120
+ * @param signal - cancellation of the read.
121
+ * @returns the text, or why the host did not provide it.
122
+ * @throws when no provider serves the change request's forge, or with `signal.reason` when it aborts.
123
+ */
124
+ fileText(ref: ChangeRequestRef, commit: string, path: string, signal: AbortSignal): Promise<string | ForgeFailure>;
125
+ /**
126
+ * Build the web page of one file at a commit of a change request's repository.
127
+ * @param ref - change request served by a registered provider.
128
+ * @param commit - commit object name.
129
+ * @param path - path relative to the repository root.
130
+ * @returns the provider's URL.
131
+ * @throws when no provider serves the change request's forge.
132
+ */
133
+ fileUrl(ref: ChangeRequestRef, commit: string, path: string): string;
134
+ /**
135
+ * Watch one change request as an effect on the calling fiber. The first
136
+ * watcher starts polling at once; listeners receive every later result,
137
+ * and `latest` returns the current one. Polling stops when the last watcher
138
+ * leaves. Listeners must not throw; a thrown error is logged and skipped.
139
+ * @param ref - change request to poll.
140
+ * @param listener - receives each polling result.
141
+ * @returns the disposer that stops this watch.
142
+ */
143
+ watch(ref: ChangeRequestRef, listener: ChangeRequestListener): () => void;
144
+ /**
145
+ * Poll a watched change request at once, including after a terminal state.
146
+ * A fetch already in flight is followed immediately by another fetch; an
147
+ * unwatched change request is unchanged.
148
+ * @param ref - change request to poll.
149
+ */
150
+ refresh(ref: ChangeRequestRef): void;
151
+ /**
152
+ * Read the latest polling result of a watched change request.
153
+ * @param ref - change request to read.
154
+ * @returns the latest result, or undefined before the first result or without a watch.
155
+ */
156
+ latest(ref: ChangeRequestRef): ChangeRequestUpdate | undefined;
157
+ private provider;
158
+ private providersChanged;
159
+ private registration;
160
+ private invoke;
161
+ private missing;
162
+ private schedule;
163
+ private poll;
164
+ /** Fetch one forge's batch; never rejects. */
165
+ private fetch;
166
+ private settle;
167
+ }
168
+ export default ForgeService;
169
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,10 @@
1
+ /** Canonical identities for change requests. */
2
+ import type { ChangeRequestKey, ChangeRequestRef } from './types.ts';
3
+ /**
4
+ * Build the canonical identity of a change request. Host, owner, and name
5
+ * compare case-insensitively, as they do on the hosts this vocabulary serves.
6
+ * @param ref - change request to identify.
7
+ * @returns `<forge>:<host>/<owner>/<name>#<number>` in lowercase.
8
+ */
9
+ export declare function changeRequestKey(ref: ChangeRequestRef): ChangeRequestKey;
10
+ //# sourceMappingURL=keys.d.ts.map
@@ -0,0 +1,22 @@
1
+ /** Poll cadence for watched change requests. */
2
+ import type { ChangeRequestSnapshot, ForgeFailure } from './types.ts';
3
+ /** Resolved intervals that pace polling. */
4
+ export interface PollIntervals {
5
+ /** Delay while checks run or mergeability is unknown. */
6
+ readonly activeIntervalMs: number;
7
+ /** Delay for a closed request or an open request whose checks have settled. */
8
+ readonly idleIntervalMs: number;
9
+ /** Largest delay after consecutive failures. */
10
+ readonly maxBackoffMs: number;
11
+ }
12
+ /**
13
+ * Choose the delay before the next poll of one change request. Failures back
14
+ * off exponentially from the active interval; merged change requests stop
15
+ * polling, while closed ones use the idle cadence to detect reopening.
16
+ * @param result - latest snapshot or failure.
17
+ * @param failures - consecutive failures, counting `result` when it is one.
18
+ * @param intervals - configured cadence.
19
+ * @returns the delay in milliseconds, or undefined after a terminal state.
20
+ */
21
+ export declare function nextPollDelay(result: ChangeRequestSnapshot | ForgeFailure, failures: number, intervals: PollIntervals): number | undefined;
22
+ //# sourceMappingURL=schedule.d.ts.map
@@ -0,0 +1,192 @@
1
+ /** Provider-neutral vocabulary for code hosts, their change requests, and provider contracts. */
2
+ import type { Branded } from '@deepseek-ai/dsh-brand';
3
+ /** One repository on a code host, identified the way its provider addresses it. */
4
+ export interface ForgeRepository {
5
+ /** Provider family that serves the repository, such as `github`. */
6
+ readonly forge: string;
7
+ /** Lowercase host name, such as `github.com`. */
8
+ readonly host: string;
9
+ /** Owner or namespace path as the host spells it. */
10
+ readonly owner: string;
11
+ /** Repository name as the host spells it. */
12
+ readonly name: string;
13
+ }
14
+ /** One change request (pull request or merge request) in a repository. */
15
+ export interface ChangeRequestRef {
16
+ /** Repository that holds the change request. */
17
+ readonly repository: ForgeRepository;
18
+ /** Host-assigned change request number. */
19
+ readonly number: number;
20
+ }
21
+ /** Canonical identity of a change request across providers; built by `changeRequestKey`. */
22
+ export type ChangeRequestKey = Branded<'ChangeRequestKey'>;
23
+ /** Lifecycle state of a change request. */
24
+ export type ChangeRequestState = 'open' | 'closed' | 'merged';
25
+ /** Normalized outcome of one status check. */
26
+ export type CheckState = 'pending' | 'success' | 'failure' | 'neutral' | 'skipped' | 'cancelled';
27
+ /** One status check reported for the head commit of a change request. */
28
+ export interface ChangeRequestCheck {
29
+ /** Check name as the host displays it. */
30
+ readonly name: string;
31
+ /** Normalized check outcome. */
32
+ readonly state: CheckState;
33
+ /** Web page for the check, or null when the host reports none. */
34
+ readonly url: string | null;
35
+ /** Workflow that produced the check, or null when the host reports none. */
36
+ readonly workflow: string | null;
37
+ /** ISO 8601 time the check started, or null before it starts and when the host reports none. */
38
+ readonly startedAt: string | null;
39
+ /** ISO 8601 time the check completed, or null while it runs and when the host reports none. */
40
+ readonly completedAt: string | null;
41
+ }
42
+ /** Combined outcome of all checks on the head commit. */
43
+ export type ChecksRollup = 'none' | 'pending' | 'success' | 'failure';
44
+ /** Whether the change request can merge into its base without conflicts. */
45
+ export type Mergeability = 'mergeable' | 'conflicting' | 'unknown';
46
+ /** Aggregate review outcome required by the host's merge rules. */
47
+ export type ReviewDecision = 'approved' | 'changes-requested' | 'review-required' | 'none';
48
+ /** How a merge brings the commits of a change request into its base branch. */
49
+ export type MergeMethod = 'merge' | 'squash' | 'rebase';
50
+ /** Status of one change request at the time its provider read it. */
51
+ export interface ChangeRequestSnapshot {
52
+ /** Change request the snapshot describes. */
53
+ readonly ref: ChangeRequestRef;
54
+ /** Web page of the change request. */
55
+ readonly url: string;
56
+ /** Title as the author wrote it. */
57
+ readonly title: string;
58
+ /** Lifecycle state. */
59
+ readonly state: ChangeRequestState;
60
+ /** Whether the author marked the change request as a draft. */
61
+ readonly draft: boolean;
62
+ /** Author login, or null when the host hides it. */
63
+ readonly author: string | null;
64
+ /** Branch that carries the proposed commits. */
65
+ readonly headBranch: string;
66
+ /** Commit object name at the head of the change request. */
67
+ readonly headSha: string;
68
+ /** Branch the change request merges into. */
69
+ readonly baseBranch: string;
70
+ /** Lines the change request adds. */
71
+ readonly additions: number;
72
+ /** Lines the change request deletes. */
73
+ readonly deletions: number;
74
+ /** Merge conflict status; `unknown` while the host computes it. */
75
+ readonly mergeability: Mergeability;
76
+ /**
77
+ * Whether the host would merge the change request now for the signed-in
78
+ * user: open, not a draft, and meeting the base branch's merge rules.
79
+ */
80
+ readonly mergeReady: boolean;
81
+ /** Merge methods the repository accepts, the signed-in user's usual one first. */
82
+ readonly mergeMethods: readonly MergeMethod[];
83
+ /** Aggregate review outcome. */
84
+ readonly reviewDecision: ReviewDecision;
85
+ /** Checks on the head commit in host order. */
86
+ readonly checks: readonly ChangeRequestCheck[];
87
+ /** Combined check outcome. */
88
+ readonly checksRollup: ChecksRollup;
89
+ /** Whether the host reported more checks than `checks` contains. */
90
+ readonly checksTruncated: boolean;
91
+ /** Number of review threads that are not resolved. */
92
+ readonly unresolvedThreads: number;
93
+ /** ISO 8601 time of the host's last update to the change request. */
94
+ readonly updatedAt: string;
95
+ /** ISO 8601 time the change request was merged or closed, or null while it is open. */
96
+ readonly closedAt: string | null;
97
+ }
98
+ /** Why a provider could not read a change request. */
99
+ export interface ForgeFailure {
100
+ /**
101
+ * `not-found`: the host does not expose it; `unauthenticated`: credentials are missing or rejected;
102
+ * `too-large`: the host or the provider refuses an answer over its size limit, so reading again fails
103
+ * the same way; `unavailable`: any other failure.
104
+ */
105
+ readonly code: 'not-found' | 'unauthenticated' | 'too-large' | 'unavailable';
106
+ /** Human-readable cause. */
107
+ readonly message: string;
108
+ }
109
+ /** Latest polling result delivered to watchers of one change request. */
110
+ export type ChangeRequestUpdate = {
111
+ /** Canonical identity of the watched change request. */
112
+ readonly key: ChangeRequestKey;
113
+ /** Epoch milliseconds when the result settled. */
114
+ readonly fetchedAt: number;
115
+ } & ({
116
+ /** Status read from the host. */
117
+ readonly snapshot: ChangeRequestSnapshot;
118
+ } | {
119
+ /** Reason the status could not be read. */
120
+ readonly failure: ForgeFailure;
121
+ });
122
+ /** A merge the user confirmed for the head commit they saw. */
123
+ export interface MergeRequest {
124
+ /** How the commits enter the base branch. */
125
+ readonly method: MergeMethod;
126
+ /** Commit object name the head must still have; the merge is refused once the head moves. */
127
+ readonly headSha: string;
128
+ }
129
+ /** Why a provider could not change a change request. */
130
+ export interface ForgeActionFailure {
131
+ /**
132
+ * `unauthenticated`: credentials are missing or rejected; `stale`: the head
133
+ * moved past the confirmed commit; `failed`: the host refused or could not be reached.
134
+ */
135
+ readonly code: 'unauthenticated' | 'stale' | 'failed';
136
+ /** Human-readable cause, usually the host's own message. */
137
+ readonly message: string;
138
+ }
139
+ /** Whether a provider can reach one host with the current credentials. */
140
+ export interface ForgeReadiness {
141
+ /** `ready`, `unauthenticated` (sign-in needed), or `unavailable` (tool missing or host unreachable). */
142
+ readonly state: 'ready' | 'unauthenticated' | 'unavailable';
143
+ /** Human-readable detail, or null when ready. */
144
+ readonly message: string | null;
145
+ }
146
+ /**
147
+ * Host-specific access registered with `ctx.forge`. One provider serves one
148
+ * forge family; methods that take a signal must stop work when it aborts.
149
+ */
150
+ export interface ForgeProvider {
151
+ /** Forge family name stored in every repository this provider returns. */
152
+ readonly forge: string;
153
+ /** Map a Git remote URL to its repository, or undefined when this provider does not serve it. */
154
+ repositoryFromRemote(url: string): ForgeRepository | undefined;
155
+ /** Map a change request web URL to its reference, or undefined when this provider does not serve it. */
156
+ changeRequestFromUrl(url: string): ChangeRequestRef | undefined;
157
+ /** Build the canonical web URL of a change request this provider serves. */
158
+ changeRequestUrl(ref: ChangeRequestRef): string;
159
+ /** Read the status of several change requests, returning one result per requested key. */
160
+ fetch(refs: readonly ChangeRequestRef[], signal: AbortSignal): Promise<ReadonlyMap<ChangeRequestKey, ChangeRequestSnapshot | ForgeFailure>>;
161
+ /** Find open change requests whose head is `branch` in `repository`, excluding the default branch and forks. */
162
+ findByHead(repository: ForgeRepository, branch: string, signal: AbortSignal): Promise<readonly ChangeRequestRef[]>;
163
+ /** Report whether the provider can reach `host` with current credentials. */
164
+ readiness(host: string, signal: AbortSignal): Promise<ForgeReadiness>;
165
+ /** Mark a draft change request ready for review, resolving to undefined once the host accepted it. */
166
+ markReady(ref: ChangeRequestRef, signal: AbortSignal): Promise<ForgeActionFailure | undefined>;
167
+ /** Merge a change request while its head is still `request.headSha`, resolving to undefined once merged. */
168
+ merge(ref: ChangeRequestRef, request: MergeRequest, signal: AbortSignal): Promise<ForgeActionFailure | undefined>;
169
+ /** Read the changes of a change request as the host computes them, as one unified patch with `a/` and `b/` path prefixes. */
170
+ diff(ref: ChangeRequestRef, signal: AbortSignal): Promise<string | ForgeFailure>;
171
+ /** Read the text of `path` at `commit` in the change request's repository. */
172
+ fileText(ref: ChangeRequestRef, commit: string, path: string, signal: AbortSignal): Promise<string | ForgeFailure>;
173
+ /** Build the web page of `path` at `commit` in the change request's repository. */
174
+ fileUrl(ref: ChangeRequestRef, commit: string, path: string): string;
175
+ }
176
+ /**
177
+ * Receives every polling result for one watched change request.
178
+ * @param update - the settled result.
179
+ */
180
+ export type ChangeRequestListener = (update: ChangeRequestUpdate) => void;
181
+ /** Poll cadence and fetch deadline for watched change requests. */
182
+ export interface Config {
183
+ /** Poll interval in milliseconds while checks run or mergeability is unknown. */
184
+ activeIntervalMs?: number;
185
+ /** Poll interval in milliseconds for a closed request or an open request whose checks have settled. */
186
+ idleIntervalMs?: number;
187
+ /** Largest delay in milliseconds between polls after consecutive failures. */
188
+ maxBackoffMs?: number;
189
+ /** Deadline in milliseconds for one provider fetch. */
190
+ fetchTimeoutMs?: number;
191
+ }
192
+ //# sourceMappingURL=types.d.ts.map
package/locale/de.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "Änderungsanfragen",
4
+ "description": "Registrieren Sie Code-Hosting-Anbieter und fragen Sie den Status von Änderungsanfragen regelmäßig über einen anbieterneutralen Dienst ab."
5
+ }
6
+ }
package/locale/en.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "Change requests",
4
+ "description": "Register code host providers and poll change request status through one provider-neutral service."
5
+ }
6
+ }
package/locale/es.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "Solicitudes de cambio",
4
+ "description": "Registre proveedores de alojamiento de código y consulte periódicamente el estado de las solicitudes de cambio mediante un único servicio independiente del proveedor."
5
+ }
6
+ }
package/locale/fr.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "Demandes de modification",
4
+ "description": "Enregistrez des fournisseurs d'hébergement de code et interrogez régulièrement l'état des demandes de modification via un seul service indépendant du fournisseur."
5
+ }
6
+ }
package/locale/ja.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "変更リクエスト",
4
+ "description": "プロバイダーに依存しない 1 つのサービスで、コードホストのプロバイダーを登録し、変更リクエストの状態をポーリングします。"
5
+ }
6
+ }
package/locale/pt.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "Solicitações de alteração",
4
+ "description": "Registre provedores de hospedagem de código e consulte periodicamente o status das solicitações de alteração por meio de um único serviço independente de provedor."
5
+ }
6
+ }
package/locale/ru.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "Запросы на изменение",
4
+ "description": "Зарегистрируйте провайдеров хостинга кода и опрашивайте статус запросов на изменение через единый сервис, не зависящий от провайдера."
5
+ }
6
+ }
package/locale/zh.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "变更请求",
4
+ "description": "通过一个与提供方无关的服务注册代码托管提供方,并轮询变更请求状态。"
5
+ }
6
+ }
package/package.json CHANGED
@@ -1,6 +1,45 @@
1
1
  {
2
2
  "name": "@deepseek-ai/dsh-experimental-forge",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "description": "Provider-neutral change request registry with shared status polling",
4
+ "version": "0.2.2-alpha.2",
5
+ "publishConfig": {
6
+ "access": "public"
7
+ },
8
+ "type": "module",
9
+ "main": "lib/index.js",
10
+ "types": "lib/types/index.d.ts",
11
+ "exports": {
12
+ ".": {
13
+ "types": "./lib/types/index.d.ts",
14
+ "default": "./lib/index.js"
15
+ },
16
+ "./types": {
17
+ "types": "./lib/types/types.d.ts"
18
+ },
19
+ "./locale/*.json": "./locale/*.json",
20
+ "./src/*": "./src/*",
21
+ "./package.json": "./package.json"
22
+ },
23
+ "files": [
24
+ "locale/*.json",
25
+ "lib/index.js",
26
+ "lib/types/**/*.d.ts"
27
+ ],
28
+ "license": "MIT",
29
+ "repository": {
30
+ "type": "git",
31
+ "url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
32
+ "directory": "packages/experimental/forge"
33
+ },
34
+ "dependencies": {
35
+ "@deepseek-ai/schemastery": "~3.18.6-alpha.1",
36
+ "@deepseek-ai/dsh-brand": "0.2.2-alpha.2",
37
+ "@deepseek-ai/dsh-timeout": "0.2.2-alpha.2"
38
+ },
39
+ "peerDependencies": {
40
+ "@deepseek-ai/cordis": "~4.0.6-alpha.1"
41
+ },
42
+ "devDependencies": {
43
+ "@deepseek-ai/cordis": "~4.0.6-alpha.1"
44
+ }
6
45
  }