create-principles-disciple 1.144.10 → 1.144.11
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/_release/product-identity.json +1 -1
- package/console/dist/server/index.js +13 -3
- package/console/dist/server/models/ApprovalsConsoleModel.d.ts +10 -0
- package/console/dist/server/models/ApprovalsConsoleModel.js +39 -5
- package/console/dist/server/models/ApprovalsGroupedConsoleModel.d.ts +11 -3
- package/console/dist/server/models/ApprovalsGroupedConsoleModel.js +74 -14
- package/console/dist/server/update-timeout.d.ts +1 -0
- package/console/dist/server/update-timeout.js +18 -8
- package/console/dist/shared/prompt-injection-contract.d.ts +16 -1
- package/console/dist/ui/i18n/en.json +8 -3
- package/console/dist/ui/i18n/zh-CN.json +8 -3
- package/console/dist/ui/pages/focus/FocusPage.js +30 -8
- package/console/dist/ui/pages/focus/focus-validation.js +11 -0
- package/console/dist/ui/utils/approval-warning-localization.js +22 -3
- package/console/dist/ui/utils/validators.d.ts +6 -0
- package/console/dist/ui/utils/validators.js +18 -1
- package/console/dist/web/assets/app.css +9 -0
- package/console/dist/web/assets/app.js +11 -13
- package/core/dist/runtime-v2/index.d.ts +8 -2
- package/core/dist/runtime-v2/index.d.ts.map +1 -1
- package/core/dist/runtime-v2/index.js +8 -2
- package/core/dist/runtime-v2/index.js.map +1 -1
- package/core/dist/runtime-v2/pain-signal-bridge.d.ts +26 -11
- package/core/dist/runtime-v2/pain-signal-bridge.d.ts.map +1 -1
- package/core/dist/runtime-v2/pain-signal-bridge.js +59 -52
- package/core/dist/runtime-v2/pain-signal-bridge.js.map +1 -1
- package/core/dist/runtime-v2/pain-signal-runtime-factory.d.ts +9 -0
- package/core/dist/runtime-v2/pain-signal-runtime-factory.d.ts.map +1 -1
- package/core/dist/runtime-v2/pain-signal-runtime-factory.js +17 -8
- package/core/dist/runtime-v2/pain-signal-runtime-factory.js.map +1 -1
- package/core/dist/runtime-v2/recovery-sweep-service.d.ts +13 -0
- package/core/dist/runtime-v2/recovery-sweep-service.d.ts.map +1 -1
- package/core/dist/runtime-v2/recovery-sweep-service.js +43 -29
- package/core/dist/runtime-v2/recovery-sweep-service.js.map +1 -1
- package/core/package.json +1 -1
- package/host-runtime/dist/prompt-injection-projection.d.ts +50 -0
- package/host-runtime/dist/prompt-injection-projection.js +57 -1
- package/host-runtime/package.json +1 -1
- package/package.json +2 -2
- package/pd-cli/dist/commands/diagnose.d.ts.map +1 -1
- package/pd-cli/dist/commands/diagnose.js +42 -1
- package/pd-cli/dist/commands/diagnose.js.map +1 -1
- package/pd-cli/dist/commands/pain-retry.d.ts.map +1 -1
- package/pd-cli/dist/commands/pain-retry.js +113 -2
- package/pd-cli/dist/commands/pain-retry.js.map +1 -1
- package/plugin/dist/bundle.js +331 -331
- package/plugin/dist/openclaw.plugin.json +1 -1
- package/plugin/openclaw.plugin.json +1 -1
- package/plugin/package.json +1 -1
- package/release-manager/package.json +2 -2
|
@@ -3,7 +3,7 @@ import * as fs from 'fs';
|
|
|
3
3
|
import * as os from 'os';
|
|
4
4
|
import * as path from 'path';
|
|
5
5
|
import { fileURLToPath } from 'url';
|
|
6
|
-
import { resolveApplyFullTimeoutMs } from './update-timeout.js';
|
|
6
|
+
import { resolveApplyFullTimeoutMs, resolveCheckTimeoutMs } from './update-timeout.js';
|
|
7
7
|
import { loadPdConfig, computeFlagsFromLoadResult, getFeedbackMaintainerEmail, getFeedbackChannelConfig, } from './config/pd-config-store.js';
|
|
8
8
|
import { resolveOwnerIdentity, defaultOwnerIdentityHomeDir } from '@principles/core/runtime-v2';
|
|
9
9
|
import { createProductTelemetryService, scheduleProductTelemetryExport } from '@principles/host-runtime';
|
|
@@ -194,6 +194,13 @@ const UPDATE_APPLY_TIMEOUT_MS = 120000;
|
|
|
194
194
|
// the PRI-671 upgrade gate on Windows/AV-slow disks). Same knob pattern as
|
|
195
195
|
// the installer's PD_INSTALL_TIMEOUT_MS.
|
|
196
196
|
const UPDATE_APPLY_FULL_TIMEOUT_MS = resolveApplyFullTimeoutMs();
|
|
197
|
+
// A check refreshes the signed channel over the network (TUF metadata + channel
|
|
198
|
+
// document + release metadata, several sequential requests to the metadata
|
|
199
|
+
// host). Measured on a CN network against GitHub Pages: normally ~1.2s, but a
|
|
200
|
+
// single slow DNS/TLS leg pushes it past 12s, which the old 10s default turned
|
|
201
|
+
// into a 504 "Request timeout" on every page load (observed ~1 run in 5).
|
|
202
|
+
// Same knob pattern as apply-full: PD_UPDATE_CHECK_TIMEOUT_MS (ms) overrides.
|
|
203
|
+
const UPDATE_CHECK_TIMEOUT_MS = resolveCheckTimeoutMs();
|
|
197
204
|
function asyncHandler(fn, timeoutMs = REQUEST_TIMEOUT_MS) {
|
|
198
205
|
return (innerReq, innerRes) => {
|
|
199
206
|
const timeoutId = setTimeout(() => {
|
|
@@ -415,14 +422,17 @@ function handleRequest(services) {
|
|
|
415
422
|
asyncHandler(() => handleUpdateTransactionRoute(req, res, subPath))(req, res);
|
|
416
423
|
return;
|
|
417
424
|
}
|
|
418
|
-
// Update routes: GET /api/update/check, POST /api/update/apply
|
|
425
|
+
// Update routes: GET /api/update/check, POST /api/update/apply-full
|
|
426
|
+
// (handleUpdateRoute 404s every other subPath)
|
|
419
427
|
if (urlPath === '/api/update' || urlPath.startsWith('/api/update/')) {
|
|
420
428
|
const subPath = urlPath.slice('/api/update'.length);
|
|
421
429
|
const isApply = subPath === '/apply';
|
|
422
430
|
const isApplyFull = subPath === '/apply-full';
|
|
431
|
+
const isCheck = subPath === '/check';
|
|
423
432
|
const timeout = isApplyFull ? UPDATE_APPLY_FULL_TIMEOUT_MS
|
|
424
433
|
: isApply ? UPDATE_APPLY_TIMEOUT_MS
|
|
425
|
-
:
|
|
434
|
+
: isCheck ? UPDATE_CHECK_TIMEOUT_MS
|
|
435
|
+
: undefined;
|
|
426
436
|
asyncHandler(() => handleUpdateRoute(req, res, services.workspaceDir, subPath), timeout)(req, res);
|
|
427
437
|
return;
|
|
428
438
|
}
|
|
@@ -65,6 +65,16 @@ export declare class ApprovalsConsoleModel {
|
|
|
65
65
|
* the activation is committed and the Owner decides whether to retire older
|
|
66
66
|
* principles. A projection error is also surfaced (never silent) but must
|
|
67
67
|
* not fail the approve.
|
|
68
|
+
*
|
|
69
|
+
* PRI-935 — the console holds no session, so it cannot supply the production
|
|
70
|
+
* round key and the projection runs `legacy_fifo_prefix_v1`. That is NOT a
|
|
71
|
+
* conservative forecast: FIFO structurally drops the newest activation from
|
|
72
|
+
* every truncated selection, while the live plugin route rotates and injects
|
|
73
|
+
* it within N user turns (PRI-904). Reporting that as "will NOT enter agent
|
|
74
|
+
* behavior until older ones are deactivated" told the Owner a falsehood and
|
|
75
|
+
* pushed them to deactivate healthy principles. The console now reports the
|
|
76
|
+
* policy it actually ran and, when the activation is merely rotated out of
|
|
77
|
+
* the current window, says so instead of claiming starvation.
|
|
68
78
|
*/
|
|
69
79
|
private checkPromptInjectionBudget;
|
|
70
80
|
/**
|
|
@@ -153,10 +153,12 @@ export class ApprovalsConsoleModel {
|
|
|
153
153
|
}
|
|
154
154
|
// PRI-890 (PRI-768 v6-02): after a successful prompt-channel activation,
|
|
155
155
|
// verify the new activation actually fits the prompt injection budget.
|
|
156
|
-
// The prompt surface renders active activations
|
|
157
|
-
//
|
|
158
|
-
//
|
|
159
|
-
//
|
|
156
|
+
// The prompt surface renders active activations under a hard char cap;
|
|
157
|
+
// when the budget is saturated the newest Owner approval may not reach
|
|
158
|
+
// agent behavior on THIS turn. Surface it as a non-fatal warning (rc-9)
|
|
159
|
+
// instead — the activation stays committed. PRI-935: whether "not on this
|
|
160
|
+
// turn" means "queued behind rotation" or genuinely starved depends on the
|
|
161
|
+
// selection policy, which checkPromptInjectionBudget now reports honestly.
|
|
160
162
|
let injectionWarning;
|
|
161
163
|
if (isActivationSuccess(activation) && existing.channel === 'prompt') {
|
|
162
164
|
// isActivationSuccess is not a type predicate; the success variants all
|
|
@@ -179,6 +181,16 @@ export class ApprovalsConsoleModel {
|
|
|
179
181
|
* the activation is committed and the Owner decides whether to retire older
|
|
180
182
|
* principles. A projection error is also surfaced (never silent) but must
|
|
181
183
|
* not fail the approve.
|
|
184
|
+
*
|
|
185
|
+
* PRI-935 — the console holds no session, so it cannot supply the production
|
|
186
|
+
* round key and the projection runs `legacy_fifo_prefix_v1`. That is NOT a
|
|
187
|
+
* conservative forecast: FIFO structurally drops the newest activation from
|
|
188
|
+
* every truncated selection, while the live plugin route rotates and injects
|
|
189
|
+
* it within N user turns (PRI-904). Reporting that as "will NOT enter agent
|
|
190
|
+
* behavior until older ones are deactivated" told the Owner a falsehood and
|
|
191
|
+
* pushed them to deactivate healthy principles. The console now reports the
|
|
192
|
+
* policy it actually ran and, when the activation is merely rotated out of
|
|
193
|
+
* the current window, says so instead of claiming starvation.
|
|
182
194
|
*/
|
|
183
195
|
async checkPromptInjectionBudget(activationId) {
|
|
184
196
|
let projection;
|
|
@@ -199,7 +211,29 @@ export class ApprovalsConsoleModel {
|
|
|
199
211
|
// that instead of blaming the budget.
|
|
200
212
|
return `injection_excluded_non_budget: the activation is committed but excluded from the prompt injection projection for a non-budget reason.${projectionDetail} nextAction=inspect the artifact/activation pair via pd runtime activation list`;
|
|
201
213
|
}
|
|
202
|
-
|
|
214
|
+
// PRI-935: fair rotation is the live plugin-local policy (PRI-904). An
|
|
215
|
+
// activation that SOME round does inject is rotated out of the current
|
|
216
|
+
// window, not starved — report that bounded fact instead of the old FIFO
|
|
217
|
+
// starvation claim. The shared route genuinely does not rotate, so its
|
|
218
|
+
// empty reachability set falls through to the starvation branch below.
|
|
219
|
+
if (projection.productionRotates && projection.eventuallyInjectedActivationIds.includes(activationId)) {
|
|
220
|
+
const window = projection.eligibleCount;
|
|
221
|
+
return `injection_budget_queued: the activation is committed but sits outside the CURRENT prompt injection window (${projection.budget}c; ${projection.injectedActivationIds.length} of ${window} eligible activations injected this round; production selection policy fair_rotation_v1). Fair rotation advances the window by one position per recorded user turn, so this principle WILL enter agent behavior within at most ${window} consecutive user turns of a continuously advancing session — deactivating older principles is NOT required. nextAction=none; verify presence via the activations page or the prompt injection telemetry`;
|
|
222
|
+
}
|
|
223
|
+
// PRI-935: report the PRODUCTION policy, not the policy this forecast
|
|
224
|
+
// happened to run. The console holds no session round key, so its own
|
|
225
|
+
// projection is always `legacy_fifo_prefix_v1` even on a route where
|
|
226
|
+
// production rotates — labelling that value "the current production
|
|
227
|
+
// selection policy" would reintroduce exactly the lie this fix removes.
|
|
228
|
+
const policyNote = projection.productionRotates
|
|
229
|
+
? 'production selection policy fair_rotation_v1'
|
|
230
|
+
: 'production selection policy legacy_fifo_prefix_v1 (FIFO by activated_at)';
|
|
231
|
+
// On a rotating route the only unreachable entries are oversized ones, so
|
|
232
|
+
// say WHY rather than implying a positional problem that rotation solves.
|
|
233
|
+
const unreachableNote = projection.productionRotates
|
|
234
|
+
? ' (this forecast ran without a round key, so the window shown is the legacy FIFO prefix; the entry is unreachable because it exceeds the budget on its own, not because of its position)'
|
|
235
|
+
: '';
|
|
236
|
+
return `injection_budget_excluded: the activation is committed but the prompt injection budget (${projection.budget}c) is full and this activation is not reachable under the ${policyNote}; ${projection.injectedActivationIds.length}/${projection.eligibleCount} eligible injected${unreachableNote} — this principle will NOT enter agent behavior until capacity frees up. nextAction=review the activations page and deactivate superseded principles`;
|
|
203
237
|
}
|
|
204
238
|
/**
|
|
205
239
|
* Bug-O L3b fix: upgrade the ledger principle linked to `artifactId` from
|
|
@@ -7,6 +7,14 @@ export interface ApprovalGroup {
|
|
|
7
7
|
* review the candidate content instead of staring at an internal ID. */
|
|
8
8
|
candidateDescription?: string;
|
|
9
9
|
status: 'pending' | 'approved' | 'rejected';
|
|
10
|
+
/**
|
|
11
|
+
* PRI-935: whether this candidate's own serialized prompt entry fits the
|
|
12
|
+
* injection budget on its own. `false` means the entry is oversized, so fair
|
|
13
|
+
* rotation can NEVER inject it �?the pre-approval badge must not promise it
|
|
14
|
+
* will come around. Computed server-side with the production serializer
|
|
15
|
+
* (the client has no access to the escaped, serialized size).
|
|
16
|
+
*/
|
|
17
|
+
fitsPromptBudget?: boolean;
|
|
10
18
|
records: {
|
|
11
19
|
id: string;
|
|
12
20
|
artifactId: string;
|
|
@@ -28,11 +36,11 @@ export declare class ApprovalsGroupedConsoleModel {
|
|
|
28
36
|
getApprovalsGrouped(): Promise<ApprovalsGroupedResponse>;
|
|
29
37
|
/**
|
|
30
38
|
* PRI-908: recompute the production prompt injection projection so the
|
|
31
|
-
* focus page can forecast "approved
|
|
39
|
+
* focus page can forecast "approved �?effective" BEFORE the Owner decides.
|
|
32
40
|
* PR #1844 follow-up: the forecast follows the workspace's REAL injection
|
|
33
|
-
* route (`buildLivePromptInjectionProjection`
|
|
41
|
+
* route (`buildLivePromptInjectionProjection` �?legacy trimToBudget vs
|
|
34
42
|
* shared render via abstraction_layer_v1), matching the PRI-890 approve-time
|
|
35
|
-
* check. Advisory only: a projection failure omits the field
|
|
43
|
+
* check. Advisory only: a projection failure omits the field �?the
|
|
36
44
|
* approve-time warning remains the fail-loud exclusion report (rc-9), so
|
|
37
45
|
* this must never fail the grouped read.
|
|
38
46
|
*/
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { SqliteConnection, SqliteApprovalQueueStore, SqlitePIArtifactStore, ApprovalQueue, } from '@principles/core/runtime-v2';
|
|
1
|
+
import { SqliteConnection, SqliteApprovalQueueStore, SqlitePIArtifactStore, ApprovalQueue, RUNTIME_V2_PRINCIPLE_BUDGET, resolvePrincipleFromArtifact, } from '@principles/core/runtime-v2';
|
|
2
2
|
import { buildLivePromptInjectionProjection } from '@principles/host-runtime';
|
|
3
|
+
import { escapeXml } from '@principles/core/prompt-builder';
|
|
3
4
|
import { loadLedger } from '@principles/core/principle-tree-ledger';
|
|
4
5
|
import { createArtifactPrincipleResolutionDeps, resolveArtifactPrincipleId, } from './artifact-principle-resolver.js';
|
|
5
6
|
import * as fs from 'node:fs';
|
|
@@ -16,7 +17,7 @@ function isMissingTableError(err) {
|
|
|
16
17
|
/**
|
|
17
18
|
* Wave 7: Extract a human-readable description from a PIArtifact's contentJson.
|
|
18
19
|
*
|
|
19
|
-
* The artifact kinds have different contentJson shapes
|
|
20
|
+
* The artifact kinds have different contentJson shapes �?this function unifies
|
|
20
21
|
* them into a single description string the Owner can actually read to make a
|
|
21
22
|
* review decision, instead of staring at a fabricated principleId.
|
|
22
23
|
*
|
|
@@ -49,7 +50,7 @@ function extractCandidateDescription(contentJson) {
|
|
|
49
50
|
const principleText = principleRaw ? principleRaw.trim() : null;
|
|
50
51
|
const ruleText = ruleRaw ? ruleRaw.trim() : null;
|
|
51
52
|
if (principleText && ruleText)
|
|
52
|
-
return `${principleText}
|
|
53
|
+
return `${principleText} �?${ruleText}`;
|
|
53
54
|
if (ruleText)
|
|
54
55
|
return ruleText;
|
|
55
56
|
if (principleText)
|
|
@@ -64,7 +65,7 @@ function extractCandidateDescription(contentJson) {
|
|
|
64
65
|
const title = typeof draft.title === 'string' ? draft.title.trim() : '';
|
|
65
66
|
const statement = typeof draft.statement === 'string' ? draft.statement.trim() : '';
|
|
66
67
|
if (title && statement)
|
|
67
|
-
return `${title}
|
|
68
|
+
return `${title} �?${statement}`;
|
|
68
69
|
if (title)
|
|
69
70
|
return title;
|
|
70
71
|
if (statement)
|
|
@@ -88,6 +89,48 @@ function extractCandidateDescription(contentJson) {
|
|
|
88
89
|
}
|
|
89
90
|
return null;
|
|
90
91
|
}
|
|
92
|
+
/**
|
|
93
|
+
* PRI-935: would this candidate's own prompt entry fit the injection budget if
|
|
94
|
+
* it were the ONLY entry?
|
|
95
|
+
*
|
|
96
|
+
* Uses the production text resolver and the production entry format/escaping
|
|
97
|
+
* (`- [<id>] <text>` via escapeXml, plus the section header `trimToBudget`
|
|
98
|
+
* charges before any entry) so the answer matches what the real selector
|
|
99
|
+
* computes rather than an approximation. An entry that cannot fit alone is
|
|
100
|
+
* "oversized" in the core selector and is skipped in EVERY rotation round, so
|
|
101
|
+
* fair rotation cannot deliver it.
|
|
102
|
+
*
|
|
103
|
+
* Degrades to `false` (do not promise rotation) whenever the real size cannot be
|
|
104
|
+
* determined �?an unknown size must never become a promise (rc-9).
|
|
105
|
+
*/
|
|
106
|
+
function candidateFitsPromptBudget(artifact) {
|
|
107
|
+
if (!artifact?.contentJson)
|
|
108
|
+
return false;
|
|
109
|
+
// The pending candidate has no activation row yet, but the resolver only
|
|
110
|
+
// needs the artifact plus an id pair �?this mirrors what approve will do.
|
|
111
|
+
const resolved = resolvePrincipleFromArtifact({
|
|
112
|
+
artifact_id: artifact.artifactId,
|
|
113
|
+
artifact_kind: artifact.artifactKind,
|
|
114
|
+
content_json: artifact.contentJson,
|
|
115
|
+
validation_status: artifact.validationStatus,
|
|
116
|
+
}, {
|
|
117
|
+
activationId: `pending:${artifact.artifactId}`,
|
|
118
|
+
idempotencyKey: `pending:${artifact.artifactId}::prompt`,
|
|
119
|
+
artifactId: artifact.artifactId,
|
|
120
|
+
channel: 'prompt',
|
|
121
|
+
action: 'prompt_activate',
|
|
122
|
+
targetRef: `pending://${artifact.artifactId}`,
|
|
123
|
+
activatedAt: new Date(0).toISOString(),
|
|
124
|
+
deactivatedAt: null,
|
|
125
|
+
});
|
|
126
|
+
if (!resolved.ok)
|
|
127
|
+
return false;
|
|
128
|
+
// The real principle id is unknown pre-approval; a UUID allowance is the
|
|
129
|
+
// worst case, and the actual id can only make the entry smaller.
|
|
130
|
+
const entry = `- [${escapeXml('00000000-0000-0000-0000-000000000000')}] ${escapeXml(resolved.principle.text)}`;
|
|
131
|
+
const header = 'Runtime V2 activated principles:';
|
|
132
|
+
return entry.length + 1 + header.length <= RUNTIME_V2_PRINCIPLE_BUDGET;
|
|
133
|
+
}
|
|
91
134
|
export class ApprovalsGroupedConsoleModel {
|
|
92
135
|
workspaceDir;
|
|
93
136
|
constructor(workspaceDir) {
|
|
@@ -96,7 +139,7 @@ export class ApprovalsGroupedConsoleModel {
|
|
|
96
139
|
async getApprovalsGrouped() {
|
|
97
140
|
const stateDbPath = path.join(this.workspaceDir, '.pd', 'state.db');
|
|
98
141
|
if (!fs.existsSync(stateDbPath)) {
|
|
99
|
-
return { groups: [], generatedAt: new Date().toISOString(), note: 'state.db not found
|
|
142
|
+
return { groups: [], generatedAt: new Date().toISOString(), note: 'state.db not found �?workspace may not be initialized' };
|
|
100
143
|
}
|
|
101
144
|
const conn = new SqliteConnection({ workspaceDir: this.workspaceDir, readonly: true });
|
|
102
145
|
try {
|
|
@@ -109,21 +152,24 @@ export class ApprovalsGroupedConsoleModel {
|
|
|
109
152
|
}
|
|
110
153
|
catch (err) {
|
|
111
154
|
if (isMissingTableError(err)) {
|
|
112
|
-
return { groups: [], generatedAt: new Date().toISOString(), note: 'approval table not found
|
|
155
|
+
return { groups: [], generatedAt: new Date().toISOString(), note: 'approval table not found �?workspace may not be initialized' };
|
|
113
156
|
}
|
|
114
157
|
throw err;
|
|
115
158
|
}
|
|
116
|
-
// Build artifactId
|
|
159
|
+
// Build artifactId �?sourcePrincipleId map AND artifactId �?candidateDescription map.
|
|
117
160
|
// Wave 7: candidateDescription lets FocusPage show human-readable content
|
|
118
161
|
// instead of a fabricated principleId.
|
|
119
162
|
//
|
|
120
163
|
// PRI-768 v5 follow-up (F3): sourcePrincipleId is null for most scribe
|
|
121
164
|
// artifacts, which used to collapse every group to `unlinked:<id>` and
|
|
122
165
|
// dead-end the owner decision UI. When the column is missing, fall back
|
|
123
|
-
// to the shared lineage resolver (scribe
|
|
124
|
-
// ledger derivedFromPainIds) so groups bind to the REAL ledger id.
|
|
166
|
+
// to the shared lineage resolver (scribe �?dreamer seed �?candidateId �? // ledger derivedFromPainIds) so groups bind to the REAL ledger id.
|
|
125
167
|
const artifactPrincipleMap = new Map();
|
|
126
168
|
const artifactDescriptionMap = new Map();
|
|
169
|
+
// PRI-935: per-artifact "does this entry fit the budget alone" fact, so
|
|
170
|
+
// the pre-approval badge never promises rotation for an entry that is
|
|
171
|
+
// oversized (rotation cannot rescue a per-entry overflow).
|
|
172
|
+
const artifactFitsBudgetMap = new Map();
|
|
127
173
|
const stateDir = path.join(this.workspaceDir, '.state');
|
|
128
174
|
const resolutionDeps = createArtifactPrincipleResolutionDeps(conn, artifactStore, this.workspaceDir);
|
|
129
175
|
for (const approval of allApprovals) {
|
|
@@ -138,11 +184,13 @@ export class ApprovalsGroupedConsoleModel {
|
|
|
138
184
|
else {
|
|
139
185
|
artifactDescriptionMap.set(approval.artifactId, null);
|
|
140
186
|
}
|
|
187
|
+
artifactFitsBudgetMap.set(approval.artifactId, candidateFitsPromptBudget(artifact));
|
|
141
188
|
}
|
|
142
189
|
catch (err) {
|
|
143
190
|
if (isMissingTableError(err)) {
|
|
144
191
|
artifactPrincipleMap.set(approval.artifactId, null);
|
|
145
192
|
artifactDescriptionMap.set(approval.artifactId, null);
|
|
193
|
+
artifactFitsBudgetMap.set(approval.artifactId, false);
|
|
146
194
|
}
|
|
147
195
|
else {
|
|
148
196
|
throw err;
|
|
@@ -159,9 +207,9 @@ export class ApprovalsGroupedConsoleModel {
|
|
|
159
207
|
}
|
|
160
208
|
}
|
|
161
209
|
catch {
|
|
162
|
-
// Ledger not available
|
|
210
|
+
// Ledger not available �?will fall back to principleId
|
|
163
211
|
}
|
|
164
|
-
// Group by principleId (null
|
|
212
|
+
// Group by principleId (null �?"unlinked")
|
|
165
213
|
const groupMap = new Map();
|
|
166
214
|
for (const approval of allApprovals) {
|
|
167
215
|
const mappedPrincipleId = artifactPrincipleMap.get(approval.artifactId);
|
|
@@ -205,6 +253,11 @@ export class ApprovalsGroupedConsoleModel {
|
|
|
205
253
|
principleTitle,
|
|
206
254
|
candidateDescription,
|
|
207
255
|
status,
|
|
256
|
+
// PRI-935: undefined-safe �?absent when the artifact could not be
|
|
257
|
+
// read, which the UI treats as "do not promise rotation".
|
|
258
|
+
...(firstArtifactId !== undefined && artifactFitsBudgetMap.has(firstArtifactId)
|
|
259
|
+
? { fitsPromptBudget: artifactFitsBudgetMap.get(firstArtifactId) === true }
|
|
260
|
+
: {}),
|
|
208
261
|
records,
|
|
209
262
|
});
|
|
210
263
|
}
|
|
@@ -223,11 +276,11 @@ export class ApprovalsGroupedConsoleModel {
|
|
|
223
276
|
}
|
|
224
277
|
/**
|
|
225
278
|
* PRI-908: recompute the production prompt injection projection so the
|
|
226
|
-
* focus page can forecast "approved
|
|
279
|
+
* focus page can forecast "approved �?effective" BEFORE the Owner decides.
|
|
227
280
|
* PR #1844 follow-up: the forecast follows the workspace's REAL injection
|
|
228
|
-
* route (`buildLivePromptInjectionProjection`
|
|
281
|
+
* route (`buildLivePromptInjectionProjection` �?legacy trimToBudget vs
|
|
229
282
|
* shared render via abstraction_layer_v1), matching the PRI-890 approve-time
|
|
230
|
-
* check. Advisory only: a projection failure omits the field
|
|
283
|
+
* check. Advisory only: a projection failure omits the field �?the
|
|
231
284
|
* approve-time warning remains the fail-loud exclusion report (rc-9), so
|
|
232
285
|
* this must never fail the grouped read.
|
|
233
286
|
*/
|
|
@@ -239,6 +292,13 @@ export class ApprovalsGroupedConsoleModel {
|
|
|
239
292
|
budget: projection.budget,
|
|
240
293
|
usedChars: projection.usedChars,
|
|
241
294
|
truncated: projection.truncated,
|
|
295
|
+
// PRI-935: the badge must describe what the PRODUCTION route will do,
|
|
296
|
+
// not which policy this session-less forecast happened to run.
|
|
297
|
+
// Without it the UI can only say "will queue", which under fair
|
|
298
|
+
// rotation reads as permanent starvation and pushes the Owner to
|
|
299
|
+
// deactivate healthy principles.
|
|
300
|
+
productionRotates: projection.productionRotates,
|
|
301
|
+
eligibleCount: projection.eligibleCount,
|
|
242
302
|
},
|
|
243
303
|
};
|
|
244
304
|
}
|
|
@@ -1,14 +1,24 @@
|
|
|
1
1
|
const UPDATE_APPLY_FULL_DEFAULT_TIMEOUT_MS = 180000;
|
|
2
|
+
const UPDATE_CHECK_DEFAULT_TIMEOUT_MS = 30000;
|
|
2
3
|
// Node clamps delays above this limit to 1ms instead of honoring the timeout.
|
|
3
|
-
const
|
|
4
|
-
|
|
5
|
-
const raw = process.env
|
|
4
|
+
const MAX_TIMEOUT_MS = 2_147_483_647;
|
|
5
|
+
function resolveTimeoutMs(envKey, defaultMs) {
|
|
6
|
+
const raw = process.env[envKey];
|
|
6
7
|
if (raw === undefined || raw.trim().length === 0)
|
|
7
|
-
return
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
8
|
+
return defaultMs;
|
|
9
|
+
// Plain decimal digits only: Number() also reads '0x10' as 16, so a hex or
|
|
10
|
+
// exponent form would silently collapse the budget instead of widening it.
|
|
11
|
+
const trimmed = raw.trim();
|
|
12
|
+
const parsed = /^\d+$/.test(trimmed) ? Number(trimmed) : Number.NaN;
|
|
13
|
+
if (!Number.isSafeInteger(parsed) || parsed <= 0 || parsed > MAX_TIMEOUT_MS) {
|
|
14
|
+
console.error(`[pd-console] ${envKey}=${JSON.stringify(raw)} must be a plain decimal integer between 1 and ${MAX_TIMEOUT_MS} — using the ${defaultMs}ms default.`);
|
|
15
|
+
return defaultMs;
|
|
12
16
|
}
|
|
13
17
|
return parsed;
|
|
14
18
|
}
|
|
19
|
+
export function resolveApplyFullTimeoutMs() {
|
|
20
|
+
return resolveTimeoutMs('PD_UPDATE_APPLY_FULL_TIMEOUT_MS', UPDATE_APPLY_FULL_DEFAULT_TIMEOUT_MS);
|
|
21
|
+
}
|
|
22
|
+
export function resolveCheckTimeoutMs() {
|
|
23
|
+
return resolveTimeoutMs('PD_UPDATE_CHECK_TIMEOUT_MS', UPDATE_CHECK_DEFAULT_TIMEOUT_MS);
|
|
24
|
+
}
|
|
@@ -17,6 +17,21 @@ export interface PromptInjectionBudgetStatus {
|
|
|
17
17
|
budget: number;
|
|
18
18
|
/** Chars currently injected into the agent prompt. */
|
|
19
19
|
usedChars: number;
|
|
20
|
-
/**
|
|
20
|
+
/**
|
|
21
|
+
* True when the CURRENT projection already truncated — the newly approved
|
|
22
|
+
* principle may not enter the prompt on this turn. PRI-935: this is
|
|
23
|
+
* policy-dependent (see `selectionPolicy`); the badge copy must not imply
|
|
24
|
+
* permanent starvation when rotation is in play.
|
|
25
|
+
*/
|
|
21
26
|
truncated: boolean;
|
|
27
|
+
/**
|
|
28
|
+
* PRI-935: whether the PRODUCTION route rotates. This — not the forecast's
|
|
29
|
+
* own selection policy — decides whether an out-of-window approval is queued
|
|
30
|
+
* behind rotation or genuinely starved, and therefore which badge copy is
|
|
31
|
+
* truthful. Optional so a pre-PRI-935 payload still validates; absent
|
|
32
|
+
* behaves as `false` (the conservative pre-PRI-935 reading).
|
|
33
|
+
*/
|
|
34
|
+
productionRotates?: boolean;
|
|
35
|
+
/** PRI-935: eligible activations that reached the selector. */
|
|
36
|
+
eligibleCount?: number;
|
|
22
37
|
}
|
|
@@ -111,13 +111,18 @@
|
|
|
111
111
|
"approveSucceededCount": "Approved {{count}} principle(s)",
|
|
112
112
|
"decidedApproved": "Approved ✓",
|
|
113
113
|
"decidedRejected": "Rejected",
|
|
114
|
-
"approveQueueBadge": "May queue after approval: prompt injection slots are full ({{used}}/{{budget}} chars).
|
|
114
|
+
"approveQueueBadge": "May queue after approval: prompt injection slots are full ({{used}}/{{budget}} chars).",
|
|
115
|
+
"approveQueueBadgeRotation": "May queue after approval: prompt injection slots are full ({{used}}/{{budget}} chars). Selection is rotating fairly, so new principles come around on later turns — no need to deactivate older principles.",
|
|
116
|
+
"approveQueueBadgeOversized": "May queue after approval: prompt injection slots are full ({{used}}/{{budget}} chars). This principle is itself larger than the injection limit, so rotation cannot fit it — shorten its content first.",
|
|
115
117
|
"approveWarningDetail": "Server details",
|
|
116
118
|
"goActivationCta": "Open activations",
|
|
117
119
|
"approveWarning": {
|
|
118
120
|
"budgetExcludedTitle": "Approved, but not yet in effect",
|
|
119
|
-
"budgetExcludedBody": "This principle was approved and saved, but the prompt injection
|
|
120
|
-
"budgetExcludedBodySimple": "This principle was approved and saved, but the prompt injection
|
|
121
|
+
"budgetExcludedBody": "This principle was approved and saved, but the prompt injection budget is full ({{budget}}-char cap) and only {{earlierCount}} principle(s) fit the current window. It will not affect agent behavior until capacity frees up.",
|
|
122
|
+
"budgetExcludedBodySimple": "This principle was approved and saved, but the prompt injection budget is full. It will not affect agent behavior until capacity frees up.",
|
|
123
|
+
"queuedTitle": "Approved — queued to take effect",
|
|
124
|
+
"queuedBody": "This principle was approved and saved. The prompt injection window is full ({{budget}}-char cap), so it is not in the prompt this turn. Selection rotates fairly, advancing one position per user turn, so it will come around within at most {{turns}} consecutive turns — there is no need to deactivate other principles.",
|
|
125
|
+
"queuedBodySimple": "This principle was approved and saved and is queued for the prompt. Selection rotates fairly, so it will come around on a later turn — there is no need to deactivate other principles.",
|
|
121
126
|
"excludedTitle": "Approved, but not yet in effect",
|
|
122
127
|
"excludedNonBudgetBody": "This principle was approved and saved but is not in the agent prompt for a non-budget reason. Check the activations page for status.",
|
|
123
128
|
"checkFailedTitle": "Approved — effect unverified",
|
|
@@ -111,13 +111,18 @@
|
|
|
111
111
|
"approveSucceededCount": "已批准 {{count}} 条原则",
|
|
112
112
|
"decidedApproved": "已批准 ✓",
|
|
113
113
|
"decidedRejected": "已拒绝",
|
|
114
|
-
"approveQueueBadge": "批准后可能排队:提示词注入位已满({{used}}/{{budget}}
|
|
114
|
+
"approveQueueBadge": "批准后可能排队:提示词注入位已满({{used}}/{{budget}} 字符)。",
|
|
115
|
+
"approveQueueBadgeRotation": "批准后可能排队:提示词注入位已满({{used}}/{{budget}} 字符)。当前按公平轮转选取,新原则会在后续轮次自行轮到,无需停用旧原则。",
|
|
116
|
+
"approveQueueBadgeOversized": "批准后可能排队:提示词注入位已满({{used}}/{{budget}} 字符)。但这条原则本身已超出注入位上限,轮转也装不下它——需要先精简它的内容。",
|
|
115
117
|
"approveWarningDetail": "原始信息",
|
|
116
118
|
"goActivationCta": "前往生效情况",
|
|
117
119
|
"approveWarning": {
|
|
118
120
|
"budgetExcludedTitle": "已批准,但暂未生效",
|
|
119
|
-
"budgetExcludedBody": "这条原则已批准并保存,但提示词注入位已满(上限 {{budget}}
|
|
120
|
-
"budgetExcludedBodySimple": "
|
|
121
|
+
"budgetExcludedBody": "这条原则已批准并保存,但提示词注入位已满(上限 {{budget}} 字符),当前仅 {{earlierCount}} 条原则进入注入窗口。它不会影响智能体行为,直到有容量释放。",
|
|
122
|
+
"budgetExcludedBodySimple": "这条原则已批准并保存,但提示词注入位已满。它不会影响智能体行为,直到有容量释放。",
|
|
123
|
+
"queuedTitle": "已批准,正在排队生效",
|
|
124
|
+
"queuedBody": "这条原则已批准并保存。当前提示词注入窗口已满(上限 {{budget}} 字符),所以它这一轮还没进入提示词。按公平轮转,每个用户回合窗口前移一位,因此最多再经过 {{turns}} 个连续回合它就会自行进入——不需要停用其他原则。",
|
|
125
|
+
"queuedBodySimple": "这条原则已批准并保存,正在排队进入提示词。按公平轮转,它会在后续回合自行进入,不需要停用其他原则。",
|
|
121
126
|
"excludedTitle": "已批准,但暂未生效",
|
|
122
127
|
"excludedNonBudgetBody": "这条原则已批准并保存,但因非预算原因暂时未进入智能体提示词。可前往生效情况页查看状态。",
|
|
123
128
|
"checkFailedTitle": "已批准,生效状态待确认",
|
|
@@ -148,10 +148,20 @@ function PendingReviewCard({ group, actionsLockedReason, onDecisionApplied, prom
|
|
|
148
148
|
function notifySuccessWarnings(successWarnings) {
|
|
149
149
|
const segments = successWarnings.flatMap((warning) => splitApprovalWarnings(warning));
|
|
150
150
|
const localized = segments.map((warning) => localizeApprovalWarning(warning, t));
|
|
151
|
-
|
|
151
|
+
// PRI-935: the raw server text is still preserved verbatim (rc-9 — never
|
|
152
|
+
// dropped), but it is operator-facing English, not Owner-facing prose. It
|
|
153
|
+
// used to be appended flat under the localized body, so the toast rendered
|
|
154
|
+
// as one mixed Chinese/English block and the machine text visually
|
|
155
|
+
// dominated the message. Collapse it behind a native <details> so the
|
|
156
|
+
// Owner reads plain language first and can still open the raw evidence.
|
|
157
|
+
const renderDetail = (detail) => (_jsxs("details", { className: "mt-2 text-[11.5px] opacity-80", children: [_jsx("summary", { className: "cursor-pointer select-none", children: t("pages.focus.approveWarningDetail", { defaultValue: "原始信息" }) }), _jsx("pre", { className: "mt-1 max-h-40 overflow-auto whitespace-pre-wrap break-words font-mono text-[11px]", children: detail })] }));
|
|
152
158
|
for (const item of localized.slice(0, MAX_WARNING_TOASTS)) {
|
|
153
159
|
toast.warning(item.title, {
|
|
154
|
-
description: item.body
|
|
160
|
+
description: item.body
|
|
161
|
+
? (_jsxs(_Fragment, { children: [item.body, renderDetail(item.detail)] }))
|
|
162
|
+
// An unrecognized code degrades to the raw string as the whole
|
|
163
|
+
// message (rc-9) — there is no localized body to sit above it.
|
|
164
|
+
: item.detail,
|
|
155
165
|
duration: 15000,
|
|
156
166
|
action: item.activationAction
|
|
157
167
|
? { label: t("pages.focus.goActivationCta", { defaultValue: "前往生效情况" }), onClick: () => navigate("/activation") }
|
|
@@ -161,7 +171,7 @@ function PendingReviewCard({ group, actionsLockedReason, onDecisionApplied, prom
|
|
|
161
171
|
const overflow = localized.slice(MAX_WARNING_TOASTS);
|
|
162
172
|
if (overflow.length > 0) {
|
|
163
173
|
toast.warning(t("pages.focus.approveWarning.overflowTitle", { defaultValue: "还有 {{count}} 条警告", count: overflow.length }), {
|
|
164
|
-
description: overflow.map((item) =>
|
|
174
|
+
description: (_jsx(_Fragment, { children: overflow.map((item) => (_jsxs("div", { className: "mb-1 last:mb-0", children: [_jsx("div", { children: item.title }), item.body ? _jsx("div", { children: item.body }) : null, renderDetail(item.detail)] }, item.detail))) })),
|
|
165
175
|
duration: 15000,
|
|
166
176
|
});
|
|
167
177
|
}
|
|
@@ -296,11 +306,23 @@ function PendingReviewCard({ group, actionsLockedReason, onDecisionApplied, prom
|
|
|
296
306
|
? "border-green/40 bg-green/10 text-green"
|
|
297
307
|
: "border-line bg-surface/80 text-ink-3"}`, role: "status", "data-testid": `decided-banner-${group.principleId}`, children: decidedOutcome === "approved"
|
|
298
308
|
? t("pages.focus.decidedApproved", { defaultValue: "已批准 ✓ 正在写入生效记录…" })
|
|
299
|
-
: t("pages.focus.decidedRejected", { defaultValue: "已拒绝" }) })), _jsxs("div", { className: "flex items-center gap-2 flex-wrap", children: [_jsx("span", { className: "inline-flex items-center border border-line rounded-[2px] px-[7px] py-1 font-mono text-[11px] text-ink-3 bg-surface/80 uppercase", children: channelLabel }), isReversible && (_jsx("span", { className: "inline-flex items-center border border-amber/35 text-amber rounded-[2px] px-[7px] py-1 font-mono text-[11px] uppercase", children: primaryChannel === "prompt" ? t("pages.focus.tagReversible") : t("pages.focus.tagLowRisk") }))] }), _jsx("div", { className: "mt-[14px] mb-2 font-semibold text-ink leading-snug", children: displayTitle }), _jsx("div", { className: "mt-2 px-3 py-2 bg-surface/60 border-l-2 border-gov text-ink-3 text-[13px] leading-snug", children: t("pages.focus.evidenceSummary", { count: group.records.length }) }), actionsLocked && (_jsxs("div", { className: "mt-3 rounded-[3px] border border-amber/40 bg-amber/5 p-3", "data-testid": `pending-actions-locked-${group.principleId}`, children: [_jsx("p", { className: "text-ink-2 text-[12.5px] leading-relaxed", children: actionsLockedReason }), _jsx(Link, { to: "/settings", "data-testid": `pending-actions-locked-settings-${group.principleId}`, className: "mt-2 inline-flex items-center border border-line text-ink bg-surface rounded-[3px] px-[14px] py-[6px] text-[12.5px] hover:border-line-2 transition-colors", children: t("pages.focus.ownerDecision.goSettingsCta") })] })), decidedOutcome === null && primaryChannel === "prompt" && promptInjection?.truncated === true && (_jsx("div", { className: "mt-3 rounded-[3px] border border-amber/40 bg-amber/5 px-3 py-2 text-[12.5px] text-amber", "data-testid": `approve-queue-badge-${group.principleId}`, children:
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
309
|
+
: t("pages.focus.decidedRejected", { defaultValue: "已拒绝" }) })), _jsxs("div", { className: "flex items-center gap-2 flex-wrap", children: [_jsx("span", { className: "inline-flex items-center border border-line rounded-[2px] px-[7px] py-1 font-mono text-[11px] text-ink-3 bg-surface/80 uppercase", children: channelLabel }), isReversible && (_jsx("span", { className: "inline-flex items-center border border-amber/35 text-amber rounded-[2px] px-[7px] py-1 font-mono text-[11px] uppercase", children: primaryChannel === "prompt" ? t("pages.focus.tagReversible") : t("pages.focus.tagLowRisk") }))] }), _jsx("div", { className: "mt-[14px] mb-2 font-semibold text-ink leading-snug", children: displayTitle }), _jsx("div", { className: "mt-2 px-3 py-2 bg-surface/60 border-l-2 border-gov text-ink-3 text-[13px] leading-snug", children: t("pages.focus.evidenceSummary", { count: group.records.length }) }), actionsLocked && (_jsxs("div", { className: "mt-3 rounded-[3px] border border-amber/40 bg-amber/5 p-3", "data-testid": `pending-actions-locked-${group.principleId}`, children: [_jsx("p", { className: "text-ink-2 text-[12.5px] leading-relaxed", children: actionsLockedReason }), _jsx(Link, { to: "/settings", "data-testid": `pending-actions-locked-settings-${group.principleId}`, className: "mt-2 inline-flex items-center border border-line text-ink bg-surface rounded-[3px] px-[14px] py-[6px] text-[12.5px] hover:border-line-2 transition-colors", children: t("pages.focus.ownerDecision.goSettingsCta") })] })), decidedOutcome === null && primaryChannel === "prompt" && promptInjection?.truncated === true && (_jsx("div", { className: "mt-3 rounded-[3px] border border-amber/40 bg-amber/5 px-3 py-2 text-[12.5px] text-amber", "data-testid": `approve-queue-badge-${group.principleId}`, children: promptInjection.productionRotates === true && group.fitsPromptBudget === true
|
|
310
|
+
? t("pages.focus.approveQueueBadgeRotation", {
|
|
311
|
+
defaultValue: "批准后可能排队:提示词注入位已满({{used}}/{{budget}} 字符)。当前按公平轮转选取,新原则会在后续轮次自行轮到,无需停用旧原则。",
|
|
312
|
+
used: promptInjection.usedChars,
|
|
313
|
+
budget: promptInjection.budget,
|
|
314
|
+
})
|
|
315
|
+
: promptInjection.productionRotates === true
|
|
316
|
+
? t("pages.focus.approveQueueBadgeOversized", {
|
|
317
|
+
defaultValue: "批准后可能排队:提示词注入位已满({{used}}/{{budget}} 字符)。这条原则本身已超出注入位上限,轮转也无法让它进入提示词——需要先精简它的内容。",
|
|
318
|
+
used: promptInjection.usedChars,
|
|
319
|
+
budget: promptInjection.budget,
|
|
320
|
+
})
|
|
321
|
+
: t("pages.focus.approveQueueBadge", {
|
|
322
|
+
defaultValue: "批准后可能排队:提示词注入位已满({{used}}/{{budget}} 字符)。",
|
|
323
|
+
used: promptInjection.usedChars,
|
|
324
|
+
budget: promptInjection.budget,
|
|
325
|
+
}) })), _jsxs("div", { className: "flex gap-2 mt-4 flex-wrap items-center", children: [_jsx("button", { type: "button", onClick: handleApprove, disabled: decisionDisabled, "data-testid": `approve-btn-${group.principleId}`, className: "inline-flex items-center border border-gov bg-gov text-paper rounded-[3px] px-[14px] py-[6px] text-[12.5px] font-medium hover:bg-gov-2 transition-colors disabled:opacity-50 disabled:cursor-not-allowed focus-visible:outline-2 focus-visible:outline-gov focus-visible:outline-offset-2", children: actionLoading ? t("common.loading") + "…" : t("pages.focus.approveAction", { defaultValue: "批准" }) }), _jsx("button", { type: "button", onClick: () => { setShowEditInput((v) => !v); setShowRejectInput(false); }, disabled: decisionDisabled, "data-testid": `edit-btn-${group.principleId}`, className: "inline-flex items-center border border-line bg-surface text-ink rounded-[3px] px-[14px] py-[6px] text-[12.5px] hover:border-line-2 transition-colors disabled:opacity-50 disabled:cursor-not-allowed focus-visible:outline-2 focus-visible:outline-gov focus-visible:outline-offset-2", children: t("pages.focus.editAction", { defaultValue: "编辑" }) }), _jsx("button", { type: "button", onClick: () => { setShowRejectInput((v) => !v); setShowEditInput(false); }, disabled: decisionDisabled, "data-testid": `reject-btn-${group.principleId}`, className: "inline-flex items-center border border-line bg-surface text-ink rounded-[3px] px-[14px] py-[6px] text-[12.5px] hover:border-line-2 transition-colors disabled:opacity-50 disabled:cursor-not-allowed focus-visible:outline-2 focus-visible:outline-gov focus-visible:outline-offset-2", children: t("pages.focus.rejectAction", { defaultValue: "拒绝" }) }), _jsx(Link, { to: "/evidence", className: "inline-flex items-center text-gov text-[12.5px] hover:underline focus-visible:outline-2 focus-visible:outline-gov focus-visible:outline-offset-2", children: t("pages.focus.viewFullChain") })] }), showRejectInput && (_jsxs("div", { className: "mt-3 p-3 border border-danger/20 rounded-[var(--radius-md)]", children: [_jsx("label", { className: "block font-mono text-[11px] uppercase tracking-[0.08em] text-ink-3 mb-2", children: t("pages.focus.rejectReasonLabel", { defaultValue: "拒绝原因(必填)" }) }), _jsx("textarea", { value: rejectReason, onChange: (e) => setRejectReason(e.target.value), "data-testid": `reject-reason-${group.principleId}`, placeholder: t("pages.focus.rejectReasonPlaceholder", { defaultValue: "请说明拒绝原因" }), className: "w-full border border-line rounded-[var(--radius-md)] bg-surface text-ink px-3 py-2 text-[13px] min-h-[80px] resize-y focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gov" }), _jsxs("div", { className: "flex gap-2 mt-2", children: [_jsx("button", { type: "button", onClick: handleReject, disabled: !rejectReason.trim() || actionLoading, "data-testid": `confirm-reject-${group.principleId}`, className: "inline-flex items-center border border-danger text-danger bg-surface rounded-[3px] px-[14px] py-[6px] text-[12.5px] hover:bg-danger/5 transition-colors disabled:opacity-50 disabled:cursor-not-allowed", children: t("pages.focus.confirmReject", { defaultValue: "确认拒绝" }) }), _jsx("button", { type: "button", onClick: () => { setShowRejectInput(false); setRejectReason(""); }, className: "inline-flex items-center border border-line bg-surface text-ink rounded-[3px] px-[14px] py-[6px] text-[12.5px] hover:border-line-2 transition-colors", children: t("pages.focus.cancelAction", { defaultValue: "取消" }) })] })] })), showEditInput && (_jsxs("div", { className: "mt-3 p-3 border border-gov/20 rounded-[var(--radius-md)]", children: [_jsx("label", { className: "block font-mono text-[11px] uppercase tracking-[0.08em] text-ink-3 mb-2", children: t("pages.focus.editReasonLabel", { defaultValue: "编辑原因(必填)" }) }), _jsxs("div", { className: "mb-2 text-ink-4 text-[12px] font-mono", children: [t("pages.focus.currentArtifactLabel", { defaultValue: "当前工件" }), ": ", currentArtifactId] }), _jsx("input", { type: "text", value: newArtifactId, onChange: (e) => setNewArtifactId(e.target.value), "data-testid": `edit-new-artifact-${group.principleId}`, placeholder: t("pages.focus.editNewArtifactPlaceholder", { defaultValue: "输入新的已验证工件 ID" }), className: "w-full border border-line rounded-[var(--radius-md)] bg-surface text-ink px-3 py-2 text-[13px] mb-2 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gov", "aria-label": t("pages.focus.editNewArtifactPlaceholder", { defaultValue: "输入新的已验证工件 ID" }) }), _jsx("textarea", { value: editReason, onChange: (e) => setEditReason(e.target.value), "data-testid": `edit-reason-${group.principleId}`, placeholder: t("pages.focus.editReasonPlaceholder", { defaultValue: "请说明编辑原因" }), className: "w-full border border-line rounded-[var(--radius-md)] bg-surface text-ink px-3 py-2 text-[13px] min-h-[80px] resize-y focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gov" }), _jsxs("div", { className: "flex gap-2 mt-2", children: [_jsx("button", { type: "button", onClick: handleEdit, disabled: !editReason.trim() || !newArtifactId.trim() || actionLoading, "data-testid": `confirm-edit-${group.principleId}`, className: "inline-flex items-center border border-gov text-gov bg-surface rounded-[3px] px-[14px] py-[6px] text-[12.5px] hover:bg-gov/5 transition-colors disabled:opacity-50 disabled:cursor-not-allowed", children: t("pages.focus.confirmEdit", { defaultValue: "确认编辑" }) }), _jsx("button", { type: "button", onClick: () => { setShowEditInput(false); setEditReason(""); setNewArtifactId(""); }, className: "inline-flex items-center border border-line bg-surface text-ink rounded-[3px] px-[14px] py-[6px] text-[12.5px] hover:border-line-2 transition-colors", children: t("pages.focus.cancelAction", { defaultValue: "取消" }) })] })] }))] }));
|
|
304
326
|
}
|
|
305
327
|
function StagnationSignalCard({ signal }) {
|
|
306
328
|
const { t } = useTranslation();
|
|
@@ -54,11 +54,22 @@ function validateApprovalGroup(raw) {
|
|
|
54
54
|
return null;
|
|
55
55
|
candidateDescription = description;
|
|
56
56
|
}
|
|
57
|
+
// PRI-935: fitsPromptBudget is optional. ERR-009 discipline — present but
|
|
58
|
+
// wrong type fails loud; absent stays undefined, which the badge treats as
|
|
59
|
+
// "unknown size → do not promise rotation".
|
|
60
|
+
let fitsPromptBudget;
|
|
61
|
+
if (Object.hasOwn(raw, "fitsPromptBudget")) {
|
|
62
|
+
const { fitsPromptBudget: fits } = raw;
|
|
63
|
+
if (typeof fits !== "boolean")
|
|
64
|
+
return null;
|
|
65
|
+
fitsPromptBudget = fits;
|
|
66
|
+
}
|
|
57
67
|
return {
|
|
58
68
|
principleId,
|
|
59
69
|
principleTitle,
|
|
60
70
|
candidateDescription,
|
|
61
71
|
status,
|
|
72
|
+
fitsPromptBudget,
|
|
62
73
|
records: validRecords,
|
|
63
74
|
};
|
|
64
75
|
}
|