@jayyuen66/dsh-quality-gate 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/client.js ADDED
@@ -0,0 +1,479 @@
1
+ // GENERATED by buildClient() in build-client.mjs — edit src/client-entry.ts, then run `node build-client.mjs`.
2
+ window.__ModuleLoader__.load({
3
+ id: '@jayyuen66/dsh-quality-gate',
4
+ factory: (require) => {
5
+ var module = { exports: {} };
6
+ var exports = module.exports;
7
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
8
+ let react = require("react");
9
+ //#region src/ui-messages.ts
10
+ const UI_MESSAGES = {
11
+ zh: {
12
+ cardTitle: "quality-gate 回合收口质量门禁",
13
+ cardDescription: "回合收口质量门禁(ECC stop-format-typecheck 移植):本回合编辑过代码文件时,收口前自动跑项目门禁(根 package.json 声明了 check 才用 pnpm check,否则 cargo check / ruff / tsc),失败自动开新回合注入修复指令;失败事实同步 lesson-loop 总线,记忆网关沉淀默认关闭",
14
+ statusReadOnly: "当前作用域只读",
15
+ statusDirty: "有未保存的修改,点「保存」生效",
16
+ statusClean: "无未保存的修改",
17
+ save: "保存",
18
+ saving: "保存中…",
19
+ revert: "撤销",
20
+ saveFailed: "保存失败:{message}",
21
+ saveRejected: "保存被宿主拒绝(配置校验未通过或写入被跳过),修改未生效",
22
+ outOfRange: "超出允许范围,未保存",
23
+ enabledLabel: "启用质量门禁",
24
+ enabledHint: "关闭后回合收口不再跑门禁(模型可带病收口)",
25
+ injectsLabel: "每回合注入配额",
26
+ injectsHint: "门禁失败每回合最多注入几次修复指令(1-5,默认 2,防跑飞)",
27
+ budgetLabel: "门禁预算(毫秒)",
28
+ budgetHint: "单次门禁最大耗时(10s-600s,默认 300s;超时按失败注入不静默放行)",
29
+ memoryLabel: "门禁失败沉淀记忆",
30
+ memoryHint: "默认关闭。开启后:代码型门禁失败的证据(命令/根/全文报错,不截断)写入记忆网关的 L0 会话层供后续会话召回(需同时填下面两项,任一留空即不启用);总线 lesson-loop 的同步上报不受此开关影响。关闭后只注入不沉淀",
31
+ gatewayUrlLabel: "记忆网关地址",
32
+ gatewayUrlHint: "留空 = 不启用记忆反馈。插件不带任何默认地址:本机用户请填自己的网关地址(例如 http://127.0.0.1:8420),没有网关就让它空着",
33
+ keyRefLabel: "网关密钥引用",
34
+ keyRefHint: "网关鉴权令牌的凭据引用名(环境变量名形态,默认 TDAI_GATEWAY_KEY)。这里存的只是引用名,密钥值由 dsh 凭据服务管理(Web 凭据页或 ~/.dsh/.credentials.yaml 的 refs),不会写进 settings.yaml;清空即恢复默认引用名"
35
+ },
36
+ en: {
37
+ cardTitle: "quality-gate turn-end quality gate",
38
+ cardDescription: "Turn-end quality gate (ported from ECC stop-format-typecheck): when this turn edited code files, the project gate runs automatically before the turn closes (pnpm check only if the root package.json declares a check script, otherwise cargo check / ruff / tsc); a failure opens a new turn with repair instructions. Failure facts are also reported to the lesson-loop bus; memory-gateway persistence is off by default",
39
+ statusReadOnly: "This scope is read-only",
40
+ statusDirty: "Unsaved changes — press Save to apply",
41
+ statusClean: "No unsaved changes",
42
+ save: "Save",
43
+ saving: "Saving…",
44
+ revert: "Revert",
45
+ saveFailed: "Save failed: {message}",
46
+ saveRejected: "Save rejected by the host (validation failed or write skipped) — changes not applied",
47
+ outOfRange: "Out of the allowed range — not saved",
48
+ enabledLabel: "Enable the quality gate",
49
+ enabledHint: "When off: no gate runs at turn end (the model may close a turn with broken code)",
50
+ injectsLabel: "Repair injections per turn",
51
+ injectsHint: "How many repair instructions a failing gate may inject per turn (1-5, default 2 — runaway protection)",
52
+ budgetLabel: "Gate budget (milliseconds)",
53
+ budgetHint: "Max duration of one gate run (10s-600s, default 300s; a timeout injects a failure instead of passing silently)",
54
+ memoryLabel: "Persist gate failures to memory",
55
+ memoryHint: "Off by default. When on: the evidence of code-level gate failures (command, root, full error text, untruncated) is written into the memory gateway's L0 session layer so later sessions can recall it (both fields below must be filled — leaving either empty disables it); synchronous reporting to the lesson-loop bus is unaffected by this switch. When off, repairs are injected but nothing is persisted",
56
+ gatewayUrlLabel: "Memory gateway URL",
57
+ gatewayUrlHint: "Empty = memory feedback disabled. The plugin ships no default address: fill in your own gateway URL (e.g. http://127.0.0.1:8420), or leave it empty if you have no gateway",
58
+ keyRefLabel: "Gateway credential ref",
59
+ keyRefHint: "Name of the credential holding the gateway token (POSIX env-var form, default TDAI_GATEWAY_KEY). Only the ref name is stored here — the secret itself is managed by the dsh credentials service (the web credentials page or the refs in ~/.dsh/.credentials.yaml) and never lands in settings.yaml; clearing it restores the default ref name"
60
+ }
61
+ };
62
+ //#endregion
63
+ //#region src/client-entry.ts
64
+ const NS = "quality-gate";
65
+ const BUNDLE_PKG = "@jayyuen66/dsh-quality-gate";
66
+ const CARD_CSS = [
67
+ ".qgc-card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px;list-style:none;transition:border-color .16s,background .16s}",
68
+ ".qgc-card:hover{border-color:var(--dsw-alias-label-dimmed)}",
69
+ ".qgc-card-open{background:var(--dsw-alias-bg-layer-2);border-color:var(--dsw-alias-label-dimmed)}",
70
+ ".qgc-header{appearance:none;width:100%;font:inherit;color:inherit;text-align:left;cursor:pointer;background:transparent;border:0;border-radius:12px;align-items:center;gap:12px;padding:14px 16px;display:flex}",
71
+ ".qgc-head{flex-direction:column;flex:1;gap:4px;min-width:0;display:flex}",
72
+ ".qgc-name{color:var(--dsw-alias-label-primary);font-size:15px;font-weight:600;line-height:1.4}",
73
+ ".qgc-desc{color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:1.5}",
74
+ ".qgc-chevron{color:var(--dsw-alias-label-tertiary);flex:none;transition:transform .16s}",
75
+ ".qgc-chevron-open{transform:rotate(180deg)}",
76
+ ".qgc-body{border-top:1px solid var(--dsw-alias-border-l2);margin:0 16px;padding:8px 0 12px}",
77
+ ".qgc-row{display:flex;flex-direction:row;justify-content:space-between;align-items:center;gap:12px;padding:9px 0}",
78
+ ".qgc-label{font-size:13px;color:var(--dsw-alias-label-primary,inherit)}",
79
+ ".qgc-hint{font-size:12px;color:var(--dsw-alias-label-tertiary,#8a8f99);line-height:1.5;margin-top:2px}",
80
+ ".qgc-switch{appearance:none;position:relative;width:34px;height:20px;border-radius:10px;background:var(--dsw-alias-fill-primary,#d8dbe2);transition:background .16s;cursor:pointer;border:0;flex:none}",
81
+ ".qgc-switch::after{content:\"\";position:absolute;top:2px;left:2px;width:16px;height:16px;border-radius:50%;background:var(--dsw-alias-bg-layer-1,#fff);transition:left .16s}",
82
+ ".qgc-switch-on{background:var(--dsw-alias-brand-primary,#e07856)}",
83
+ ".qgc-switch-on::after{left:16px}",
84
+ ".qgc-switch:disabled{cursor:not-allowed;opacity:.6}",
85
+ ".qgc-input{width:100%;box-sizing:border-box;font:inherit;font-size:12px;color:var(--dsw-alias-label-primary,inherit);background:var(--dsw-alias-bg-layer-1,transparent);border:1px solid var(--dsw-alias-border-l2,transparent);border-radius:8px;padding:6px 8px;margin-top:4px}",
86
+ ".qgc-savebar{display:flex;gap:8px;align-items:center;padding:10px 0 2px;border-top:1px dashed var(--dsw-alias-border-l2);margin-top:6px;flex-wrap:wrap}",
87
+ ".qgc-btn{appearance:none;font:inherit;font-size:12px;cursor:pointer;border-radius:6px;padding:4px 12px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1,transparent);color:var(--dsw-alias-label-primary,inherit)}",
88
+ ".qgc-btn:hover:not(:disabled){border-color:var(--dsw-alias-label-dimmed)}",
89
+ ".qgc-btn-primary{background:var(--dsw-alias-brand-primary,#e07856);border-color:var(--dsw-alias-brand-primary,#e07856);color:var(--dsw-alias-label-primary-foreground,#fff)}",
90
+ ".qgc-btn:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,#e07856);outline-offset:1px}",
91
+ ".qgc-btn:disabled{opacity:.5;cursor:not-allowed}",
92
+ ".qgc-dirty{font-size:12px;color:var(--dsw-alias-label-tertiary,#8a8f99)}",
93
+ ".qgc-saveerr{font-size:12px;color:#c4483f}"
94
+ ].join("\n");
95
+ function ToggleRow(props) {
96
+ return (0, react.createElement)("div", { className: "qgc-row" }, (0, react.createElement)("div", null, (0, react.createElement)("div", { className: "qgc-label" }, props.label), (0, react.createElement)("div", { className: "qgc-hint" }, props.hint)), (0, react.createElement)("button", {
97
+ type: "button",
98
+ className: `qgc-switch${props.checked ? " qgc-switch-on" : ""}`,
99
+ role: "switch",
100
+ "aria-checked": props.checked,
101
+ "data-field": props.field,
102
+ disabled: props.disabled === true,
103
+ onClick: props.onToggle
104
+ }));
105
+ }
106
+ /** 数字输入行:settings 数字字段 ↔ UI 字符串(v7 配置面补齐)。 */
107
+ function NumberInputRow(props) {
108
+ const num = typeof props.value === "number" && Number.isFinite(props.value) ? String(props.value) : "";
109
+ const [text, setText] = (0, react.useState)(num);
110
+ const [invalid, setInvalid] = (0, react.useState)(false);
111
+ (0, react.useEffect)(() => {
112
+ setText(num);
113
+ setInvalid(false);
114
+ }, [num]);
115
+ const invalidHint = props.t("outOfRange");
116
+ return (0, react.createElement)("div", { className: "qgc-row" }, (0, react.createElement)("div", null, (0, react.createElement)("div", { className: "qgc-label" }, props.label), (0, react.createElement)("div", { className: "qgc-hint" }, props.hint), (0, react.createElement)("input", {
117
+ type: "number",
118
+ className: "qgc-input",
119
+ value: text,
120
+ disabled: props.disabled === true,
121
+ "aria-invalid": invalid || void 0,
122
+ onChange: (event) => {
123
+ const next = event.target.value;
124
+ setText(next);
125
+ if (next === "") {
126
+ setInvalid(false);
127
+ props.onClear(props.field);
128
+ return;
129
+ }
130
+ const numeric = Number(next);
131
+ if (!Number.isFinite(numeric)) {
132
+ setInvalid(true);
133
+ return;
134
+ }
135
+ const min = props.min ?? -Infinity;
136
+ const max = props.max ?? Infinity;
137
+ if (numeric >= min && numeric <= max) {
138
+ setInvalid(false);
139
+ props.onChange(props.field, numeric);
140
+ } else setInvalid(true);
141
+ }
142
+ }), invalid ? (0, react.createElement)("div", {
143
+ className: "qgc-hint",
144
+ style: { color: "var(--dsw-alias-danger, #e5484d)" }
145
+ }, invalidHint) : null));
146
+ }
147
+ /**
148
+ * 文本输入行:settings 字符串字段 ↔ UI 字符串。
149
+ * 清空 = unset(回到 schema 默认值)——对本卡的两个网关字段,默认值就是"留空 = 不启用"。
150
+ */
151
+ function TextInputRow(props) {
152
+ const text = typeof props.value === "string" ? props.value : "";
153
+ const [draft, setDraft] = (0, react.useState)(text);
154
+ (0, react.useEffect)(() => {
155
+ setDraft(text);
156
+ }, [text]);
157
+ return (0, react.createElement)("div", { className: "qgc-row" }, (0, react.createElement)("div", null, (0, react.createElement)("div", { className: "qgc-label" }, props.label), (0, react.createElement)("div", { className: "qgc-hint" }, props.hint), (0, react.createElement)("input", {
158
+ type: "text",
159
+ className: "qgc-input",
160
+ value: draft,
161
+ placeholder: props.placeholder ?? "",
162
+ disabled: props.disabled === true,
163
+ onChange: (event) => {
164
+ const next = event.target.value;
165
+ setDraft(next);
166
+ if (next.trim() === "") {
167
+ props.onClear(props.field);
168
+ return;
169
+ }
170
+ props.onChange(props.field, next.trim());
171
+ }
172
+ })));
173
+ }
174
+ /** touched 层与快照的差异字段(值语义比较;undefined 与缺失等价)。 */
175
+ function diffTouched(touched, value) {
176
+ const out = [];
177
+ for (const key of Object.keys(touched)) if (JSON.stringify(touched[key] ?? null) !== JSON.stringify(value[key] ?? null)) out.push(key);
178
+ return out;
179
+ }
180
+ function SaveBar(props) {
181
+ const { t } = props;
182
+ const dis = !props.writable || props.busy;
183
+ let dirtyText;
184
+ if (props.writable) dirtyText = props.dirty ? t("statusDirty") : t("statusClean");
185
+ else dirtyText = t("statusReadOnly");
186
+ return (0, react.createElement)("div", { className: "qgc-savebar" }, (0, react.createElement)("button", {
187
+ type: "button",
188
+ className: "qgc-btn qgc-btn-primary",
189
+ "data-field": "save",
190
+ disabled: dis || !props.dirty,
191
+ onClick: props.onSave
192
+ }, props.busy ? t("saving") : t("save")), (0, react.createElement)("button", {
193
+ type: "button",
194
+ className: "qgc-btn",
195
+ "data-field": "discard",
196
+ disabled: dis || !props.dirty,
197
+ onClick: props.onDiscard
198
+ }, t("revert")), props.error === null ? (0, react.createElement)("span", { className: "qgc-dirty" }, dirtyText) : (0, react.createElement)("span", { className: "qgc-saveerr" }, props.error));
199
+ }
200
+ /**
201
+ * 逐键 await 写入并**消费受理位**(0.1.7 的 set/unset 回 Promise<boolean>:
202
+ * true=宿主受理;false=拒绝或写入被跳过,settings.md:13 "refuses stale revisions";
203
+ * 传输失败才 reject)。false 或 rejection 都**不清 touched**——否则宿主拒写时
204
+ * 卡片仍显示"已保存",用户修改被静默丢弃(旧版 setTimeout 盲清正是这个缺陷)。
205
+ */
206
+ async function writeTouched(channels) {
207
+ const { t, set, unset, keys, touched, setBusy, setSaveError, setTouched } = channels;
208
+ try {
209
+ for (const key of keys) {
210
+ const val = touched[key];
211
+ if (!await (val === void 0 ? unset(key) : set(key, val))) {
212
+ setBusy(false);
213
+ setSaveError(t("saveRejected"));
214
+ return;
215
+ }
216
+ }
217
+ setBusy(false);
218
+ setTouched({});
219
+ } catch (error) {
220
+ console.error("[quality-gate] save write failed:", error);
221
+ setBusy(false);
222
+ setSaveError(t("saveFailed", { message: String(error instanceof Error ? error.message : error) }));
223
+ }
224
+ }
225
+ /** 头部:渲染子树扁平化(规避 max-nested-calls)——DOM 各层拆成独立 createElement 元素
226
+ * const,逐层用变量组合,避免 createElement 回调逐层内嵌。 */
227
+ function buildCardHeader(props) {
228
+ const { t, open, onToggle } = props;
229
+ const nameEl = (0, react.createElement)("div", { className: "qgc-name" }, t("cardTitle"));
230
+ const descEl = (0, react.createElement)("div", { className: "qgc-desc" }, t("cardDescription"));
231
+ const headDiv = (0, react.createElement)("div", { className: "qgc-head" }, nameEl, descEl);
232
+ const chevronPathData = "M3 5l4 4 4-4";
233
+ const chevronPath = (0, react.createElement)("path", {
234
+ ["d"]: chevronPathData,
235
+ fill: "none",
236
+ stroke: "currentColor",
237
+ strokeWidth: 1.5,
238
+ strokeLinecap: "round",
239
+ strokeLinejoin: "round"
240
+ });
241
+ const chevron = (0, react.createElement)("svg", {
242
+ width: 14,
243
+ height: 14,
244
+ viewBox: "0 0 14 14",
245
+ "aria-hidden": true,
246
+ className: `qgc-chevron${open ? " qgc-chevron-open" : ""}`
247
+ }, chevronPath);
248
+ return (0, react.createElement)("button", {
249
+ type: "button",
250
+ className: "qgc-header",
251
+ "aria-expanded": open,
252
+ onClick: onToggle
253
+ }, headDiv, chevron);
254
+ }
255
+ /** 展开态的 `ul` 本体:行元素仍按从前的顺序逐个传给同一个 createElement(不折成数组
256
+ * 子节点,保持宿主侧 children 形状一致)。 */
257
+ function buildCardBody(props) {
258
+ const { t, writable, eff, setField, clearField, dirty, busy, saveError, onSave, onDiscard } = props;
259
+ const rowEnabled = (0, react.createElement)(ToggleRow, {
260
+ label: t("enabledLabel"),
261
+ hint: t("enabledHint"),
262
+ field: "enabled",
263
+ checked: eff("enabled") !== false,
264
+ disabled: !writable,
265
+ onToggle: () => {
266
+ setField("enabled", eff("enabled") === false);
267
+ }
268
+ });
269
+ const rowInjects = (0, react.createElement)(NumberInputRow, {
270
+ t,
271
+ label: t("injectsLabel"),
272
+ hint: t("injectsHint"),
273
+ field: "maxInjectsPerTurn",
274
+ value: eff("maxInjectsPerTurn"),
275
+ disabled: !writable,
276
+ onChange: (field, val) => {
277
+ setField(field, val);
278
+ },
279
+ onClear: (field) => {
280
+ clearField(field);
281
+ },
282
+ min: 1,
283
+ max: 5
284
+ });
285
+ const rowBudget = (0, react.createElement)(NumberInputRow, {
286
+ t,
287
+ label: t("budgetLabel"),
288
+ hint: t("budgetHint"),
289
+ field: "gateBudgetMs",
290
+ value: eff("gateBudgetMs"),
291
+ disabled: !writable,
292
+ onChange: (field, val) => {
293
+ setField(field, val);
294
+ },
295
+ onClear: (field) => {
296
+ clearField(field);
297
+ },
298
+ min: 1e4,
299
+ max: 6e5
300
+ });
301
+ const rowMemory = (0, react.createElement)(ToggleRow, {
302
+ label: t("memoryLabel"),
303
+ hint: t("memoryHint"),
304
+ field: "memoryFeedback",
305
+ checked: eff("memoryFeedback") === true,
306
+ disabled: !writable,
307
+ onToggle: () => {
308
+ setField("memoryFeedback", eff("memoryFeedback") !== true);
309
+ }
310
+ });
311
+ const rowGatewayUrl = (0, react.createElement)(TextInputRow, {
312
+ label: t("gatewayUrlLabel"),
313
+ hint: t("gatewayUrlHint"),
314
+ field: "memoryGatewayUrl",
315
+ value: eff("memoryGatewayUrl"),
316
+ placeholder: "http://127.0.0.1:8420",
317
+ disabled: !writable,
318
+ onChange: (field, val) => {
319
+ setField(field, val);
320
+ },
321
+ onClear: (field) => {
322
+ clearField(field);
323
+ }
324
+ });
325
+ const rowGatewayKeyRef = (0, react.createElement)(TextInputRow, {
326
+ label: t("keyRefLabel"),
327
+ hint: t("keyRefHint"),
328
+ field: "memoryGatewayKeyRef",
329
+ value: eff("memoryGatewayKeyRef"),
330
+ placeholder: "TDAI_GATEWAY_KEY",
331
+ disabled: !writable,
332
+ onChange: (field, val) => {
333
+ setField(field, val);
334
+ },
335
+ onClear: (field) => {
336
+ clearField(field);
337
+ }
338
+ });
339
+ const rowSave = (0, react.createElement)(SaveBar, {
340
+ t,
341
+ dirty,
342
+ writable,
343
+ busy,
344
+ error: saveError,
345
+ onSave,
346
+ onDiscard
347
+ });
348
+ return (0, react.createElement)("ul", { className: "qgc-body" }, rowEnabled, rowInjects, rowBudget, rowMemory, rowGatewayUrl, rowGatewayKeyRef, rowSave);
349
+ }
350
+ function DgCard(props) {
351
+ const { t } = props;
352
+ const [open, setOpen] = (0, react.useState)(false);
353
+ const snap = props.useCard((cardSnap) => cardSnap);
354
+ const { value } = snap;
355
+ const writable = snap.status === "ready" && snap.writable;
356
+ const [touched, setTouched] = (0, react.useState)({});
357
+ const [busy, setBusy] = (0, react.useState)(false);
358
+ const [saveError, setSaveError] = (0, react.useState)(null);
359
+ /** 渲染取值:touched 优先,快照兜底(未触碰字段实时跟随外部快照)。 */
360
+ const eff = (field) => field in touched ? touched[field] : value[field];
361
+ const dirty = diffTouched(touched, value).length > 0;
362
+ const setField = (field, val) => {
363
+ setTouched((prev) => ({
364
+ ...prev,
365
+ [field]: val
366
+ }));
367
+ };
368
+ const clearField = (field) => {
369
+ setTouched((prev) => ({
370
+ ...prev,
371
+ [field]: void 0
372
+ }));
373
+ };
374
+ const save = () => {
375
+ const keys = diffTouched(touched, value);
376
+ if (keys.length === 0) return;
377
+ setBusy(true);
378
+ setSaveError(null);
379
+ writeTouched({
380
+ t,
381
+ set: props.set,
382
+ unset: props.unset,
383
+ keys,
384
+ touched,
385
+ setBusy,
386
+ setSaveError,
387
+ setTouched
388
+ });
389
+ };
390
+ const discard = () => {
391
+ setTouched({});
392
+ setSaveError(null);
393
+ };
394
+ const header = buildCardHeader({
395
+ t,
396
+ open,
397
+ onToggle: () => {
398
+ setOpen(!open);
399
+ }
400
+ });
401
+ const body = open ? buildCardBody({
402
+ t,
403
+ writable,
404
+ eff,
405
+ setField,
406
+ clearField,
407
+ dirty,
408
+ busy,
409
+ saveError,
410
+ onSave: save,
411
+ onDiscard: discard
412
+ }) : null;
413
+ return (0, react.createElement)("li", { className: `qgc-card${open ? " qgc-card-open" : ""}` }, header, body);
414
+ }
415
+ function cardStore(scope) {
416
+ let cachedSnap = null;
417
+ let cachedView = null;
418
+ const EMPTY_SNAPSHOT = {
419
+ status: "loading",
420
+ writable: false,
421
+ value: {}
422
+ };
423
+ return {
424
+ getSnapshot() {
425
+ const snap = scope.getSnapshot();
426
+ if (snap !== cachedSnap) {
427
+ cachedSnap = snap;
428
+ cachedView = {
429
+ status: snap.status,
430
+ writable: snap.writable,
431
+ value: snap.value ?? {}
432
+ };
433
+ }
434
+ return cachedView ?? EMPTY_SNAPSHOT;
435
+ },
436
+ subscribe(listener) {
437
+ return scope.subscribe(listener);
438
+ }
439
+ };
440
+ }
441
+ const inject = [
442
+ "slots",
443
+ "configForms",
444
+ "locale"
445
+ ];
446
+ function apply(ctx) {
447
+ ctx.effect(() => {
448
+ const tag = document.createElement("style");
449
+ tag.id = "quality-gate-card-css";
450
+ tag.textContent = CARD_CSS;
451
+ document.head.append(tag);
452
+ return () => {
453
+ tag.remove();
454
+ };
455
+ }, "quality-gate-card: styles");
456
+ const scope = ctx.configForms.get(NS);
457
+ const store = cardStore(scope);
458
+ ctx.effect(() => ctx.locale.register(NS, UI_MESSAGES), "quality-gate-card: locale dictionaries");
459
+ const t = ctx.locale.bind(NS);
460
+ ctx.slots.inject("plugins.bundle.config", () => {
461
+ return ctx.slots.register({
462
+ name: "plugins.bundle.config",
463
+ key: BUNDLE_PKG,
464
+ inject: () => ({
465
+ t,
466
+ hooks: { card: store },
467
+ set: (field, value) => scope.set(field, value),
468
+ unset: (field) => scope.unset(field)
469
+ })
470
+ }, DgCard);
471
+ });
472
+ }
473
+ //#endregion
474
+ exports.apply = apply;
475
+ exports.diffTouched = diffTouched;
476
+ exports.inject = inject;
477
+ return module.exports;
478
+ },
479
+ });
@@ -0,0 +1,20 @@
1
+ # quality-gate(2026-09-03):回合收口质量门禁。
2
+ # ECC(MIT, github.com/affaan-m/ECC)stop-format-typecheck 移植:
3
+ # - 编辑记账:本回合编辑过的代码/配置文件(O(1),md 等非代码面豁免);
4
+ # - agent/turn-stopping 批处理:从编辑路径向上探测最近清单根
5
+ # (pnpm-workspace 且根 package.json 声明了 check→pnpm check /
6
+ # Cargo.toml→cargo check / pyproject→ruff / package.json+tsconfig→tsc),
7
+ # 门禁失败经 followup 注入修复指令 → 回合物理不结束(agent.ts:304-308 inbox 二次检查);
8
+ # 探测只提项目自己声明过的命令,拿不到脚本清单时宁可不给门禁(防假失败注入);
9
+ # - 防跑飞:每回合每根至多 2 次注入、总预算封顶、超时按「未执行」注入不静默通过;
10
+ # inbox 有待处理时不抢话。
11
+ # 设置卡:enabled / 注入配额 / 门禁预算 / 记忆反馈(开关 + 网关地址 + 密钥凭据引用,
12
+ # 记忆反馈默认关闭且地址默认为空 = 不启用;密钥走 dsh 凭据通道,不落设置)。
13
+ #
14
+ # 组合包层(2026-09-05 bundle 化):本文件由 package.json 的 dsh.bundle.patch
15
+ # 指向,随 `dsh plugin --profile web add/remove ./plugins/quality-gate` 维护。
16
+ # 行按裸包名引用本包(client-modules 只扫描裸包名条目的 dsh.client 声明)。
17
+ # 注意 pnpm ≥12.3.0:12.0.0 的 add 本地目录有 #14437 bug。
18
+ - insert:
19
+ - id: quality-gate
20
+ name: "@jayyuen66/dsh-quality-gate"