create-principles-disciple 1.145.35 → 1.145.36

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.
Files changed (79) hide show
  1. package/_release/product-identity.json +2 -2
  2. package/console/dist/server/models/ApprovalsConsoleModel.d.ts +22 -2
  3. package/console/dist/server/models/ApprovalsConsoleModel.js +56 -16
  4. package/console/dist/server/models/ApprovalsGroupedConsoleModel.d.ts +8 -0
  5. package/console/dist/server/models/ApprovalsGroupedConsoleModel.js +3 -2
  6. package/console/dist/server/models/approval-review-material.d.ts +15 -0
  7. package/console/dist/server/models/approval-review-material.js +32 -0
  8. package/console/dist/server/routes/approvals.js +32 -7
  9. package/console/dist/ui/api.d.ts +8 -2
  10. package/console/dist/ui/api.js +8 -4
  11. package/console/dist/ui/i18n/en.json +98 -7
  12. package/console/dist/ui/i18n/zh-CN.json +98 -7
  13. package/console/dist/ui/pages/focus/ApprovalSubjectReading.d.ts +24 -0
  14. package/console/dist/ui/pages/focus/ApprovalSubjectReading.js +133 -0
  15. package/console/dist/ui/pages/focus/FocusPage.d.ts +5 -26
  16. package/console/dist/ui/pages/focus/FocusPage.js +166 -459
  17. package/console/dist/ui/pages/focus/OwnerWorkbench.d.ts +19 -0
  18. package/console/dist/ui/pages/focus/OwnerWorkbench.js +75 -0
  19. package/console/dist/ui/pages/focus/RuleCandidateBindingPanel.d.ts +4 -1
  20. package/console/dist/ui/pages/focus/RuleCandidateBindingPanel.js +7 -3
  21. package/console/dist/ui/pages/focus/focus-validation.js +31 -0
  22. package/console/dist/ui/pages/principles/PrincipleDetailPage.js +110 -86
  23. package/console/dist/ui/pages/principles/PrinciplesPage.js +39 -30
  24. package/console/dist/ui/utils/approval-review.d.ts +30 -0
  25. package/console/dist/ui/utils/approval-review.js +50 -0
  26. package/console/dist/ui/utils/reading-layout.d.ts +1 -0
  27. package/console/dist/ui/utils/reading-layout.js +1 -1
  28. package/console/dist/ui/utils/validators.d.ts +4 -0
  29. package/console/dist/ui/utils/validators.js +10 -2
  30. package/console/dist/web/assets/app.css +119 -11
  31. package/console/dist/web/assets/app.js +40 -30
  32. package/core/dist/runtime-v2/activation/activation-types.d.ts +12 -2
  33. package/core/dist/runtime-v2/activation/activation-types.d.ts.map +1 -1
  34. package/core/dist/runtime-v2/activation/activation-types.js.map +1 -1
  35. package/core/dist/runtime-v2/activation/approval-queue.d.ts +3 -2
  36. package/core/dist/runtime-v2/activation/approval-queue.d.ts.map +1 -1
  37. package/core/dist/runtime-v2/activation/approval-queue.js +6 -4
  38. package/core/dist/runtime-v2/activation/approval-queue.js.map +1 -1
  39. package/core/dist/runtime-v2/activation/approval-review-guard.d.ts +26 -0
  40. package/core/dist/runtime-v2/activation/approval-review-guard.d.ts.map +1 -0
  41. package/core/dist/runtime-v2/activation/approval-review-guard.js +25 -0
  42. package/core/dist/runtime-v2/activation/approval-review-guard.js.map +1 -0
  43. package/core/dist/runtime-v2/activation/memory-approval-store.d.ts +6 -2
  44. package/core/dist/runtime-v2/activation/memory-approval-store.d.ts.map +1 -1
  45. package/core/dist/runtime-v2/activation/memory-approval-store.js +26 -2
  46. package/core/dist/runtime-v2/activation/memory-approval-store.js.map +1 -1
  47. package/core/dist/runtime-v2/activation/sqlite-approval-store.d.ts +6 -2
  48. package/core/dist/runtime-v2/activation/sqlite-approval-store.d.ts.map +1 -1
  49. package/core/dist/runtime-v2/activation/sqlite-approval-store.js +59 -2
  50. package/core/dist/runtime-v2/activation/sqlite-approval-store.js.map +1 -1
  51. package/core/dist/runtime-v2/diagnostician-association-shadow.d.ts +24 -0
  52. package/core/dist/runtime-v2/diagnostician-association-shadow.d.ts.map +1 -0
  53. package/core/dist/runtime-v2/diagnostician-association-shadow.js +84 -0
  54. package/core/dist/runtime-v2/diagnostician-association-shadow.js.map +1 -0
  55. package/core/dist/runtime-v2/diagnostician-association-validation.d.ts +18 -0
  56. package/core/dist/runtime-v2/diagnostician-association-validation.d.ts.map +1 -0
  57. package/core/dist/runtime-v2/diagnostician-association-validation.js +47 -0
  58. package/core/dist/runtime-v2/diagnostician-association-validation.js.map +1 -0
  59. package/core/dist/runtime-v2/diagnostician-output.d.ts +123 -0
  60. package/core/dist/runtime-v2/diagnostician-output.d.ts.map +1 -1
  61. package/core/dist/runtime-v2/diagnostician-output.js +41 -0
  62. package/core/dist/runtime-v2/diagnostician-output.js.map +1 -1
  63. package/core/dist/runtime-v2/index.d.ts +2 -0
  64. package/core/dist/runtime-v2/index.d.ts.map +1 -1
  65. package/core/dist/runtime-v2/index.js +1 -0
  66. package/core/dist/runtime-v2/index.js.map +1 -1
  67. package/core/dist/runtime-v2/internalization/diag-router-runner.d.ts +12 -1
  68. package/core/dist/runtime-v2/internalization/diag-router-runner.d.ts.map +1 -1
  69. package/core/dist/runtime-v2/internalization/diag-router-runner.js +50 -2
  70. package/core/dist/runtime-v2/internalization/diag-router-runner.js.map +1 -1
  71. package/core/dist/runtime-v2/internalization/router-context-evidence.d.ts +45 -0
  72. package/core/dist/runtime-v2/internalization/router-context-evidence.d.ts.map +1 -0
  73. package/core/dist/runtime-v2/internalization/router-context-evidence.js +64 -0
  74. package/core/dist/runtime-v2/internalization/router-context-evidence.js.map +1 -0
  75. package/core/package.json +1 -1
  76. package/package.json +1 -1
  77. package/pd-cli/package.json +1 -1
  78. package/plugin/dist/bundle.js +395 -395
  79. package/release-manager/package.json +1 -1
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "productVersion": "2.4.0",
4
- "sourceCommit": "6d080625ac692f02ad2dce386d3f61a769f16214"
3
+ "productVersion": "2.5.0",
4
+ "sourceCommit": "66ad1c4d4af4653b5483e53af676cc84c90001d3"
5
5
  }
@@ -1,4 +1,5 @@
1
- import type { ApprovalListFilter, ApprovalListResult, ApprovalDecisionResult, ApprovalRecord, ApprovalStatus } from '@principles/core/runtime-v2';
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 existing = await this.readSafeGetById(approvalId);
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 existing = await this.readSafeGetById(approvalId);
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
- async reject(approvalId, decidedBy, reason) {
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 existing = await this.readSafeGetById(approvalId);
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 === 'not_found') {
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 === 'not_found') {
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 === 'not_found') {
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,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): Promise<ApiResponse<ApprovalRecordData>>;
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[];
@@ -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) ────────────────────────────────
@@ -48,6 +48,97 @@
48
48
  },
49
49
  "pages": {
50
50
  "focus": {
51
+ "workbench": {
52
+ "textPreview": "Text preview",
53
+ "codePreview": "Rule implementation preview",
54
+ "fullText": "Expand complete text",
55
+ "fullCode": "Expand complete rule implementation",
56
+ "scope": "Applicable scope",
57
+ "compare": "Compare the previous version and retained intent",
58
+ "intentReviewed": "I checked the old and new versions and the intent to retain",
59
+ "promptConsequence": "Approval enters the existing prompt activation flow. Delivery still depends on host and capacity.",
60
+ "ruleConsequence": "Accepting an execution proposal is not promotion to live. Observation and promotion remain separate decisions.",
61
+ "approveOne": "Approve this revision",
62
+ "rejectOne": "Reject this revision",
63
+ "replaceOne": "Replace pending revision",
64
+ "replacementId": "Validated replacement artifact identifier",
65
+ "reason": "Reason for this decision",
66
+ "confirm": "Confirm submission",
67
+ "cancel": "Cancel",
68
+ "resultUnknown": "The submission result is unconfirmed. Read the current state before deciding again; do not resubmit.",
69
+ "notRecorded": "This decision is not complete. Review the refreshed material.",
70
+ "approved": "Approval is complete. Check activation and delivery in runtime status.",
71
+ "rejected": "This revision was rejected. Other subjects were not processed.",
72
+ "activationFailed": "Approval is complete, but activation failed. Use runtime recovery; do not approve again.",
73
+ "approvedRuntimeUnknown": "Approval was recorded. Activation is incomplete or unconfirmed. Check runtime status.",
74
+ "replaced": "The pending revision was replaced. Review the current text before deciding.",
75
+ "versionNeedsReview": "Revision {{index}} to review",
76
+ "title": "Needs your attention",
77
+ "subtitle": "Choose one item, understand the proposal and its consequences, then continue.",
78
+ "list": "Work items",
79
+ "count": "{{count}} items",
80
+ "uncertain": "Some sources could not be read. This list may be incomplete.",
81
+ "empty": "No work items are currently confirmed.",
82
+ "back": "Back to work items",
83
+ "next": "View next item",
84
+ "readingEnd": "This is the end of this item. Return to the list or review the next one.",
85
+ "completed": "Review outcome",
86
+ "notCurrent": "This item is no longer in the current list",
87
+ "startTitle": "Start with one item",
88
+ "startBody": "Choose an item to read its material, check the concerns, and see the available actions in one place.",
89
+ "notCurrentBody": "It may have been resolved, or its source may be unavailable. Use the freshly read state.",
90
+ "start": "Start review",
91
+ "loading": "Loading this approval’s exact material…",
92
+ "why": "What this is trying to solve",
93
+ "proposal": "What PD proposes to change",
94
+ "decision": "Your decision",
95
+ "proposedBehavior": "Proposed behavior (this draft)",
96
+ "currentApproved": "currently approved",
97
+ "currentRejected": "currently rejected",
98
+ "refusedCurrentState": "This operation was not performed; {{state}}.",
99
+ "currentStateConfirmed": "The record was reloaded: {{state}}. Whether this submission succeeded remains unconfirmed.",
100
+ "runtimeCheckLink": "Check activation and delivery on the runtime page.",
101
+ "sourceUnlinked": "This approval could not be linked to a principle or a readable name. Check the exact text and source first; you can reject it if the material is insufficient.",
102
+ "risksUnknown": "Risk notes for this revision are missing or unreadable. Risks remain unconfirmed.",
103
+ "risksEmpty": "This revision lists no risks; that does not establish safety.",
104
+ "approvalNote": "Approval note (optional)",
105
+ "budgetBasis": "Prompt capacity source and estimate basis",
106
+ "sourcesPending": "Sources unconfirmed",
107
+ "countExplanation": "How items are counted",
108
+ "archiveReview": "Defer or archive review",
109
+ "archiveConsequence": "Approving defers or archives this proposal; it does not activate a prompt or execution rule.",
110
+ "approveArchive": "Approve defer or archive",
111
+ "beforeDecision": "Before you decide",
112
+ "subjectUnconfirmed": "The complete decision material for this revision is unconfirmed. Check its source first.",
113
+ "revisionChanged": "The pending revision changed. Return to the list and read the new revision and intent comparison before deciding.",
114
+ "scopeUnknown": "Unspecified scope and exceptions remain unknown; they do not mean this applies in every situation.",
115
+ "principleLimits": "Principle-level risks and gaps (not validation of this revision)",
116
+ "reasonAndScope": "Rationale and consequences",
117
+ "materialUnavailable": "The material for this item is unavailable.",
118
+ "exactMaterialMissing": "No text matching this approval and revision was found. Check the material first.",
119
+ "versionLock": "You can read the material. The current approval service does not yet guarantee that the submitted version is the one you reviewed, so approve, reject, and replace actions are unavailable here.",
120
+ "openPrinciple": "View principle details and existing records",
121
+ "subjectDetails": "View approval and revision identifiers",
122
+ "permissionsUnknown": "Approval permissions could not be confirmed. Reading remains available; decision actions are locked.",
123
+ "taskDecision": "Current proposal decision",
124
+ "revisionDecision": "Proposal revision decision",
125
+ "validationReview": "Behavior validation review",
126
+ "approvalMaterialReview": "Approval material check",
127
+ "unknownReview": "Item type needs checking",
128
+ "promptReview": "Prompt principle review",
129
+ "ruleReview": "Execution proposal review",
130
+ "approvalSummary": "Check the proposed behavior, scope, and exact revision.",
131
+ "bindingReview": "Rule attribution review",
132
+ "bindingSummary": "Confirm which principle this rule should implement.",
133
+ "decisionRecorded": "The service received the decision. The list is refreshing from current state.",
134
+ "retry": "Read again",
135
+ "runtimeRecords": "Runtime and historical records",
136
+ "rulePromotion": "Rule promotion review",
137
+ "promotionSummary": "This rule is still being observed. Review the safety evidence before continuing observation, rejecting, or promoting it.",
138
+ "openActivation": "View rule runtime and emergency controls",
139
+ "safetyAlert": "{{count}} rules are safety-isolated. Check their runtime state.",
140
+ "approvalsUnavailable": "Some approval material could not be read. Other confirmed items remain available."
141
+ },
51
142
  "ruleBinding": {
52
143
  "title": "Formal principle attribution for rules",
53
144
  "separate": "Attribution only states which principle a rule belongs to; it does not approve the rule, enable it, or replace a development design approval.",
@@ -522,13 +613,7 @@
522
613
  },
523
614
  "conclusion": {
524
615
  "sectionTitle": "Current status",
525
- "allClear": "Nothing needs your attention right now.",
526
- "pendingTotal": "{{count}} item(s) waiting on you",
527
- "actionableUnlocked": "{{count}} of them actionable now",
528
- "actionableLocked": "{{count}} actionable object(s) — governance actions are locked, see the notice above",
529
- "countsNote": "Scope: \"waiting on you\" = actionable objects on screen + inbox pending items (decisions / individually-attended blocked / recovery) not already shown; each object counts once; historical unknowns stay explanatory and are not counted.",
530
- "pendingUncertain": "Pending count cannot be confirmed — decision updates are unavailable.",
531
- "actionableWithoutTotal": "{{count}} item(s) actionable now",
616
+ "countsNote": "Scope: the count lists the work items in this workbench; blocked and recovery items are listed separately below.",
532
617
  "degradedLabel": "Decision updates degraded:"
533
618
  },
534
619
  "ownerAttention": {
@@ -756,6 +841,9 @@
756
841
  "metricLatest": "Last Updated",
757
842
  "blockBasis": "Formation Basis",
758
843
  "blockBasisFallback": "This principle was distilled from historical behavior calibration. Its detailed source is not yet linked.",
844
+ "contextDisclosure": "Formation and other information",
845
+ "libraryOverview": "Principle overview",
846
+ "openDetails": "View principle details",
759
847
  "blockImpact": "Current Impact",
760
848
  "blockImpactSeeDecisionView": "Whether this currently shapes behavior is shown on the detail page under current enforcement — this card does not assert impact from lifecycle status alone.",
761
849
  "blockImpactActive": "This principle is currently shaping the Agent's subsequent behavior.",
@@ -812,6 +900,9 @@
812
900
  "detail": {
813
901
  "loading": "Loading…",
814
902
  "backToList": "← Back to list",
903
+ "formationDisclosure": "How this principle formed",
904
+ "fullRule": "Read the complete principle",
905
+ "extraContext": "Other source notes and reflection questions",
815
906
  "conclusion": "Conclusion",
816
907
  "policyNote": "This is a behavioral policy for future similar scenarios. You can approve, modify, reject, or park it.",
817
908
  "modifyWording": "Modify Wording",