create-principles-disciple 1.145.35 → 1.145.37
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 +2 -2
- package/console/dist/server/index.js +4 -0
- package/console/dist/server/models/ApprovalsConsoleModel.d.ts +22 -2
- package/console/dist/server/models/ApprovalsConsoleModel.js +56 -16
- package/console/dist/server/models/ApprovalsGroupedConsoleModel.d.ts +8 -0
- package/console/dist/server/models/ApprovalsGroupedConsoleModel.js +3 -2
- package/console/dist/server/models/approval-review-material.d.ts +15 -0
- package/console/dist/server/models/approval-review-material.js +32 -0
- package/console/dist/server/routes/approvals.js +32 -7
- package/console/dist/server/utils/response.js +8 -0
- package/console/dist/ui/api.d.ts +8 -2
- package/console/dist/ui/api.js +8 -4
- package/console/dist/ui/i18n/en.json +98 -7
- package/console/dist/ui/i18n/zh-CN.json +98 -7
- package/console/dist/ui/pages/focus/ApprovalSubjectReading.d.ts +24 -0
- package/console/dist/ui/pages/focus/ApprovalSubjectReading.js +133 -0
- package/console/dist/ui/pages/focus/FocusPage.d.ts +5 -26
- package/console/dist/ui/pages/focus/FocusPage.js +166 -459
- package/console/dist/ui/pages/focus/OwnerWorkbench.d.ts +19 -0
- package/console/dist/ui/pages/focus/OwnerWorkbench.js +75 -0
- package/console/dist/ui/pages/focus/RuleCandidateBindingPanel.d.ts +4 -1
- package/console/dist/ui/pages/focus/RuleCandidateBindingPanel.js +7 -3
- package/console/dist/ui/pages/focus/focus-validation.js +31 -0
- package/console/dist/ui/pages/principles/PrincipleDetailPage.js +110 -86
- package/console/dist/ui/pages/principles/PrinciplesPage.js +39 -30
- package/console/dist/ui/utils/approval-review.d.ts +30 -0
- package/console/dist/ui/utils/approval-review.js +50 -0
- package/console/dist/ui/utils/reading-layout.d.ts +1 -0
- package/console/dist/ui/utils/reading-layout.js +1 -1
- package/console/dist/ui/utils/validators.d.ts +4 -0
- package/console/dist/ui/utils/validators.js +10 -2
- package/console/dist/web/assets/app.css +119 -11
- package/console/dist/web/assets/app.js +40 -30
- package/core/dist/runtime-v2/activation/activation-types.d.ts +12 -2
- package/core/dist/runtime-v2/activation/activation-types.d.ts.map +1 -1
- package/core/dist/runtime-v2/activation/activation-types.js.map +1 -1
- package/core/dist/runtime-v2/activation/approval-queue.d.ts +3 -2
- package/core/dist/runtime-v2/activation/approval-queue.d.ts.map +1 -1
- package/core/dist/runtime-v2/activation/approval-queue.js +6 -4
- package/core/dist/runtime-v2/activation/approval-queue.js.map +1 -1
- package/core/dist/runtime-v2/activation/approval-review-guard.d.ts +26 -0
- package/core/dist/runtime-v2/activation/approval-review-guard.d.ts.map +1 -0
- package/core/dist/runtime-v2/activation/approval-review-guard.js +25 -0
- package/core/dist/runtime-v2/activation/approval-review-guard.js.map +1 -0
- package/core/dist/runtime-v2/activation/memory-approval-store.d.ts +6 -2
- package/core/dist/runtime-v2/activation/memory-approval-store.d.ts.map +1 -1
- package/core/dist/runtime-v2/activation/memory-approval-store.js +26 -2
- package/core/dist/runtime-v2/activation/memory-approval-store.js.map +1 -1
- package/core/dist/runtime-v2/activation/sqlite-approval-store.d.ts +6 -2
- package/core/dist/runtime-v2/activation/sqlite-approval-store.d.ts.map +1 -1
- package/core/dist/runtime-v2/activation/sqlite-approval-store.js +59 -2
- package/core/dist/runtime-v2/activation/sqlite-approval-store.js.map +1 -1
- package/core/dist/runtime-v2/diagnostician-association-shadow.d.ts +24 -0
- package/core/dist/runtime-v2/diagnostician-association-shadow.d.ts.map +1 -0
- package/core/dist/runtime-v2/diagnostician-association-shadow.js +84 -0
- package/core/dist/runtime-v2/diagnostician-association-shadow.js.map +1 -0
- package/core/dist/runtime-v2/diagnostician-association-validation.d.ts +18 -0
- package/core/dist/runtime-v2/diagnostician-association-validation.d.ts.map +1 -0
- package/core/dist/runtime-v2/diagnostician-association-validation.js +47 -0
- package/core/dist/runtime-v2/diagnostician-association-validation.js.map +1 -0
- package/core/dist/runtime-v2/diagnostician-output.d.ts +123 -0
- package/core/dist/runtime-v2/diagnostician-output.d.ts.map +1 -1
- package/core/dist/runtime-v2/diagnostician-output.js +41 -0
- package/core/dist/runtime-v2/diagnostician-output.js.map +1 -1
- package/core/dist/runtime-v2/index.d.ts +2 -0
- package/core/dist/runtime-v2/index.d.ts.map +1 -1
- package/core/dist/runtime-v2/index.js +1 -0
- package/core/dist/runtime-v2/index.js.map +1 -1
- package/core/dist/runtime-v2/internalization/diag-router-runner.d.ts +12 -1
- package/core/dist/runtime-v2/internalization/diag-router-runner.d.ts.map +1 -1
- package/core/dist/runtime-v2/internalization/diag-router-runner.js +50 -2
- package/core/dist/runtime-v2/internalization/diag-router-runner.js.map +1 -1
- package/core/dist/runtime-v2/internalization/router-context-evidence.d.ts +45 -0
- package/core/dist/runtime-v2/internalization/router-context-evidence.d.ts.map +1 -0
- package/core/dist/runtime-v2/internalization/router-context-evidence.js +64 -0
- package/core/dist/runtime-v2/internalization/router-context-evidence.js.map +1 -0
- package/core/package.json +1 -1
- package/package.json +1 -1
- package/pd-cli/package.json +1 -1
- package/plugin/dist/bundle.js +395 -395
- package/release-manager/package.json +1 -1
|
@@ -208,6 +208,10 @@ function asyncHandler(fn, timeoutMs = REQUEST_TIMEOUT_MS) {
|
|
|
208
208
|
return (innerReq, innerRes) => {
|
|
209
209
|
const timeoutId = setTimeout(() => {
|
|
210
210
|
if (!innerRes.headersSent) {
|
|
211
|
+
// The route keeps running after this point; record the real cause
|
|
212
|
+
// before any dropped late write, so the log never leads with a
|
|
213
|
+
// transport error (ERR_HTTP_HEADERS_SENT) instead of the timeout.
|
|
214
|
+
console.warn(`[pd-console] Request timed out after ${timeoutMs}ms: ${innerReq.method ?? 'GET'} ${innerReq.url ?? ''}`);
|
|
211
215
|
sendJson(innerRes, 504, { success: false, error: 'Request timeout' });
|
|
212
216
|
innerRes.end();
|
|
213
217
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { type ApprovalReviewMaterial } from './approval-review-material.js';
|
|
2
|
+
import type { ApprovalListFilter, ApprovalListResult, ApprovalDecisionResult, ApprovalRecord, ApprovalStatus, ApprovalReviewGuard } from '@principles/core/runtime-v2';
|
|
2
3
|
import type { ApprovalWithContext, ActivationDecision } from '@principles/core/runtime-v2';
|
|
3
4
|
import { type PromptInjectionTargetHost } from '@principles/host-runtime';
|
|
4
5
|
type UnsupportedChannelResult = {
|
|
@@ -49,6 +50,12 @@ export type ApproveWithActivationResult = {
|
|
|
49
50
|
} | {
|
|
50
51
|
ok: false;
|
|
51
52
|
error: 'not_found';
|
|
53
|
+
} | {
|
|
54
|
+
ok: false;
|
|
55
|
+
error: 'stale_review';
|
|
56
|
+
reason: string;
|
|
57
|
+
nextAction: string;
|
|
58
|
+
status?: never;
|
|
52
59
|
} | {
|
|
53
60
|
ok: false;
|
|
54
61
|
error: 'unsupported_channel';
|
|
@@ -113,6 +120,11 @@ export declare class ApprovalsConsoleModel {
|
|
|
113
120
|
listApprovals(filter?: ApprovalListFilter): Promise<ApprovalListResult>;
|
|
114
121
|
getApprovalDetail(approvalId: string): Promise<(ApprovalWithContext & {
|
|
115
122
|
isMvpProven: boolean;
|
|
123
|
+
writeGuardVersion: 'approval-review-v1';
|
|
124
|
+
review: {
|
|
125
|
+
expectedReview: ApprovalReviewGuard;
|
|
126
|
+
material: ApprovalReviewMaterial;
|
|
127
|
+
};
|
|
116
128
|
}) | null>;
|
|
117
129
|
approve(approvalId: string, decidedBy: string, options?: {
|
|
118
130
|
note?: string;
|
|
@@ -120,6 +132,7 @@ export declare class ApprovalsConsoleModel {
|
|
|
120
132
|
intentReviewed?: boolean;
|
|
121
133
|
reviewedArtifactId?: string;
|
|
122
134
|
retryActivation?: boolean;
|
|
135
|
+
expectedReview?: ApprovalReviewGuard;
|
|
123
136
|
}): Promise<ApproveWithActivationResult>;
|
|
124
137
|
/**
|
|
125
138
|
* PD_PROMPT_CAPACITY_V1 R-B2: run the shared host-runtime deliverability
|
|
@@ -175,6 +188,7 @@ export declare class ApprovalsConsoleModel {
|
|
|
175
188
|
editedBy: string;
|
|
176
189
|
newArtifactId: string;
|
|
177
190
|
editReason: string;
|
|
191
|
+
expectedReview?: ApprovalReviewGuard;
|
|
178
192
|
}): Promise<{
|
|
179
193
|
ok: true;
|
|
180
194
|
record: ApprovalRecord;
|
|
@@ -183,9 +197,15 @@ export declare class ApprovalsConsoleModel {
|
|
|
183
197
|
error: 'not_found' | 'already_decided' | 'artifact_not_found' | 'artifact_not_validated' | 'artifact_lineage_mismatch';
|
|
184
198
|
status?: ApprovalStatus;
|
|
185
199
|
reason?: string;
|
|
200
|
+
} | {
|
|
201
|
+
ok: false;
|
|
202
|
+
error: 'stale_review';
|
|
203
|
+
reason: string;
|
|
204
|
+
nextAction: string;
|
|
205
|
+
status?: never;
|
|
186
206
|
}>;
|
|
187
207
|
private dispatchActivationAfterApproval;
|
|
188
|
-
reject(approvalId: string, decidedBy: string, reason: string): Promise<ChannelGuardedDecisionResult>;
|
|
208
|
+
reject(approvalId: string, decidedBy: string, reason: string, expectedReview?: ApprovalReviewGuard): Promise<ChannelGuardedDecisionResult>;
|
|
189
209
|
/**
|
|
190
210
|
* EP002-R4 follow-up: reopen a terminal approval so the Owner can decide
|
|
191
211
|
* again in the Console. The concrete gap: an APPROVED activation was
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { Value } from '@sinclair/typebox/value';
|
|
2
|
+
import { approvalReviewMaterial } from './approval-review-material.js';
|
|
1
3
|
import * as fs from 'node:fs';
|
|
2
4
|
import * as path from 'node:path';
|
|
3
|
-
import { SqliteConnection, SqliteApprovalQueueStore, SqliteActivationStateStore, SqlitePIArtifactStore, ActivationDispatcher, PromptWriter, DeferArchiveWriter, RuleHostWriter, createProductionGateDeps, ApprovalCompletionService, ApprovalQueue, mapConfidenceToLabel, isArtifactRevisionOf, detectPromptReplacementTarget, buildOwnerRevisionArtifact, PrincipleTreeLedgerAdapter, MVP_CHANNELS, } from '@principles/core/runtime-v2';
|
|
5
|
+
import { SqliteConnection, SqliteApprovalQueueStore, SqliteActivationStateStore, SqlitePIArtifactStore, ActivationDispatcher, PromptWriter, DeferArchiveWriter, RuleHostWriter, createProductionGateDeps, ApprovalCompletionService, ApprovalQueue, mapConfidenceToLabel, isArtifactRevisionOf, detectPromptReplacementTarget, buildOwnerRevisionArtifact, PrincipleTreeLedgerAdapter, MVP_CHANNELS, computeArtifactDigest, } from '@principles/core/runtime-v2';
|
|
4
6
|
import { resolveLedgerPrincipleId } from './principle-id-resolution.js';
|
|
5
7
|
import { createArtifactPrincipleResolutionDeps } from './artifact-principle-resolver.js';
|
|
6
8
|
import { loadPdConfig, computeFlagsFromLoadResult } from '../config/pd-config-store.js';
|
|
@@ -94,17 +96,39 @@ export class ApprovalsConsoleModel {
|
|
|
94
96
|
};
|
|
95
97
|
}
|
|
96
98
|
async getApprovalDetail(approvalId) {
|
|
97
|
-
if (!stateDbExists(this.workspaceDir))
|
|
99
|
+
if (!stateDbExists(this.workspaceDir))
|
|
98
100
|
return null;
|
|
101
|
+
const { queue, connection } = this.createReadContext();
|
|
102
|
+
const db = connection.getDb();
|
|
103
|
+
db.exec('BEGIN');
|
|
104
|
+
try {
|
|
105
|
+
const record = await queue.getById(approvalId);
|
|
106
|
+
if (!record) {
|
|
107
|
+
db.exec('COMMIT');
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
const artifact = await new SqlitePIArtifactStore(connection).getArtifactById(record.artifactId);
|
|
111
|
+
const common = { schemaVersion: 'approval-review-v1', approvalId, artifactId: record.artifactId, channel: record.channel };
|
|
112
|
+
const expectedReview = artifact
|
|
113
|
+
? { ...common, artifactState: 'present', artifactDigest: computeArtifactDigest(artifact) }
|
|
114
|
+
: { ...common, artifactState: 'missing' };
|
|
115
|
+
const material = approvalReviewMaterial(record.artifactId, record.channel, artifact?.contentJson ?? null);
|
|
116
|
+
db.exec('COMMIT');
|
|
117
|
+
return { ...record, confidenceLabel: mapConfidenceToLabel(record.confidence), isMvpProven: MVP_PROVEN_CHANNELS.has(record.channel),
|
|
118
|
+
writeGuardVersion: 'approval-review-v1', review: { expectedReview, material } };
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
try {
|
|
122
|
+
db.exec('ROLLBACK');
|
|
123
|
+
}
|
|
124
|
+
catch { /* Preserve the original read failure. */ }
|
|
125
|
+
if (isMissingTableError(error))
|
|
126
|
+
return null;
|
|
127
|
+
throw error;
|
|
128
|
+
}
|
|
129
|
+
finally {
|
|
130
|
+
connection.close();
|
|
99
131
|
}
|
|
100
|
-
const record = await this.readSafeGetById(approvalId);
|
|
101
|
-
if (!record)
|
|
102
|
-
return null;
|
|
103
|
-
return {
|
|
104
|
-
...record,
|
|
105
|
-
confidenceLabel: mapConfidenceToLabel(record.confidence),
|
|
106
|
-
isMvpProven: MVP_PROVEN_CHANNELS.has(record.channel),
|
|
107
|
-
};
|
|
108
132
|
}
|
|
109
133
|
async approve(approvalId, decidedBy, options) {
|
|
110
134
|
const note = options?.note;
|
|
@@ -112,9 +136,12 @@ export class ApprovalsConsoleModel {
|
|
|
112
136
|
if (!stateDbExists(this.workspaceDir)) {
|
|
113
137
|
return { ok: false, error: 'not_found' };
|
|
114
138
|
}
|
|
115
|
-
const
|
|
139
|
+
const reviewed = options?.expectedReview ? await this.getApprovalDetail(approvalId) : null;
|
|
140
|
+
const existing = options?.expectedReview ? reviewed : await this.readSafeGetById(approvalId);
|
|
116
141
|
if (!existing)
|
|
117
142
|
return { ok: false, error: 'not_found' };
|
|
143
|
+
if (options?.expectedReview && !Value.Equal(reviewed?.review.expectedReview, options?.expectedReview))
|
|
144
|
+
return { ok: false, error: 'stale_review', reason: 'The reviewed approval material changed.', nextAction: 'Reload this approval before deciding again.' };
|
|
118
145
|
if (!MVP_PROVEN_CHANNELS.has(existing.channel)) {
|
|
119
146
|
return { ok: false, error: 'unsupported_channel', channel: existing.channel };
|
|
120
147
|
}
|
|
@@ -169,7 +196,7 @@ export class ApprovalsConsoleModel {
|
|
|
169
196
|
try {
|
|
170
197
|
approvalResult = existing.channel === 'prompt' && existing.status === 'approved' && options?.retryActivation === true
|
|
171
198
|
? { ok: true, record: existing }
|
|
172
|
-
: await writeQueue.approve(approvalId, decidedBy, note);
|
|
199
|
+
: await writeQueue.approve(approvalId, decidedBy, note, options?.expectedReview);
|
|
173
200
|
}
|
|
174
201
|
finally {
|
|
175
202
|
try {
|
|
@@ -178,6 +205,8 @@ export class ApprovalsConsoleModel {
|
|
|
178
205
|
catch { /* best-effort */ }
|
|
179
206
|
}
|
|
180
207
|
if (!approvalResult.ok) {
|
|
208
|
+
if (approvalResult.error === 'stale_review')
|
|
209
|
+
return approvalResult;
|
|
181
210
|
if (approvalResult.error === 'already_decided') {
|
|
182
211
|
return { ok: false, error: 'already_decided', status: approvalResult.status };
|
|
183
212
|
}
|
|
@@ -508,9 +537,12 @@ export class ApprovalsConsoleModel {
|
|
|
508
537
|
if (!stateDbExists(this.workspaceDir)) {
|
|
509
538
|
return { ok: false, error: 'not_found' };
|
|
510
539
|
}
|
|
511
|
-
const
|
|
540
|
+
const reviewed = input.expectedReview ? await this.getApprovalDetail(approvalId) : null;
|
|
541
|
+
const existing = input.expectedReview ? reviewed : await this.readSafeGetById(approvalId);
|
|
512
542
|
if (!existing)
|
|
513
543
|
return { ok: false, error: 'not_found' };
|
|
544
|
+
if (input.expectedReview && !Value.Equal(reviewed?.review.expectedReview, input.expectedReview))
|
|
545
|
+
return { ok: false, error: 'stale_review', reason: 'The reviewed approval material changed.', nextAction: 'Reload this approval before deciding again.' };
|
|
514
546
|
if (existing.status !== 'pending') {
|
|
515
547
|
return { ok: false, error: 'already_decided', status: existing.status };
|
|
516
548
|
}
|
|
@@ -543,6 +575,8 @@ export class ApprovalsConsoleModel {
|
|
|
543
575
|
};
|
|
544
576
|
}
|
|
545
577
|
const editResult = await queue.edit({
|
|
578
|
+
expectedNewArtifactDigest: input.expectedReview ? computeArtifactDigest(newArtifact) : undefined,
|
|
579
|
+
expectedReview: input.expectedReview,
|
|
546
580
|
approvalId,
|
|
547
581
|
editedBy,
|
|
548
582
|
newArtifactId,
|
|
@@ -550,6 +584,8 @@ export class ApprovalsConsoleModel {
|
|
|
550
584
|
now: new Date().toISOString(),
|
|
551
585
|
});
|
|
552
586
|
if (!editResult.ok) {
|
|
587
|
+
if (editResult.error === 'stale_review')
|
|
588
|
+
return editResult;
|
|
553
589
|
if (editResult.error === 'already_decided') {
|
|
554
590
|
return { ok: false, error: 'already_decided', status: editResult.status };
|
|
555
591
|
}
|
|
@@ -702,19 +738,23 @@ export class ApprovalsConsoleModel {
|
|
|
702
738
|
catch { /* best-effort */ }
|
|
703
739
|
}
|
|
704
740
|
}
|
|
705
|
-
|
|
741
|
+
// eslint-disable-next-line @typescript-eslint/max-params -- preserve the existing model contract with an additive optional review guard
|
|
742
|
+
async reject(approvalId, decidedBy, reason, expectedReview) {
|
|
706
743
|
if (!stateDbExists(this.workspaceDir)) {
|
|
707
744
|
return { ok: false, error: 'not_found' };
|
|
708
745
|
}
|
|
709
|
-
const
|
|
746
|
+
const reviewed = expectedReview ? await this.getApprovalDetail(approvalId) : null;
|
|
747
|
+
const existing = expectedReview ? reviewed : await this.readSafeGetById(approvalId);
|
|
710
748
|
if (!existing)
|
|
711
749
|
return { ok: false, error: 'not_found' };
|
|
750
|
+
if (expectedReview && !Value.Equal(reviewed?.review.expectedReview, expectedReview))
|
|
751
|
+
return { ok: false, error: 'stale_review', reason: 'The reviewed approval material changed.', nextAction: 'Reload this approval before deciding again.' };
|
|
712
752
|
if (!MVP_PROVEN_CHANNELS.has(existing.channel)) {
|
|
713
753
|
return { ok: false, error: 'unsupported_channel', channel: existing.channel };
|
|
714
754
|
}
|
|
715
755
|
const { queue, connection } = this.createWriteContext();
|
|
716
756
|
try {
|
|
717
|
-
return await queue.reject(approvalId, decidedBy, reason);
|
|
757
|
+
return await queue.reject(approvalId, decidedBy, reason, expectedReview);
|
|
718
758
|
}
|
|
719
759
|
finally {
|
|
720
760
|
try {
|
|
@@ -30,6 +30,8 @@ export interface ApprovalGroup {
|
|
|
30
30
|
promptBudgetTierFit?: 'fits_all_tiers' | 'model_dependent' | 'exceeds_max_tier' | 'unconfirmed';
|
|
31
31
|
revisionReviewUnavailable?: boolean;
|
|
32
32
|
revisionReview?: {
|
|
33
|
+
artifactId: string;
|
|
34
|
+
artifactDigest: string;
|
|
33
35
|
oldStatement: string;
|
|
34
36
|
newStatement: string;
|
|
35
37
|
intentFields: Record<string, string>;
|
|
@@ -48,6 +50,12 @@ export interface ApprovalGroup {
|
|
|
48
50
|
artifactId: string;
|
|
49
51
|
channel: string;
|
|
50
52
|
createdAt: string;
|
|
53
|
+
/**
|
|
54
|
+
* This record's own revision review is incomplete (the group flag also
|
|
55
|
+
* covers the multi-pending ambiguity, so readers must not use it to block
|
|
56
|
+
* an intact sibling approval).
|
|
57
|
+
*/
|
|
58
|
+
revisionReviewUnavailable?: boolean;
|
|
51
59
|
}[];
|
|
52
60
|
}
|
|
53
61
|
export interface ApprovalsGroupedResponse {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { SqliteConnection, SqliteApprovalQueueStore, SqlitePIArtifactStore, ApprovalQueue, RUNTIME_V2_PRINCIPLE_BUDGET, detectPromptReplacementTarget, SqliteActivationStateStore, } from '@principles/core/runtime-v2';
|
|
1
|
+
import { SqliteConnection, SqliteApprovalQueueStore, SqlitePIArtifactStore, ApprovalQueue, RUNTIME_V2_PRINCIPLE_BUDGET, detectPromptReplacementTarget, computeArtifactDigest, SqliteActivationStateStore, } from '@principles/core/runtime-v2';
|
|
2
2
|
import { checkPromptArtifactDeliverability, operatorDeclaredBudgetResolution, resolveLivePromptInjectionProjection, RUNTIME_BUDGET_OBSERVATION_STALE_REASON, runtimeBudgetObservationToResolution, selectConsoleRuntimeBudgetFacts, } from '@principles/host-runtime';
|
|
3
3
|
import { PROMPT_BUDGET_HOST_KINDS } from '@principles/core/runtime-v2';
|
|
4
4
|
import { loadLedger } from '@principles/core/principle-tree-ledger';
|
|
@@ -244,7 +244,7 @@ export class ApprovalsGroupedConsoleModel {
|
|
|
244
244
|
intentFields[key] = value;
|
|
245
245
|
}
|
|
246
246
|
}
|
|
247
|
-
revisionReviews.set(artifact.artifactId, { oldStatement, newStatement, intentFields });
|
|
247
|
+
revisionReviews.set(artifact.artifactId, { artifactId: artifact.artifactId, artifactDigest: computeArtifactDigest(artifact), oldStatement, newStatement, intentFields });
|
|
248
248
|
break;
|
|
249
249
|
}
|
|
250
250
|
}
|
|
@@ -297,6 +297,7 @@ export class ApprovalsGroupedConsoleModel {
|
|
|
297
297
|
status: approval.status === 'approved' || approval.status === 'rejected'
|
|
298
298
|
? approval.status
|
|
299
299
|
: 'pending',
|
|
300
|
+
...(revisionReviewUnavailable.has(approval.artifactId) ? { revisionReviewUnavailable: true } : {}),
|
|
300
301
|
});
|
|
301
302
|
}
|
|
302
303
|
const groups = [];
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface ApprovalReviewMaterial {
|
|
2
|
+
availability: 'available' | 'missing' | 'unreadable';
|
|
3
|
+
artifactId: string;
|
|
4
|
+
statement: string | null;
|
|
5
|
+
rationale: string | null;
|
|
6
|
+
scope: string[];
|
|
7
|
+
risks: string[] | null;
|
|
8
|
+
targetBehavior: string | null;
|
|
9
|
+
exactBody: {
|
|
10
|
+
kind: 'principle' | 'rule_code';
|
|
11
|
+
text: string;
|
|
12
|
+
} | null;
|
|
13
|
+
}
|
|
14
|
+
/** Whitelisted normative fields only; raw artifact JSON is never returned. */
|
|
15
|
+
export declare function approvalReviewMaterial(artifactId: string, channel: string, contentJson: string | null): ApprovalReviewMaterial;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
function record(value) { return value !== null && typeof value === 'object' && !Array.isArray(value); }
|
|
2
|
+
function text(value) { return typeof value === 'string' && value.trim() !== '' ? value : null; }
|
|
3
|
+
/** Whitelisted normative fields only; raw artifact JSON is never returned. */
|
|
4
|
+
export function approvalReviewMaterial(artifactId, channel, contentJson) {
|
|
5
|
+
const result = { artifactId, availability: contentJson === null ? 'missing' : 'unreadable', statement: null, rationale: null, scope: [], risks: null, targetBehavior: null, exactBody: null };
|
|
6
|
+
if (contentJson === null)
|
|
7
|
+
return result;
|
|
8
|
+
let parsed;
|
|
9
|
+
try {
|
|
10
|
+
parsed = JSON.parse(contentJson);
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return result;
|
|
14
|
+
}
|
|
15
|
+
if (!record(parsed))
|
|
16
|
+
return result;
|
|
17
|
+
if (record(parsed.intentContract))
|
|
18
|
+
result.targetBehavior = text(parsed.intentContract.targetBehavior);
|
|
19
|
+
if (Array.isArray(parsed.risks) && parsed.risks.every(item => typeof item === 'string'))
|
|
20
|
+
result.risks = parsed.risks;
|
|
21
|
+
const draft = record(parsed.principleDraft) ? parsed.principleDraft : parsed;
|
|
22
|
+
result.statement = text(draft.statement);
|
|
23
|
+
result.rationale = text(draft.rationale);
|
|
24
|
+
if (Array.isArray(draft.applicability) && draft.applicability.every(item => typeof item === 'string'))
|
|
25
|
+
result.scope = draft.applicability;
|
|
26
|
+
const body = channel === 'code_tool_hook' ? text(parsed.implementationCode) : result.statement;
|
|
27
|
+
if (body !== null) {
|
|
28
|
+
result.exactBody = { kind: channel === 'code_tool_hook' ? 'rule_code' : 'principle', text: body };
|
|
29
|
+
result.availability = 'available';
|
|
30
|
+
}
|
|
31
|
+
return result;
|
|
32
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { MVP_CHANNELS } from '@principles/core/runtime-v2';
|
|
1
|
+
import { isApprovalReviewGuard, MVP_CHANNELS } from '@principles/core/runtime-v2';
|
|
2
2
|
import { ApprovalsConsoleModel } from '../models/ApprovalsConsoleModel.js';
|
|
3
3
|
import { sendSuccess, sendError, sendNotFound, sendBadRequest } from '../utils/response.js';
|
|
4
4
|
import { parseQuery, readBody } from '../utils/request.js';
|
|
@@ -124,6 +124,11 @@ export async function handleApprovalsRoute(req, res, workspaceDir, subPath) {
|
|
|
124
124
|
const parsed = parseJsonBody(body, res);
|
|
125
125
|
if (!parsed)
|
|
126
126
|
return;
|
|
127
|
+
const expectedReview = Object.hasOwn(parsed, 'expectedReview') ? parsed.expectedReview : undefined;
|
|
128
|
+
if (Object.hasOwn(parsed, 'expectedReview') && (!isApprovalReviewGuard(expectedReview) || expectedReview.approvalId !== approvalId || expectedReview.artifactState !== 'present')) {
|
|
129
|
+
sendBadRequest(res, 'expectedReview must identify this approval and its exact reviewed artifact/version');
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
127
132
|
if (Object.hasOwn(parsed, 'note') && typeof parsed.note !== 'string') {
|
|
128
133
|
sendBadRequest(res, 'note must be a string');
|
|
129
134
|
return;
|
|
@@ -158,9 +163,13 @@ export async function handleApprovalsRoute(req, res, workspaceDir, subPath) {
|
|
|
158
163
|
intentReviewed: parsed.intentReviewed === true,
|
|
159
164
|
...(typeof parsed.reviewedArtifactId === 'string' ? { reviewedArtifactId: parsed.reviewedArtifactId } : {}),
|
|
160
165
|
retryActivation: parsed.retryActivation === true,
|
|
166
|
+
expectedReview: isApprovalReviewGuard(expectedReview) ? expectedReview : undefined,
|
|
161
167
|
});
|
|
162
168
|
if (!result.ok) {
|
|
163
|
-
if (result.error === '
|
|
169
|
+
if (result.error === 'stale_review') {
|
|
170
|
+
sendError(res, 409, 'stale_review', result.reason, { reason: 'stale_review', nextAction: result.nextAction });
|
|
171
|
+
}
|
|
172
|
+
else if (result.error === 'not_found') {
|
|
164
173
|
sendNotFound(res, 'Approval ' + approvalId + ' not found');
|
|
165
174
|
}
|
|
166
175
|
else if (result.error === 'unsupported_channel') {
|
|
@@ -170,7 +179,7 @@ export async function handleApprovalsRoute(req, res, workspaceDir, subPath) {
|
|
|
170
179
|
sendError(res, 409, result.error, result.reason, { nextAction: result.nextAction });
|
|
171
180
|
}
|
|
172
181
|
else if (result.error === 'activation_failed') {
|
|
173
|
-
sendError(res, 500, 'activation_failed', `Approval was ${result.approvalRolledBack ? 'rolled back to pending' : 'approved but activation failed'}. Reason: ${result.reason}`, { nextAction: result.nextAction ?? 'Inspect or regenerate the rule artifact, verify it is validated, then retry approval.' });
|
|
182
|
+
sendError(res, 500, 'activation_failed', `Approval was ${result.approvalRolledBack ? 'rolled back to pending' : 'approved but activation failed'}. Reason: ${result.reason}`, { reason: 'activation_failed', approvalRolledBack: result.approvalRolledBack, nextAction: result.nextAction ?? 'Inspect or regenerate the rule artifact, verify it is validated, then retry approval.' });
|
|
174
183
|
}
|
|
175
184
|
else if (result.error === 'prompt_capacity_refused') {
|
|
176
185
|
// R-B2: refused BEFORE any governance write — the approval stays
|
|
@@ -224,6 +233,11 @@ export async function handleApprovalsRoute(req, res, workspaceDir, subPath) {
|
|
|
224
233
|
const parsed = parseJsonBody(body, res);
|
|
225
234
|
if (!parsed)
|
|
226
235
|
return;
|
|
236
|
+
const expectedReview = Object.hasOwn(parsed, 'expectedReview') ? parsed.expectedReview : undefined;
|
|
237
|
+
if (Object.hasOwn(parsed, 'expectedReview') && (!isApprovalReviewGuard(expectedReview) || expectedReview.approvalId !== approvalId)) {
|
|
238
|
+
sendBadRequest(res, 'expectedReview must identify this approval and its exact reviewed artifact/version');
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
227
241
|
if (!Object.hasOwn(parsed, 'reason') || typeof parsed.reason !== 'string') {
|
|
228
242
|
sendBadRequest(res, 'reason is required and must be a string');
|
|
229
243
|
return;
|
|
@@ -232,9 +246,12 @@ export async function handleApprovalsRoute(req, res, workspaceDir, subPath) {
|
|
|
232
246
|
sendBadRequest(res, 'reason must not be empty');
|
|
233
247
|
return;
|
|
234
248
|
}
|
|
235
|
-
const result = await model.reject(approvalId, 'operator', parsed.reason);
|
|
249
|
+
const result = await model.reject(approvalId, 'operator', parsed.reason, isApprovalReviewGuard(expectedReview) ? expectedReview : undefined);
|
|
236
250
|
if (!result.ok) {
|
|
237
|
-
if (result.error === '
|
|
251
|
+
if (result.error === 'stale_review') {
|
|
252
|
+
sendError(res, 409, 'stale_review', result.reason, { reason: 'stale_review', nextAction: result.nextAction });
|
|
253
|
+
}
|
|
254
|
+
else if (result.error === 'not_found') {
|
|
238
255
|
sendNotFound(res, 'Approval ' + approvalId + ' not found');
|
|
239
256
|
}
|
|
240
257
|
else if (result.error === 'unsupported_channel') {
|
|
@@ -319,6 +336,11 @@ export async function handleApprovalsRoute(req, res, workspaceDir, subPath) {
|
|
|
319
336
|
const parsed = parseJsonBody(body, res);
|
|
320
337
|
if (!parsed)
|
|
321
338
|
return;
|
|
339
|
+
const expectedReview = Object.hasOwn(parsed, 'expectedReview') ? parsed.expectedReview : undefined;
|
|
340
|
+
if (Object.hasOwn(parsed, 'expectedReview') && (!isApprovalReviewGuard(expectedReview) || expectedReview.approvalId !== approvalId || expectedReview.artifactState !== 'present')) {
|
|
341
|
+
sendBadRequest(res, 'expectedReview must identify this approval and its exact reviewed artifact/version');
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
322
344
|
if (!Object.hasOwn(parsed, 'newArtifactId') || typeof parsed.newArtifactId !== 'string') {
|
|
323
345
|
sendBadRequest(res, 'newArtifactId is required and must be a string');
|
|
324
346
|
return;
|
|
@@ -331,9 +353,12 @@ export async function handleApprovalsRoute(req, res, workspaceDir, subPath) {
|
|
|
331
353
|
sendBadRequest(res, 'editReason must not be empty');
|
|
332
354
|
return;
|
|
333
355
|
}
|
|
334
|
-
const result = await model.editApproval({ approvalId, editedBy: 'operator', newArtifactId: parsed.newArtifactId, editReason: parsed.editReason });
|
|
356
|
+
const result = await model.editApproval({ expectedReview: isApprovalReviewGuard(expectedReview) ? expectedReview : undefined, approvalId, editedBy: 'operator', newArtifactId: parsed.newArtifactId, editReason: parsed.editReason });
|
|
335
357
|
if (!result.ok) {
|
|
336
|
-
if (result.error === '
|
|
358
|
+
if (result.error === 'stale_review') {
|
|
359
|
+
sendError(res, 409, 'stale_review', result.reason, { reason: 'stale_review', nextAction: result.nextAction });
|
|
360
|
+
}
|
|
361
|
+
else if (result.error === 'not_found') {
|
|
337
362
|
sendNotFound(res, 'Approval ' + approvalId + ' not found');
|
|
338
363
|
}
|
|
339
364
|
else if (result.error === 'already_decided') {
|
|
@@ -1,4 +1,12 @@
|
|
|
1
1
|
export function sendJson(res, statusCode, payload) {
|
|
2
|
+
// A response can already be committed by an earlier writer — most often the
|
|
3
|
+
// request-timeout path in server/index.ts, which answers 504 while the route
|
|
4
|
+
// is still running. Writing again throws ERR_HTTP_HEADERS_SENT, which hides
|
|
5
|
+
// the first (real) failure behind a transport error. Drop the late write.
|
|
6
|
+
if (res.headersSent || res.writableEnded) {
|
|
7
|
+
console.warn(`[pd-console] Dropped late response write (status ${statusCode}): headers already sent`);
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
2
10
|
const body = JSON.stringify(payload);
|
|
3
11
|
res.writeHead(statusCode, {
|
|
4
12
|
'Content-Type': 'application/json; charset=utf-8',
|
package/console/dist/ui/api.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type ApprovalReviewGuard, type ApprovalReviewData } from './utils/approval-review.js';
|
|
1
2
|
import type { ApiResponse } from "../types.js";
|
|
2
3
|
import type { OwnerDecisionsData, OwnerResolutionResultData, OwnerDecisionInboxData, OwnerDecisionLibraryData } from "./utils/validators.js";
|
|
3
4
|
import type { DesignGateAction, DesignGateDecision, DesignGateDeclaration, DesignGateScope } from "@principles/core/runtime-v2/internalization/design-gate-facts";
|
|
@@ -210,13 +211,18 @@ declare function fetchEvidenceAudit(selectorType: "principle" | "activation" | "
|
|
|
210
211
|
evidenceId: string;
|
|
211
212
|
};
|
|
212
213
|
}): Promise<ApiResponse<EvidenceAuditData>>;
|
|
214
|
+
export declare function fetchApprovalReview(approvalId: string): Promise<ApiResponse<ApprovalReviewData>>;
|
|
213
215
|
declare function approveApproval(approvalId: string, note?: string, options?: {
|
|
216
|
+
expectedReview?: ApprovalReviewGuard;
|
|
214
217
|
targetHost?: 'openclaw' | 'codex' | 'workbuddy';
|
|
215
218
|
intentReviewed?: boolean;
|
|
216
219
|
reviewedArtifactId?: string;
|
|
217
220
|
}): Promise<ApiResponse<ApprovalRecordData>>;
|
|
218
|
-
declare function rejectApproval(approvalId: string, reason: string): Promise<ApiResponse<ApprovalRecordData>>;
|
|
219
|
-
declare function editApproval(approvalId: string, newArtifactId: string, editReason: string
|
|
221
|
+
declare function rejectApproval(approvalId: string, reason: string, expectedReview?: ApprovalReviewGuard): Promise<ApiResponse<ApprovalRecordData>>;
|
|
222
|
+
declare function editApproval(approvalId: string, newArtifactId: string, editReason: string | {
|
|
223
|
+
reason: string;
|
|
224
|
+
expectedReview: ApprovalReviewGuard;
|
|
225
|
+
}): Promise<ApiResponse<ApprovalRecordData>>;
|
|
220
226
|
declare function createFeedbackReport(input: unknown, diagnostics: unknown): Promise<ApiResponse<FeedbackReportData>>;
|
|
221
227
|
declare function listFeedbackReports(): Promise<ApiResponse<{
|
|
222
228
|
drafts: FeedbackDraftSummaryData[];
|
package/console/dist/ui/api.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { validateApprovalReview } from './utils/approval-review.js';
|
|
1
2
|
import { clearStoredToken, loadStoredToken, storeToken } from "./utils/token-storage.js";
|
|
2
3
|
import { validateBindingReview, validateBindingQueue, validateBindingMutation, validateBindingRevise } from './utils/rule-binding-validators.js';
|
|
3
4
|
import { validateErrorResponse, validateHeaders, validateFeedbackReport, validateFeedbackDraftsList, validateFeedbackDraftEnvelope, validateFeedbackChannels, validateFeedbackSubmitResult, validateDeleteEnvelope, validateWorkspaceEntry, validateWorkspaceList, validateRemovedEnvelope, validateSyncResult, validateConfigSummary, validateConfigCatalog, validateAgentBindingUpdate, validateDefaultRuntimeUpdate, validateRuntimeProfileMutation, validateReadinessCheck, validateFeatureFlagUpdate, validateOutputLanguage, validateGovernanceQueue, validateRecoveryResult, validateFailedTaskDetail, validateOwnerDecisionsData, validateOwnerResolutionResult, validateActivations, validateDisableActivation, validateLifecycleMetrics, validateUpdateStatus, validateUpdateHistory, validateApplyUpdateResult, validateUpdateRecovery, validateUpdateTransaction, validateApprovalRecordDirect, validatePrinciplesList, validateCorePrinciples, validateApprovalsGrouped, validateEvidenceChain, validateTrajectoryData, validateIntentSummary, validateIntentDecisionList, validateIntentDecisionResult, validateIntentDecisionSummary, validateFollowUpResponse, validateIntentInitResult, validateIntentSaveResult, validateIntentRawContent, validateIntentVersions, validateOwnerGovernanceView, validateOwnerDecisionViewCore, validateOwnerDecisionInbox, validateOwnerDecisionLibrary, validateGovernanceExperienceSnapshot, validateRuleCodeOwnerReview, validateRuleCodeMutation, validateOwnerIdentityView, validateOwnerIdentityRegister, validateOwnerIdentityUnregister, } from "./utils/validators.js";
|
|
@@ -556,23 +557,26 @@ async function fetchEvidenceAudit(selectorType, id, cursor) {
|
|
|
556
557
|
}
|
|
557
558
|
return request(`/api/v1/receipts/evidence-audit?${query.toString()}`, undefined, validateEvidenceAudit);
|
|
558
559
|
}
|
|
560
|
+
export function fetchApprovalReview(approvalId) {
|
|
561
|
+
return request('/api/v1/approvals/' + encodeURIComponent(approvalId), undefined, validateApprovalReview);
|
|
562
|
+
}
|
|
559
563
|
// ── Approvals ─────────────────────────────────────────────────────────────────
|
|
560
564
|
async function approveApproval(approvalId, note, options) {
|
|
561
565
|
return request('/api/v1/approvals/' + encodeURIComponent(approvalId) + '/approve', {
|
|
562
566
|
method: 'POST',
|
|
563
|
-
body: JSON.stringify({ note, ...(options?.targetHost !== undefined ? { host: options.targetHost } : {}), ...(options?.intentReviewed !== undefined ? { intentReviewed: options.intentReviewed, reviewedArtifactId: options.reviewedArtifactId } : {}) }),
|
|
567
|
+
body: JSON.stringify({ note, ...(options?.expectedReview === undefined ? {} : { expectedReview: options.expectedReview }), ...(options?.targetHost !== undefined ? { host: options.targetHost } : {}), ...(options?.intentReviewed !== undefined ? { intentReviewed: options.intentReviewed, reviewedArtifactId: options.reviewedArtifactId } : {}) }),
|
|
564
568
|
}, validateApprovalRecordDirect);
|
|
565
569
|
}
|
|
566
|
-
async function rejectApproval(approvalId, reason) {
|
|
570
|
+
async function rejectApproval(approvalId, reason, expectedReview) {
|
|
567
571
|
return request('/api/v1/approvals/' + encodeURIComponent(approvalId) + '/reject', {
|
|
568
572
|
method: 'POST',
|
|
569
|
-
body: JSON.stringify({ reason }),
|
|
573
|
+
body: JSON.stringify({ reason, ...(expectedReview === undefined ? {} : { expectedReview }) }),
|
|
570
574
|
}, validateApprovalRecordDirect);
|
|
571
575
|
}
|
|
572
576
|
async function editApproval(approvalId, newArtifactId, editReason) {
|
|
573
577
|
return request('/api/v1/approvals/' + encodeURIComponent(approvalId) + '/edit', {
|
|
574
578
|
method: 'POST',
|
|
575
|
-
body: JSON.stringify({ newArtifactId, editReason }),
|
|
579
|
+
body: JSON.stringify({ newArtifactId, editReason: typeof editReason === 'string' ? editReason : editReason.reason, ...(typeof editReason === 'string' ? {} : { expectedReview: editReason.expectedReview }) }),
|
|
576
580
|
}, validateApprovalRecordDirect);
|
|
577
581
|
}
|
|
578
582
|
// ── MVP seed feedback report drafts (PRI-285) ────────────────────────────────
|