@lmzhen/dsh-evolution-approval 0.26.26 → 0.27.1
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/lib/index.js +134 -150
- package/lib/types/index.d.ts +6 -2
- package/lib/types/routes.d.ts +21 -13
- package/package.json +13 -11
package/lib/index.js
CHANGED
|
@@ -1,93 +1,82 @@
|
|
|
1
1
|
import { Service } from "@deepseek-ai/cordis";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
|
-
import { clampOnce, elapsedSince,
|
|
3
|
+
import { clampOnce, elapsedSince, textDiffFacts } from "@lmzhen/dsh-evolution-core";
|
|
4
4
|
import z from "@deepseek-ai/schemastery";
|
|
5
5
|
import { PENDING_STATUSES } from "@lmzhen/dsh-evolution-state-storage";
|
|
6
|
+
import { HOST_ROUTES, json, methodNotAllowed, readJsonObject, registerHostRoutes } from "@lmzhen/dsh-evolution-core/http-routes";
|
|
6
7
|
//#region lib/types/routes.js
|
|
7
8
|
/**
|
|
8
|
-
* The approval HTTP surface: four
|
|
9
|
+
* The approval HTTP surface: four exact Fetch routes over the write gate's pending window.
|
|
10
|
+
*
|
|
11
|
+
* W6/G4: the carrier is the platform's Host connection data plane (`connection.fetch`) — the trust
|
|
12
|
+
* fence, browser auth and owner-only admission are applied by the platform BEFORE `fetch(request)`
|
|
13
|
+
* runs, so this module has no fence and no 403/401 branch. It still answers the same JSON envelope
|
|
14
|
+
* the browser half reads; the loopback f-string that used to live here was a SECOND admission path
|
|
15
|
+
* (the S2 spike showed it could diverge from the platform's).
|
|
9
16
|
*
|
|
10
17
|
* Layering: this module maps a route to a USE CASE and does nothing else. The pending window itself
|
|
11
18
|
* (ids, statuses, claim/resolve rules, the replay runners) lives in this package's service and in
|
|
12
19
|
* `evolution-state-storage`; the routes only read the window and forward the two decisions, so the
|
|
13
20
|
* console command face (`/evolution pending`, `approve <id>`) and this one cannot drift — both call
|
|
14
21
|
* the same methods.
|
|
15
|
-
*
|
|
16
|
-
* The browser half never touches the state medium: it lists, it approves, it rejects.
|
|
17
22
|
* @module @lmzhen/dsh-evolution-approval/routes
|
|
18
23
|
*/
|
|
19
|
-
/** Route paths
|
|
20
|
-
const APPROVAL_ROUTES =
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
/** Route paths: the ONE declaration lives in core (W1) and the browser half is generated from it. */
|
|
25
|
+
const APPROVAL_ROUTES = HOST_ROUTES.approval;
|
|
26
|
+
/**
|
|
27
|
+
* Run one decision against the service.
|
|
28
|
+
*
|
|
29
|
+
* A refusal is a BUSINESS answer, not a transport failure: the body carries the service's own
|
|
30
|
+
* sentence — the same one the console command prints — so the two faces cannot drift. A throw is
|
|
31
|
+
* reported the same way rather than escaping as an empty 500, because the operator's next step
|
|
32
|
+
* ("reject it", "retry") is only actionable if the reason is visible.
|
|
33
|
+
*/
|
|
34
|
+
async function decide(run, id) {
|
|
35
|
+
try {
|
|
36
|
+
const result = await run(id);
|
|
37
|
+
return json(200, result.ok ? {
|
|
38
|
+
ok: true,
|
|
39
|
+
data: result
|
|
40
|
+
} : {
|
|
41
|
+
ok: false,
|
|
42
|
+
code: "approval-refused",
|
|
43
|
+
message: result.message
|
|
44
|
+
});
|
|
45
|
+
} catch (error) {
|
|
46
|
+
return json(200, {
|
|
47
|
+
ok: false,
|
|
48
|
+
code: "approval-failed",
|
|
49
|
+
message: error instanceof Error ? error.message : String(error)
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/** The one shape a decision route accepts: a non-empty string id. */
|
|
54
|
+
function idOf(payload) {
|
|
55
|
+
return typeof payload?.id === "string" ? payload.id.trim() : "";
|
|
56
|
+
}
|
|
26
57
|
/**
|
|
27
|
-
* Build the four routes over the approval service.
|
|
58
|
+
* Build the four exact Fetch routes over the approval service.
|
|
28
59
|
* @param approval - the host's approval face (the service instance, or a spec fixture).
|
|
29
|
-
* @
|
|
60
|
+
* @param audit - the DecisionAudit captured where the connection service is known (registration).
|
|
61
|
+
* @returns the route list for `registerHostRoutes`.
|
|
30
62
|
*/
|
|
31
|
-
function makeApprovalRoutes(approval) {
|
|
32
|
-
/** Guard: trust fence, then method. Every refusal is a JSON body, never an empty response. */
|
|
33
|
-
const guard = (req, res, method) => {
|
|
34
|
-
if (!isLoopbackRequest(req)) {
|
|
35
|
-
writeJson(res, 403, { error: "禁止访问:仅限回环地址" });
|
|
36
|
-
return false;
|
|
37
|
-
}
|
|
38
|
-
if (req.method !== method) {
|
|
39
|
-
writeJson(res, 405, { error: "方法不允许:" + method });
|
|
40
|
-
return false;
|
|
41
|
-
}
|
|
42
|
-
return true;
|
|
43
|
-
};
|
|
44
|
-
/**
|
|
45
|
-
* Run one decision against the service.
|
|
46
|
-
*
|
|
47
|
-
* A refusal is a BUSINESS answer, not a transport failure: the body carries the service's own
|
|
48
|
-
* sentence — the same one the console command prints — so the two faces cannot drift. A throw is
|
|
49
|
-
* reported the same way rather than escaping as an empty 500, because the operator's next step
|
|
50
|
-
* ("reject it", "retry") is only actionable if the reason is visible.
|
|
51
|
-
*/
|
|
52
|
-
const decide = async (res, run, id) => {
|
|
53
|
-
try {
|
|
54
|
-
const result = await run(id);
|
|
55
|
-
writeJson(res, 200, result.ok ? {
|
|
56
|
-
ok: true,
|
|
57
|
-
data: result
|
|
58
|
-
} : {
|
|
59
|
-
ok: false,
|
|
60
|
-
code: "approval-refused",
|
|
61
|
-
message: result.message
|
|
62
|
-
});
|
|
63
|
-
} catch (error) {
|
|
64
|
-
writeJson(res, 200, {
|
|
65
|
-
ok: false,
|
|
66
|
-
code: "approval-failed",
|
|
67
|
-
message: error instanceof Error ? error.message : String(error)
|
|
68
|
-
});
|
|
69
|
-
}
|
|
70
|
-
};
|
|
71
|
-
/** The one shape a decision route accepts: a non-empty string id. */
|
|
72
|
-
const idOf = (payload) => typeof payload?.id === "string" ? payload.id.trim() : "";
|
|
63
|
+
function makeApprovalRoutes(approval, audit) {
|
|
73
64
|
return [
|
|
74
65
|
{
|
|
75
|
-
kind: "exact",
|
|
76
66
|
path: APPROVAL_ROUTES.pending,
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
67
|
+
methods: ["GET"],
|
|
68
|
+
requestBody: "buffered",
|
|
69
|
+
fetch: async (request) => {
|
|
70
|
+
if (request.method !== "GET") return methodNotAllowed(["GET"]);
|
|
71
|
+
const raw = new URL(request.url).searchParams.get("status")?.trim() ?? "";
|
|
80
72
|
const status = raw === "" ? "pending" : raw;
|
|
81
|
-
if (!PENDING_STATUSES.includes(status)) {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
});
|
|
87
|
-
return;
|
|
88
|
-
}
|
|
73
|
+
if (!PENDING_STATUSES.includes(status)) return json(400, {
|
|
74
|
+
ok: false,
|
|
75
|
+
code: "bad-request",
|
|
76
|
+
message: "status 必须是以下之一:" + PENDING_STATUSES.join(", ")
|
|
77
|
+
});
|
|
89
78
|
try {
|
|
90
|
-
|
|
79
|
+
return json(200, {
|
|
91
80
|
ok: true,
|
|
92
81
|
data: (await approval.list(status)).map((record) => ({
|
|
93
82
|
id: record.id,
|
|
@@ -100,7 +89,7 @@ function makeApprovalRoutes(approval) {
|
|
|
100
89
|
}))
|
|
101
90
|
});
|
|
102
91
|
} catch (error) {
|
|
103
|
-
|
|
92
|
+
return json(200, {
|
|
104
93
|
ok: false,
|
|
105
94
|
code: "approval-failed",
|
|
106
95
|
message: error instanceof Error ? error.message : String(error)
|
|
@@ -109,53 +98,42 @@ function makeApprovalRoutes(approval) {
|
|
|
109
98
|
}
|
|
110
99
|
},
|
|
111
100
|
{
|
|
112
|
-
kind: "exact",
|
|
113
101
|
path: APPROVAL_ROUTES.preview,
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
if (
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
}
|
|
102
|
+
methods: ["GET"],
|
|
103
|
+
requestBody: "buffered",
|
|
104
|
+
fetch: async (request) => {
|
|
105
|
+
if (request.method !== "GET") return methodNotAllowed(["GET"]);
|
|
106
|
+
const id = new URL(request.url).searchParams.get("id")?.trim() ?? "";
|
|
107
|
+
if (id === "") return json(400, {
|
|
108
|
+
ok: false,
|
|
109
|
+
code: "bad-request",
|
|
110
|
+
message: "缺少 id"
|
|
111
|
+
});
|
|
125
112
|
try {
|
|
126
113
|
const record = [...await approval.list("pending"), ...await approval.list("executing")].find((candidate) => candidate.id === id);
|
|
127
|
-
if (record === void 0) {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
});
|
|
133
|
-
return;
|
|
134
|
-
}
|
|
114
|
+
if (record === void 0) return json(200, {
|
|
115
|
+
ok: false,
|
|
116
|
+
code: "not-found",
|
|
117
|
+
message: "没有 id 为「" + id + "」的待批准写入"
|
|
118
|
+
});
|
|
135
119
|
const preview = approval.previewOf(record.kind);
|
|
136
|
-
if (preview === void 0) {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
});
|
|
144
|
-
return;
|
|
145
|
-
}
|
|
120
|
+
if (preview === void 0) return json(200, {
|
|
121
|
+
ok: true,
|
|
122
|
+
data: {
|
|
123
|
+
available: false,
|
|
124
|
+
reason: "类型 \"" + record.kind + "\" 的写入没有可预览内容"
|
|
125
|
+
}
|
|
126
|
+
});
|
|
146
127
|
const answer = await preview(record.args);
|
|
147
|
-
if (!answer.available) {
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
});
|
|
155
|
-
return;
|
|
156
|
-
}
|
|
128
|
+
if (!answer.available) return json(200, {
|
|
129
|
+
ok: true,
|
|
130
|
+
data: {
|
|
131
|
+
available: false,
|
|
132
|
+
reason: answer.reason
|
|
133
|
+
}
|
|
134
|
+
});
|
|
157
135
|
const facts = textDiffFacts(answer.before ?? "", answer.after ?? "", answer.path);
|
|
158
|
-
|
|
136
|
+
return json(200, {
|
|
159
137
|
ok: true,
|
|
160
138
|
data: {
|
|
161
139
|
available: true,
|
|
@@ -164,7 +142,7 @@ function makeApprovalRoutes(approval) {
|
|
|
164
142
|
}
|
|
165
143
|
});
|
|
166
144
|
} catch (error) {
|
|
167
|
-
|
|
145
|
+
return json(200, {
|
|
168
146
|
ok: false,
|
|
169
147
|
code: "approval-failed",
|
|
170
148
|
message: error instanceof Error ? error.message : String(error)
|
|
@@ -173,37 +151,33 @@ function makeApprovalRoutes(approval) {
|
|
|
173
151
|
}
|
|
174
152
|
},
|
|
175
153
|
{
|
|
176
|
-
kind: "exact",
|
|
177
154
|
path: APPROVAL_ROUTES.approve,
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
if (
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
await decide(res, (current) => approval.approve(current), id);
|
|
155
|
+
methods: ["POST"],
|
|
156
|
+
requestBody: "buffered",
|
|
157
|
+
fetch: async (request) => {
|
|
158
|
+
if (request.method !== "POST") return methodNotAllowed(["POST"]);
|
|
159
|
+
const id = idOf(await readJsonObject(request));
|
|
160
|
+
if (id === "") return json(400, {
|
|
161
|
+
ok: false,
|
|
162
|
+
code: "bad-request",
|
|
163
|
+
message: "缺少 id"
|
|
164
|
+
});
|
|
165
|
+
return await decide((current) => approval.approve(current, audit), id);
|
|
190
166
|
}
|
|
191
167
|
},
|
|
192
168
|
{
|
|
193
|
-
kind: "exact",
|
|
194
169
|
path: APPROVAL_ROUTES.reject,
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
if (
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
await decide(res, (current) => approval.reject(current), id);
|
|
170
|
+
methods: ["POST"],
|
|
171
|
+
requestBody: "buffered",
|
|
172
|
+
fetch: async (request) => {
|
|
173
|
+
if (request.method !== "POST") return methodNotAllowed(["POST"]);
|
|
174
|
+
const id = idOf(await readJsonObject(request));
|
|
175
|
+
if (id === "") return json(400, {
|
|
176
|
+
ok: false,
|
|
177
|
+
code: "bad-request",
|
|
178
|
+
message: "缺少 id"
|
|
179
|
+
});
|
|
180
|
+
return await decide((current) => approval.reject(current, audit), id);
|
|
207
181
|
}
|
|
208
182
|
}
|
|
209
183
|
];
|
|
@@ -300,14 +274,7 @@ var EvolutionApproval = class extends Service {
|
|
|
300
274
|
const disposer = settings.configure({ auto: false }, this.ctx.fiber);
|
|
301
275
|
if (typeof disposer === "function") this.ctx.effect(() => disposer, "evolution-approval: settings presentation");
|
|
302
276
|
});
|
|
303
|
-
this.ctx
|
|
304
|
-
scope.effect(() => {
|
|
305
|
-
const disposers = makeApprovalRoutes(this).map((route) => scope.webServer.register(route));
|
|
306
|
-
return () => {
|
|
307
|
-
for (const dispose of disposers) dispose();
|
|
308
|
-
};
|
|
309
|
-
}, "evolution-approval: routes");
|
|
310
|
-
});
|
|
277
|
+
registerHostRoutes(this.ctx, (audit) => makeApprovalRoutes(this, audit));
|
|
311
278
|
}
|
|
312
279
|
state() {
|
|
313
280
|
return this.ctx.evolutionState;
|
|
@@ -446,8 +413,8 @@ var EvolutionApproval = class extends Service {
|
|
|
446
413
|
await this.expireStale();
|
|
447
414
|
return await this.state().listPending(status);
|
|
448
415
|
}
|
|
449
|
-
async approve(id) {
|
|
450
|
-
return await this.dedupe(`approve:${id}`, () => this.doApprove(id));
|
|
416
|
+
async approve(id, audit) {
|
|
417
|
+
return await this.dedupe(`approve:${id}`, () => this.doApprove(id, audit));
|
|
451
418
|
}
|
|
452
419
|
/** S2-P2-22 (0.3.80): return an ORPHANED executing record to the pending
|
|
453
420
|
* window. "Orphaned" is DECIDED here, never assumed: the older rationale
|
|
@@ -513,7 +480,7 @@ var EvolutionApproval = class extends Service {
|
|
|
513
480
|
message: `已把「${id}」放回待批准窗口。${holderPid === null ? " Its claim carries no pid, so liveness could NOT be verified: another process may still be running this approve." : ""} 批准前请先核验效果:批准会重跑该写入,非幂等的重放会造成效果重复;改为拒绝则可关闭该记录。`
|
|
514
481
|
};
|
|
515
482
|
}
|
|
516
|
-
async reject(id) {
|
|
483
|
+
async reject(id, audit) {
|
|
517
484
|
return await this.dedupe(`reject:${id}`, async () => {
|
|
518
485
|
const claimId = this.newClaimId();
|
|
519
486
|
if (!await this.state().claimPending(id, claimId)) {
|
|
@@ -537,6 +504,7 @@ var EvolutionApproval = class extends Service {
|
|
|
537
504
|
message: `待批准写入「${id}」已不在待批准状态(已了结或不存在)。`
|
|
538
505
|
};
|
|
539
506
|
}
|
|
507
|
+
await this.recordDecision(resolution.record, audit);
|
|
540
508
|
return {
|
|
541
509
|
ok: true,
|
|
542
510
|
message: `已拒绝 ${resolution.record.kind} 写入「${id}」。`
|
|
@@ -577,7 +545,20 @@ var EvolutionApproval = class extends Service {
|
|
|
577
545
|
this.inFlight.set(id, run);
|
|
578
546
|
return run;
|
|
579
547
|
}
|
|
580
|
-
|
|
548
|
+
/** W6: attach the platform's admission decision to a resolved record (read-tolerant; absent on
|
|
549
|
+
* records resolved by expiry or before 0.2.1). */
|
|
550
|
+
async recordDecision(record, audit) {
|
|
551
|
+
if (record === null) return;
|
|
552
|
+
try {
|
|
553
|
+
await this.state().savePending({
|
|
554
|
+
...record,
|
|
555
|
+
decidedBy: audit
|
|
556
|
+
});
|
|
557
|
+
} catch (error) {
|
|
558
|
+
this.ctx.logger.warn("evolution-approval: could not attach decidedBy audit: " + (error instanceof Error ? error.message : String(error)));
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
async doApprove(id, audit) {
|
|
581
562
|
const claimId = this.newClaimId();
|
|
582
563
|
const record = await this.state().claimPending(id, claimId);
|
|
583
564
|
if (!record) {
|
|
@@ -593,7 +574,8 @@ var EvolutionApproval = class extends Service {
|
|
|
593
574
|
const runner = this.runners.get(record.kind);
|
|
594
575
|
if (!runner) {
|
|
595
576
|
if (record.kind === "capability") {
|
|
596
|
-
|
|
577
|
+
const resolution = await this.state().tryResolvePending(id, "approved", claimId);
|
|
578
|
+
if (!resolution.applied) {
|
|
597
579
|
if ((await this.state().listPending("rejected")).find((item) => item.id === id)) return {
|
|
598
580
|
ok: false,
|
|
599
581
|
message: `能力「${id}」已获批准,但记录被并发地了结为「rejected」:没有代码运行,也无需重放;重新提交前请先核验。`
|
|
@@ -603,6 +585,7 @@ var EvolutionApproval = class extends Service {
|
|
|
603
585
|
message: `待批准写入「${id}」已被另一个写入方了结。`
|
|
604
586
|
};
|
|
605
587
|
}
|
|
588
|
+
await this.recordDecision(resolution.record, audit);
|
|
606
589
|
return {
|
|
607
590
|
ok: true,
|
|
608
591
|
message: "能力已批准,可在 Creator 模式下手动激活(未执行任何代码)。"
|
|
@@ -649,6 +632,7 @@ var EvolutionApproval = class extends Service {
|
|
|
649
632
|
message: `待批准写入「${id}」已被另一个写入方了结。`
|
|
650
633
|
};
|
|
651
634
|
}
|
|
635
|
+
await this.recordDecision(resolution.record, audit);
|
|
652
636
|
return {
|
|
653
637
|
ok: true,
|
|
654
638
|
message: `已批准 ${record.kind} 写入「${id}」(${record.summary})。`
|
package/lib/types/index.d.ts
CHANGED
|
@@ -21,6 +21,7 @@ import type { Volatile } from '@deepseek-ai/cordis';
|
|
|
21
21
|
import { type WriteConfirmMode } from '@lmzhen/dsh-evolution-core';
|
|
22
22
|
import z from '@deepseek-ai/schemastery';
|
|
23
23
|
import type { PendingKind, PendingRecord, PendingStatus } from '@lmzhen/dsh-evolution-state-storage';
|
|
24
|
+
import type { DecisionAudit } from '@lmzhen/dsh-evolution-core/http-routes';
|
|
24
25
|
export type { PendingKind, PendingRecord, PendingStatus };
|
|
25
26
|
export type WriteRunner = (args: unknown) => Promise<{
|
|
26
27
|
ok: boolean;
|
|
@@ -262,7 +263,7 @@ export declare class EvolutionApproval extends Service {
|
|
|
262
263
|
*/
|
|
263
264
|
expireStale(): Promise<string[]>;
|
|
264
265
|
list(status?: PendingStatus): Promise<PendingRecord[]>;
|
|
265
|
-
approve(id: string): Promise<{
|
|
266
|
+
approve(id: string, audit: DecisionAudit): Promise<{
|
|
266
267
|
ok: boolean;
|
|
267
268
|
message: string;
|
|
268
269
|
}>;
|
|
@@ -290,7 +291,7 @@ export declare class EvolutionApproval extends Service {
|
|
|
290
291
|
ok: boolean;
|
|
291
292
|
message: string;
|
|
292
293
|
}>;
|
|
293
|
-
reject(id: string): Promise<{
|
|
294
|
+
reject(id: string, audit: DecisionAudit): Promise<{
|
|
294
295
|
ok: boolean;
|
|
295
296
|
message: string;
|
|
296
297
|
}>;
|
|
@@ -311,6 +312,9 @@ export declare class EvolutionApproval extends Service {
|
|
|
311
312
|
* claim-id grammar accepts). */
|
|
312
313
|
private newClaimId;
|
|
313
314
|
private dedupe;
|
|
315
|
+
/** W6: attach the platform's admission decision to a resolved record (read-tolerant; absent on
|
|
316
|
+
* records resolved by expiry or before 0.2.1). */
|
|
317
|
+
private recordDecision;
|
|
314
318
|
private doApprove;
|
|
315
319
|
}
|
|
316
320
|
export default EvolutionApproval;
|
package/lib/types/routes.d.ts
CHANGED
|
@@ -1,25 +1,29 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The approval HTTP surface: four
|
|
2
|
+
* The approval HTTP surface: four exact Fetch routes over the write gate's pending window.
|
|
3
|
+
*
|
|
4
|
+
* W6/G4: the carrier is the platform's Host connection data plane (`connection.fetch`) — the trust
|
|
5
|
+
* fence, browser auth and owner-only admission are applied by the platform BEFORE `fetch(request)`
|
|
6
|
+
* runs, so this module has no fence and no 403/401 branch. It still answers the same JSON envelope
|
|
7
|
+
* the browser half reads; the loopback f-string that used to live here was a SECOND admission path
|
|
8
|
+
* (the S2 spike showed it could diverge from the platform's).
|
|
3
9
|
*
|
|
4
10
|
* Layering: this module maps a route to a USE CASE and does nothing else. The pending window itself
|
|
5
11
|
* (ids, statuses, claim/resolve rules, the replay runners) lives in this package's service and in
|
|
6
12
|
* `evolution-state-storage`; the routes only read the window and forward the two decisions, so the
|
|
7
13
|
* console command face (`/evolution pending`, `approve <id>`) and this one cannot drift — both call
|
|
8
14
|
* the same methods.
|
|
9
|
-
*
|
|
10
|
-
* The browser half never touches the state medium: it lists, it approves, it rejects.
|
|
11
15
|
* @module @lmzhen/dsh-evolution-approval/routes
|
|
12
16
|
*/
|
|
13
|
-
import type { WebRoute } from '@deepseek-ai/dsh-host-webserver';
|
|
14
17
|
import type { PendingKind, PendingRecord, PendingStatus } from '@lmzhen/dsh-evolution-state-storage';
|
|
15
18
|
import { elapsedSince } from '@lmzhen/dsh-evolution-core';
|
|
19
|
+
import type { ConnectionFetchRouteLike, DecisionAudit } from '@lmzhen/dsh-evolution-core/http-routes';
|
|
16
20
|
import type { WritePreview } from './index.ts';
|
|
17
|
-
/** Route paths
|
|
21
|
+
/** Route paths: the ONE declaration lives in core (W1) and the browser half is generated from it. */
|
|
18
22
|
export declare const APPROVAL_ROUTES: {
|
|
19
23
|
readonly pending: "/api/dsh-evolution/approval/pending";
|
|
24
|
+
readonly preview: "/api/dsh-evolution/approval/preview";
|
|
20
25
|
readonly approve: "/api/dsh-evolution/approval/approve";
|
|
21
26
|
readonly reject: "/api/dsh-evolution/approval/reject";
|
|
22
|
-
readonly preview: "/api/dsh-evolution/approval/preview";
|
|
23
27
|
};
|
|
24
28
|
/** What one row of the pending list carries to the browser half. */
|
|
25
29
|
export interface PendingRow {
|
|
@@ -36,26 +40,30 @@ export interface PendingRow {
|
|
|
36
40
|
/**
|
|
37
41
|
* The slice of the approval service these routes read.
|
|
38
42
|
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
43
|
+
* W6: approve/reject take the DecisionAudit captured at registration (channel/principal/
|
|
44
|
+
* deploymentAllowsRemote). The audit rides the service call so the ledger records the ONE admission
|
|
45
|
+
* decision the platform already made — the route never re-derives it.
|
|
41
46
|
*/
|
|
42
47
|
export interface ApprovalFace {
|
|
43
48
|
list(status?: PendingStatus): Promise<PendingRecord[]>;
|
|
44
|
-
approve(id: string): Promise<{
|
|
49
|
+
approve(id: string, audit: DecisionAudit): Promise<{
|
|
45
50
|
ok: boolean;
|
|
46
51
|
message: string;
|
|
47
52
|
}>;
|
|
48
|
-
reject(id: string): Promise<{
|
|
53
|
+
reject(id: string, audit: DecisionAudit): Promise<{
|
|
49
54
|
ok: boolean;
|
|
50
55
|
message: string;
|
|
51
56
|
}>;
|
|
52
57
|
/** The preview the replayer of that kind registered, when it registered one. */
|
|
53
58
|
previewOf(kind: PendingKind): WritePreview | undefined;
|
|
54
59
|
}
|
|
60
|
+
/** The one shape a decision route accepts: a non-empty string id. */
|
|
61
|
+
export declare function idOf(payload: Record<string, unknown> | null): string;
|
|
55
62
|
/**
|
|
56
|
-
* Build the four routes over the approval service.
|
|
63
|
+
* Build the four exact Fetch routes over the approval service.
|
|
57
64
|
* @param approval - the host's approval face (the service instance, or a spec fixture).
|
|
58
|
-
* @
|
|
65
|
+
* @param audit - the DecisionAudit captured where the connection service is known (registration).
|
|
66
|
+
* @returns the route list for `registerHostRoutes`.
|
|
59
67
|
*/
|
|
60
|
-
export declare function makeApprovalRoutes(approval: ApprovalFace):
|
|
68
|
+
export declare function makeApprovalRoutes(approval: ApprovalFace, audit: DecisionAudit): ConnectionFetchRouteLike[];
|
|
61
69
|
//# sourceMappingURL=routes.d.ts.map
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lmzhen/dsh-evolution-approval",
|
|
3
3
|
"description": "Stage/pending approval service for Hermes-style self-evolution writes (community build)",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.27.1",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
@@ -26,19 +26,21 @@
|
|
|
26
26
|
],
|
|
27
27
|
"license": "MIT",
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@deepseek-ai/schemastery": "^3.18.
|
|
30
|
-
"@lmzhen/dsh-evolution-core": "^0.
|
|
29
|
+
"@deepseek-ai/schemastery": "^3.18.5",
|
|
30
|
+
"@lmzhen/dsh-evolution-core": "^0.27.1"
|
|
31
31
|
},
|
|
32
32
|
"peerDependencies": {
|
|
33
|
-
"@deepseek-ai/cordis": "^4.0.
|
|
34
|
-
"@
|
|
35
|
-
"@lmzhen/dsh-evolution-state
|
|
36
|
-
"@lmzhen/dsh-evolution-state": "^0.
|
|
33
|
+
"@deepseek-ai/cordis": "^4.0.5",
|
|
34
|
+
"@lmzhen/dsh-evolution-core": "^0.27.1",
|
|
35
|
+
"@lmzhen/dsh-evolution-state": "^0.27.1",
|
|
36
|
+
"@lmzhen/dsh-evolution-state-storage": "^0.27.1"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
|
-
"@
|
|
40
|
-
"@lmzhen/dsh-evolution-
|
|
41
|
-
"@lmzhen/dsh-evolution-state-storage": "^0.
|
|
42
|
-
|
|
39
|
+
"@lmzhen/dsh-evolution-core": "^0.27.1",
|
|
40
|
+
"@lmzhen/dsh-evolution-state": "^0.27.1",
|
|
41
|
+
"@lmzhen/dsh-evolution-state-storage": "^0.27.1"
|
|
42
|
+
},
|
|
43
|
+
"engines": {
|
|
44
|
+
"dsh": ">=0.2.1-rc.1 <0.2.2-0"
|
|
43
45
|
}
|
|
44
46
|
}
|