create-principles-disciple 1.109.0 → 1.111.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/console/dist/server/config/pd-config-store.d.ts +29 -0
  2. package/console/dist/server/config/pd-config-store.js +55 -0
  3. package/console/dist/server/feedback/channels.d.ts +36 -0
  4. package/console/dist/server/feedback/channels.js +89 -0
  5. package/console/dist/server/feedback/github-adapter.d.ts +44 -0
  6. package/console/dist/server/feedback/github-adapter.js +125 -0
  7. package/console/dist/server/feedback/ingest-adapter.d.ts +45 -0
  8. package/console/dist/server/feedback/ingest-adapter.js +129 -0
  9. package/console/dist/server/feedback/submit-service.d.ts +44 -0
  10. package/console/dist/server/feedback/submit-service.js +177 -0
  11. package/console/dist/server/index.js +23 -7
  12. package/console/dist/server/models/FeedbackReportConsoleModel.d.ts +8 -0
  13. package/console/dist/server/models/FeedbackReportConsoleModel.js +45 -0
  14. package/console/dist/server/routes/feedback-reports.d.ts +24 -0
  15. package/console/dist/server/routes/feedback-reports.js +151 -0
  16. package/console/dist/ui/api.d.ts +11 -3
  17. package/console/dist/ui/api.js +25 -2
  18. package/console/dist/ui/i18n/en.json +48 -2
  19. package/console/dist/ui/i18n/zh-CN.json +48 -2
  20. package/console/dist/ui/pages/ReportProblemValidators.d.ts +30 -0
  21. package/console/dist/ui/pages/ReportProblemValidators.js +66 -0
  22. package/console/dist/ui/pages/activation/ActivationPage.js +14 -3
  23. package/console/dist/ui/pages/pain/PainPage.js +6 -1
  24. package/console/dist/ui/pages/principles/PrincipleDetailPage.js +4 -1
  25. package/console/dist/ui/pages/report-problem/ReportProblemPage.js +263 -70
  26. package/console/dist/ui/utils/validators.d.ts +22 -0
  27. package/console/dist/ui/utils/validators.js +66 -0
  28. package/console/dist/web/assets/app.css +42 -0
  29. package/console/dist/web/assets/app.js +699 -127
  30. package/console/package.json +4 -4
  31. package/core/dist/runtime-v2/__tests__/architecture-regression.test.js +4 -0
  32. package/core/dist/runtime-v2/__tests__/architecture-regression.test.js.map +1 -1
  33. package/core/dist/runtime-v2/feature-flags/feature-flag-contract.js +1 -1
  34. package/core/dist/runtime-v2/feature-flags/feature-flag-contract.js.map +1 -1
  35. package/core/dist/runtime-v2/feedback/__tests__/fingerprint.test.d.ts +2 -0
  36. package/core/dist/runtime-v2/feedback/__tests__/fingerprint.test.d.ts.map +1 -0
  37. package/core/dist/runtime-v2/feedback/__tests__/fingerprint.test.js +126 -0
  38. package/core/dist/runtime-v2/feedback/__tests__/fingerprint.test.js.map +1 -0
  39. package/core/dist/runtime-v2/feedback/__tests__/slice1-typed-fields.test.d.ts +2 -0
  40. package/core/dist/runtime-v2/feedback/__tests__/slice1-typed-fields.test.d.ts.map +1 -0
  41. package/core/dist/runtime-v2/feedback/__tests__/slice1-typed-fields.test.js +278 -0
  42. package/core/dist/runtime-v2/feedback/__tests__/slice1-typed-fields.test.js.map +1 -0
  43. package/core/dist/runtime-v2/feedback/create-report.d.ts.map +1 -1
  44. package/core/dist/runtime-v2/feedback/create-report.js +38 -1
  45. package/core/dist/runtime-v2/feedback/create-report.js.map +1 -1
  46. package/core/dist/runtime-v2/feedback/feedback-types.d.ts +60 -0
  47. package/core/dist/runtime-v2/feedback/feedback-types.d.ts.map +1 -1
  48. package/core/dist/runtime-v2/feedback/feedback-types.js +58 -0
  49. package/core/dist/runtime-v2/feedback/feedback-types.js.map +1 -1
  50. package/core/dist/runtime-v2/feedback/fingerprint.d.ts +31 -0
  51. package/core/dist/runtime-v2/feedback/fingerprint.d.ts.map +1 -0
  52. package/core/dist/runtime-v2/feedback/fingerprint.js +51 -0
  53. package/core/dist/runtime-v2/feedback/fingerprint.js.map +1 -0
  54. package/core/dist/runtime-v2/feedback/index.d.ts +3 -2
  55. package/core/dist/runtime-v2/feedback/index.d.ts.map +1 -1
  56. package/core/dist/runtime-v2/feedback/index.js +2 -1
  57. package/core/dist/runtime-v2/feedback/index.js.map +1 -1
  58. package/core/dist/runtime-v2/feedback/privacy-preview.d.ts.map +1 -1
  59. package/core/dist/runtime-v2/feedback/privacy-preview.js +43 -0
  60. package/core/dist/runtime-v2/feedback/privacy-preview.js.map +1 -1
  61. package/core/dist/runtime-v2/feedback/render-markdown.d.ts.map +1 -1
  62. package/core/dist/runtime-v2/feedback/render-markdown.js +33 -0
  63. package/core/dist/runtime-v2/feedback/render-markdown.js.map +1 -1
  64. package/core/dist/runtime-v2/index.d.ts +2 -2
  65. package/core/dist/runtime-v2/index.d.ts.map +1 -1
  66. package/core/dist/runtime-v2/index.js +1 -1
  67. package/core/dist/runtime-v2/index.js.map +1 -1
  68. package/dist/installer.d.ts.map +1 -1
  69. package/dist/installer.js.map +1 -1
  70. package/dist/mvp-config.d.ts +22 -1
  71. package/dist/mvp-config.d.ts.map +1 -1
  72. package/dist/mvp-config.js +36 -1
  73. package/dist/mvp-config.js.map +1 -1
  74. package/package.json +1 -1
  75. package/plugin/dist/bundle.js +302 -302
  76. package/plugin/dist/openclaw.plugin.json +1 -18
  77. package/plugin/openclaw.plugin.json +2 -19
  78. package/plugin/package.json +3 -3
  79. package/plugin/scripts/sync-plugin.mjs +34 -33
@@ -0,0 +1,177 @@
1
+ // submit-service.ts
2
+ // Orchestrates a feedback submission: load the saved (already-redacted) draft
3
+ // by id, compute its fingerprint, hand it to the chosen adapter, then write
4
+ // the receipt back when the adapter succeeds (spec §8.2).
5
+ //
6
+ // Trust boundaries:
7
+ // - The request body never carries report content — only { channel } (route-level).
8
+ // Everything sent onwards comes from the re-loaded disk draft.
9
+ // - Idempotency: an already-submitted draft returns alreadySubmitted:true and
10
+ // is never resubmitted.
11
+ // - Failure never downgrades the draft's status: any adapter error leaves it
12
+ // 'draft' with a structured reason + nextAction (rc-9).
13
+ // - Write-back failure still returns success + writeBackFailed:true rather than
14
+ // dropping the receipt (spec §12 — "宁重复提示不丢回执").
15
+ import { computeFeedbackFingerprint, } from '@principles/core/runtime-v2/feedback';
16
+ import { submitToIngest } from './ingest-adapter.js';
17
+ import { submitToGithub } from './github-adapter.js';
18
+ /**
19
+ * Submit a saved draft by id through the given channel. `maintainerEmail`
20
+ * (route-level) is accepted separately for parity/future use.
21
+ */
22
+ export async function submitReport(opts) {
23
+ const { model, reportId, channel, config, deps } = opts;
24
+ // 1. Load the saved draft (already redacted at creation time).
25
+ const loaded = await model.get(reportId);
26
+ const { ok, report, errorCode, error } = loaded;
27
+ if (!ok || !report) {
28
+ if (errorCode === 'NOT_FOUND') {
29
+ return {
30
+ ok: false,
31
+ statusCode: 404,
32
+ error: 'feedback_report_not_found',
33
+ message: `No saved feedback draft with id ${reportId}`,
34
+ nextAction: '从草稿列表选择一个已保存的反馈再提交',
35
+ };
36
+ }
37
+ return {
38
+ ok: false,
39
+ statusCode: 400,
40
+ error: 'feedback_report_load_failed',
41
+ message: `Failed to load draft: ${error ?? 'unknown'}`,
42
+ nextAction: '检查草稿文件是否可读后重试',
43
+ };
44
+ }
45
+ // 2. Idempotency — never resubmit an already-submitted draft.
46
+ if (report.status === 'submitted') {
47
+ return {
48
+ ok: true,
49
+ alreadySubmitted: true,
50
+ status: 'submitted',
51
+ submittedVia: report.submittedVia,
52
+ trackingId: report.trackingId,
53
+ externalUrl: report.externalUrl,
54
+ };
55
+ }
56
+ // 3. Channel configuration gate (single-channel disable = remove its config key).
57
+ if (channel === 'ingest' && config.ingestUrl.length === 0) {
58
+ return {
59
+ ok: false,
60
+ statusCode: 400,
61
+ error: 'ingest_channel_not_configured',
62
+ message: 'ingest 通道未配置(feedback.ingest_url 为空)',
63
+ nextAction: '在 .pd/config.yaml 配置 feedback.ingest_url,或选择其他通道',
64
+ };
65
+ }
66
+ if (channel === 'github' && config.githubRepo.length === 0) {
67
+ return {
68
+ ok: false,
69
+ statusCode: 400,
70
+ error: 'github_channel_not_configured',
71
+ message: 'gh 通道未配置(feedback.github_repo 为空)',
72
+ nextAction: '在 .pd/config.yaml 配置 feedback.github_repo,或选择其他通道',
73
+ };
74
+ }
75
+ // 4. Deterministic fingerprint for relay-side dedup/clustering.
76
+ const fingerprint = computeFeedbackFingerprint({
77
+ type: report.type,
78
+ title: report.title,
79
+ area: report.area,
80
+ });
81
+ const { area } = report;
82
+ const submittedAt = new Date().toISOString();
83
+ if (channel === 'ingest') {
84
+ const outcome = await submitToIngest({
85
+ url: config.ingestUrl,
86
+ token: config.ingestToken,
87
+ report,
88
+ fingerprint,
89
+ area,
90
+ fetchFn: deps?.fetchFn,
91
+ });
92
+ if (!outcome.ok) {
93
+ // 0 = network/timeout (downstream). Failed → keep draft untouched.
94
+ return {
95
+ ok: false,
96
+ statusCode: outcome.status === 0 ? 502 : outcome.status,
97
+ error: 'ingest_submit_failed',
98
+ message: outcome.reason,
99
+ nextAction: outcome.nextAction,
100
+ };
101
+ }
102
+ // 5a. Write receipt back; a write-back failure must not drop the success.
103
+ const patch = {
104
+ status: 'submitted',
105
+ submittedAt,
106
+ submittedVia: 'ingest',
107
+ };
108
+ if (outcome.trackingId)
109
+ patch.trackingId = outcome.trackingId;
110
+ if (outcome.issueUrl)
111
+ patch.externalUrl = outcome.issueUrl;
112
+ const wb = await model.update(reportId, patch);
113
+ if (!wb.ok || !wb.report) {
114
+ return {
115
+ ok: true,
116
+ alreadySubmitted: false,
117
+ status: 'submitted',
118
+ submittedVia: 'ingest',
119
+ trackingId: outcome.trackingId,
120
+ externalUrl: outcome.issueUrl,
121
+ writeBackFailed: true,
122
+ nextAction: '提交已完成但状态写回失败,请手动记录回执编号(可在此页面查看)',
123
+ };
124
+ }
125
+ return {
126
+ ok: true,
127
+ alreadySubmitted: false,
128
+ status: 'submitted',
129
+ submittedVia: 'ingest',
130
+ trackingId: outcome.trackingId,
131
+ externalUrl: outcome.issueUrl,
132
+ };
133
+ }
134
+ // github
135
+ const outcome = await submitToGithub({
136
+ repo: config.githubRepo,
137
+ proxy: config.githubProxy,
138
+ report,
139
+ execFile: deps?.execFile,
140
+ fs: deps?.fs,
141
+ os: deps?.os,
142
+ path: deps?.path,
143
+ });
144
+ if (!outcome.ok) {
145
+ return {
146
+ ok: false,
147
+ statusCode: 502,
148
+ error: 'github_submit_failed',
149
+ message: outcome.reason,
150
+ nextAction: outcome.nextAction,
151
+ };
152
+ }
153
+ const wb = await model.update(reportId, {
154
+ status: 'submitted',
155
+ submittedAt,
156
+ submittedVia: 'github',
157
+ externalUrl: outcome.issueUrl,
158
+ });
159
+ if (!wb.ok || !wb.report) {
160
+ return {
161
+ ok: true,
162
+ alreadySubmitted: false,
163
+ status: 'submitted',
164
+ submittedVia: 'github',
165
+ externalUrl: outcome.issueUrl,
166
+ writeBackFailed: true,
167
+ nextAction: '提交已完成但状态写回失败,请手动记录 issue 链接',
168
+ };
169
+ }
170
+ return {
171
+ ok: true,
172
+ alreadySubmitted: false,
173
+ status: 'submitted',
174
+ submittedVia: 'github',
175
+ externalUrl: outcome.issueUrl,
176
+ };
177
+ }
@@ -2,11 +2,11 @@ import * as http from 'http';
2
2
  import * as fs from 'fs';
3
3
  import * as path from 'path';
4
4
  import { fileURLToPath } from 'url';
5
- import { loadPdConfig, computeFlagsFromLoadResult, getFeedbackMaintainerEmail } from './config/pd-config-store.js';
5
+ import { loadPdConfig, computeFlagsFromLoadResult, getFeedbackMaintainerEmail, getFeedbackChannelConfig, } from './config/pd-config-store.js';
6
6
  import { AuthConfig } from './config/AuthConfig.js';
7
7
  import { WorkspaceConfigStore } from './config/WorkspaceConfigStore.js';
8
8
  import { WorkspaceService } from './models/WorkspaceService.js';
9
- import { handleFeedbackReportsRoute, disposeFeedbackReportModels } from './routes/feedback-reports.js';
9
+ import { handleFeedbackReportsRoute, handleFeedbackChannelsRoute, disposeFeedbackReportModels, } from './routes/feedback-reports.js';
10
10
  import { handleFailedTasksRoute, disposeFailedTasksModels } from './routes/failed-tasks.js';
11
11
  import { handleApprovalsRoute, disposeApprovalsModels } from './routes/approvals.js';
12
12
  import { handleHealthRoute, disposeHealthModels } from './routes/health.js';
@@ -216,10 +216,14 @@ async function initServices(workspaceDir, authConfig) {
216
216
  if (!configResult.ok) {
217
217
  console.warn('[pd-console] PD config loading failed (using defaults for feedback channel):', configResult.errors.map(e => e.reason).join('; '));
218
218
  }
219
- // Read feedback.maintainer_email from .pd/config.yaml (defaults to
220
- // csuzngjh@hotmail.com when absent). Used to build mailto: URLs in feedback
221
- // reports so the owner can open a pre-filled email directly.
219
+ // Read feedback.maintainer_email from .pd/config.yaml. Falls back to a
220
+ // placeholder (maintainer@example.com) when absent; UI/email channel honours
221
+ // that default at the channel-level (not here). Used to build mailto: URLs in
222
+ // feedback reports so the owner can open a pre-filled email directly.
222
223
  const maintainerEmail = getFeedbackMaintainerEmail(workspaceDir);
224
+ // Read the feedback submit-channel parameters (ingest_url / ingest_token /
225
+ // github_repo / github_proxy). Presence of a key enables its channel.
226
+ const feedbackChannelConfig = getFeedbackChannelConfig(workspaceDir);
223
227
  return {
224
228
  workspaceDir,
225
229
  authConfig,
@@ -227,6 +231,7 @@ async function initServices(workspaceDir, authConfig) {
227
231
  workspaceService,
228
232
  feedbackFlags,
229
233
  maintainerEmail,
234
+ feedbackChannelConfig,
230
235
  };
231
236
  }
232
237
  async function closeServices() {
@@ -277,10 +282,21 @@ function handleRequest(services) {
277
282
  return;
278
283
  }
279
284
  // ── API routes ──────────────────────────────────────────────────
280
- // GET/POST /api/feedback/reports, /api/feedback/reports/:id
285
+ // GET /api/feedback/submit/channels — submit-ladder probe
286
+ if (urlPath === '/api/feedback/submit/channels') {
287
+ asyncHandler(() => handleFeedbackChannelsRoute(req, res, {
288
+ workspaceDir: services.workspaceDir,
289
+ channelConfig: services.feedbackChannelConfig,
290
+ featureFlags: services.feedbackFlags,
291
+ maintainerEmail: services.maintainerEmail,
292
+ }))(req, res);
293
+ return;
294
+ }
295
+ // GET/POST /api/feedback/reports, /api/feedback/reports/:id,
296
+ // POST /api/feedback/reports/:id/submit
281
297
  if (urlPath === '/api/feedback/reports' || urlPath.startsWith('/api/feedback/reports/')) {
282
298
  const subPath = urlPath.slice('/api/feedback/reports'.length);
283
- asyncHandler(() => handleFeedbackReportsRoute(req, res, { workspaceDir: services.workspaceDir, subPath, featureFlags: services.feedbackFlags, maintainerEmail: services.maintainerEmail }))(req, res);
299
+ asyncHandler(() => handleFeedbackReportsRoute(req, res, { workspaceDir: services.workspaceDir, subPath, featureFlags: services.feedbackFlags, maintainerEmail: services.maintainerEmail, channelConfig: services.feedbackChannelConfig }))(req, res);
284
300
  return;
285
301
  }
286
302
  // Task 9: GET /api/v1/failed-tasks, /api/v1/failed-tasks/:id
@@ -4,6 +4,8 @@ export type FeedbackReportDraftSummary = {
4
4
  createdAt: string;
5
5
  type: string;
6
6
  title: string;
7
+ /** 分发状态(缺省 draft);用于草稿列表区分 待发/已发(切片3) */
8
+ status?: string;
7
9
  };
8
10
  export type FeedbackReportListResult = {
9
11
  ok: boolean;
@@ -45,5 +47,11 @@ export declare class FeedbackReportConsoleModel {
45
47
  * Delete a draft by id. No-op (still ok:true) if the file does not exist.
46
48
  */
47
49
  delete(id: string): Promise<FeedbackReportDeleteResult>;
50
+ /**
51
+ * Update a persisted draft by applying a partial patch, then atomic-write.
52
+ * Mirrors `create` (tmp + rename). Fails loud (not_found / read_error /
53
+ * invalid shape) without mutating on error.
54
+ */
55
+ update(id: string, patch: Partial<FeedbackReport>): Promise<FeedbackReportGetResult>;
48
56
  dispose(): void;
49
57
  }
@@ -138,6 +138,7 @@ export class FeedbackReportConsoleModel {
138
138
  createdAt: obj.createdAt,
139
139
  type: obj.type,
140
140
  title: obj.title,
141
+ status: typeof obj.status === 'string' ? obj.status : undefined,
141
142
  });
142
143
  }
143
144
  }
@@ -233,6 +234,50 @@ export class FeedbackReportConsoleModel {
233
234
  };
234
235
  }
235
236
  }
237
+ /**
238
+ * Update a persisted draft by applying a partial patch, then atomic-write.
239
+ * Mirrors `create` (tmp + rename). Fails loud (not_found / read_error /
240
+ * invalid shape) without mutating on error.
241
+ */
242
+ async update(id, patch) {
243
+ if (!reportIdValidator(id)) {
244
+ return {
245
+ ok: false,
246
+ error: `Invalid feedback report id: ${JSON.stringify(id)}`,
247
+ errorCode: 'INVALID_ID',
248
+ nextAction: 'pass a safe id matching /^[A-Za-z0-9._-]+$/ with length 1-256',
249
+ };
250
+ }
251
+ if (patch === null || typeof patch !== 'object' || Array.isArray(patch)) {
252
+ return {
253
+ ok: false,
254
+ error: 'patch must be a plain object with optional FeedbackReport fields',
255
+ nextAction: 'pass a partial FeedbackReport object to update',
256
+ };
257
+ }
258
+ const loaded = await this.get(id);
259
+ if (!loaded.ok || !loaded.report)
260
+ return loaded;
261
+ const merged = { ...loaded.report, ...patch, id };
262
+ const dir = this.ensureDraftsDir();
263
+ if (!dir.ok)
264
+ return dir;
265
+ const filePath = path.join(this.draftsDir, `${id}.json`);
266
+ try {
267
+ const tmpPath = `${filePath}.${randomUUID()}.tmp`;
268
+ const body = JSON.stringify(merged, null, 2);
269
+ await fs.promises.writeFile(tmpPath, body, { encoding: 'utf8', mode: 0o600 });
270
+ await fs.promises.rename(tmpPath, filePath);
271
+ return { ok: true, report: merged };
272
+ }
273
+ catch (err) {
274
+ return {
275
+ ok: false,
276
+ error: `Failed to write feedback report update at ${filePath}: ${err instanceof Error ? err.message : String(err)}`,
277
+ nextAction: 'verify the workspace directory is writable and that disk space is available',
278
+ };
279
+ }
280
+ }
236
281
  dispose() {
237
282
  // Nothing to dispose; this model holds no connections.
238
283
  // (This override satisfies the class-methods-use-this ESLint rule for
@@ -1,4 +1,7 @@
1
1
  import type { IncomingMessage, ServerResponse } from 'node:http';
2
+ import type { FeedbackChannelConfig } from '../config/pd-config-store.js';
3
+ import { probeChannels } from '../feedback/channels.js';
4
+ import { type SubmitDeps } from '../feedback/submit-service.js';
2
5
  /**
3
6
  * Handle requests to `/api/feedback/reports*` for the given workspace.
4
7
  * Routes:
@@ -6,6 +9,7 @@ import type { IncomingMessage, ServerResponse } from 'node:http';
6
9
  * GET /api/feedback/reports — list draft summaries
7
10
  * GET /api/feedback/reports/:id — fetch a single draft
8
11
  * DELETE /api/feedback/reports/:id — delete a draft
12
+ * POST /api/feedback/reports/:id/submit — submit a saved draft via a channel
9
13
  */
10
14
  export type FeedbackReportsContext = {
11
15
  workspaceDir: string;
@@ -14,6 +18,26 @@ export type FeedbackReportsContext = {
14
18
  enabled: boolean;
15
19
  }>;
16
20
  maintainerEmail?: string;
21
+ channelConfig?: FeedbackChannelConfig;
22
+ submitDeps?: SubmitDeps;
17
23
  };
18
24
  export declare function handleFeedbackReportsRoute(req: IncomingMessage, res: ServerResponse, ctx: FeedbackReportsContext): Promise<void>;
25
+ /**
26
+ * Channels context for the submit-ladder probe endpoint.
27
+ */
28
+ export type FeedbackChannelsContext = {
29
+ workspaceDir: string;
30
+ channelConfig?: FeedbackChannelConfig;
31
+ featureFlags?: Record<string, {
32
+ enabled: boolean;
33
+ }>;
34
+ maintainerEmail?: string;
35
+ channelDeps?: Parameters<typeof probeChannels>[1];
36
+ };
37
+ /**
38
+ * GET /api/feedback/submit/channels — build the channel ladder availability
39
+ * (spec §8.1). When the feedback_channel flag is off, the whole submit area is
40
+ * hidden from the UI; the endpoint still probes but the client drops it.
41
+ */
42
+ export declare function handleFeedbackChannelsRoute(req: IncomingMessage, res: ServerResponse, ctx: FeedbackChannelsContext): Promise<void>;
19
43
  export declare function disposeFeedbackReportModels(): void;
@@ -8,6 +8,8 @@
8
8
  import { createFeedbackReport, PendingAgentDraftStore, } from '@principles/core/runtime-v2/feedback';
9
9
  import { SqliteConnection } from '@principles/core/runtime-v2';
10
10
  import { FeedbackReportConsoleModel, } from '../models/FeedbackReportConsoleModel.js';
11
+ import { probeChannels } from '../feedback/channels.js';
12
+ import { submitReport } from '../feedback/submit-service.js';
11
13
  import { sendSuccess, sendError, sendNotFound, sendMethodNotAllowed, sendBadRequest, } from '../utils/response.js';
12
14
  const models = new Map();
13
15
  function getModel(workspaceDir) {
@@ -67,6 +69,10 @@ async function readJsonBody(req) {
67
69
  function errorMessage(err) {
68
70
  return err instanceof Error ? err.message : String(err);
69
71
  }
72
+ // Matches /:id/submit where :id is a safe report id.
73
+ const SUBMIT_SUBPATH = /^\/[A-Za-z0-9._-]+\/submit$/;
74
+ // Matches /:id/mark-sent (manual "marked as sent" for mailto/export channels).
75
+ const MARK_SENT_SUBPATH = /^\/[A-Za-z0-9._-]+\/mark-sent$/;
70
76
  export async function handleFeedbackReportsRoute(req, res, ctx) {
71
77
  const { workspaceDir, subPath } = ctx;
72
78
  const model = getModel(workspaceDir);
@@ -131,6 +137,127 @@ export async function handleFeedbackReportsRoute(req, res, ctx) {
131
137
  sendMethodNotAllowed(res);
132
138
  return;
133
139
  }
140
+ // Submit: POST /api/feedback/reports/:id/submit (feature-flag gated).
141
+ if (SUBMIT_SUBPATH.test(subPath)) {
142
+ if (method !== 'POST') {
143
+ sendMethodNotAllowed(res);
144
+ return;
145
+ }
146
+ // Feature flag gate — feedback_channel off ⇒ submit endpoint 403 (spec §12).
147
+ if (ctx.featureFlags
148
+ && Object.hasOwn(ctx.featureFlags, 'feedback_channel')) {
149
+ const feedbackChannel = ctx.featureFlags.feedback_channel;
150
+ if (feedbackChannel && !feedbackChannel.enabled) {
151
+ sendError(res, 403, 'feedback_channel_disabled', 'feedback_channel feature flag is disabled. Enable feedback_channel in .pd/config.yaml to submit feedback.', { nextAction: '在 .pd/config.yaml 将 features.feedback_channel.enabled 改为 true' });
152
+ return;
153
+ }
154
+ }
155
+ // The report id is everything before the trailing "/submit".
156
+ const reportId = subPath.slice(1, subPath.length - '/submit'.length);
157
+ const bodyResult = await readJsonBody(req);
158
+ if (!bodyResult.ok) {
159
+ sendBadRequest(res, bodyResult.error);
160
+ return;
161
+ }
162
+ if (bodyResult.value === null || typeof bodyResult.value !== 'object' || Array.isArray(bodyResult.value)) {
163
+ sendBadRequest(res, 'request body must be a JSON object with { channel }');
164
+ return;
165
+ }
166
+ const obj = bodyResult.value;
167
+ const channelRaw = Object.hasOwn(obj, 'channel') ? obj.channel : undefined;
168
+ if (channelRaw !== 'ingest' && channelRaw !== 'github') {
169
+ sendBadRequest(res, 'channel must be one of: "ingest", "github"');
170
+ return;
171
+ }
172
+ const channel = channelRaw;
173
+ try {
174
+ const result = await submitReport({
175
+ model,
176
+ reportId,
177
+ channel,
178
+ config: ctx.channelConfig ?? { ingestUrl: '', ingestToken: '', githubRepo: '', githubProxy: '' },
179
+ deps: ctx.submitDeps,
180
+ });
181
+ if (!result.ok) {
182
+ sendError(res, result.statusCode, result.error, result.message, { nextAction: result.nextAction });
183
+ return;
184
+ }
185
+ sendSuccess(res, result);
186
+ }
187
+ catch (err) {
188
+ // rc-9: never silence — structured error + nextAction.
189
+ sendError(res, 500, 'feedback_reports_submit_error', errorMessage(err), {
190
+ nextAction: '稍后重试;或改用导出文件通道发送给维护者',
191
+ });
192
+ }
193
+ return;
194
+ }
195
+ // Manual "mark as sent" for mailto/export channels (spec §11.4): user
196
+ // declares the draft delivered; the console cannot confirm server-side, so
197
+ // this is an honest client-declared status write-back — never a fake ack.
198
+ if (MARK_SENT_SUBPATH.test(subPath)) {
199
+ if (method !== 'POST') {
200
+ sendMethodNotAllowed(res);
201
+ return;
202
+ }
203
+ if (ctx.featureFlags
204
+ && Object.hasOwn(ctx.featureFlags, 'feedback_channel')) {
205
+ const feedbackChannel = ctx.featureFlags.feedback_channel;
206
+ if (feedbackChannel && !feedbackChannel.enabled) {
207
+ sendError(res, 403, 'feedback_channel_disabled', 'feedback_channel feature flag is disabled. Enable feedback_channel in .pd/config.yaml to mark feedback as sent.', { nextAction: '在 .pd/config.yaml 将 features.feedback_channel.enabled 改为 true' });
208
+ return;
209
+ }
210
+ }
211
+ const reportId = subPath.slice(1, subPath.length - '/mark-sent'.length);
212
+ const bodyResult = await readJsonBody(req);
213
+ if (!bodyResult.ok) {
214
+ sendBadRequest(res, bodyResult.error);
215
+ return;
216
+ }
217
+ const viaRaw = bodyResult.value !== null &&
218
+ typeof bodyResult.value === 'object' &&
219
+ !Array.isArray(bodyResult.value)
220
+ ? bodyResult.value.via
221
+ : undefined;
222
+ if (viaRaw !== 'email' && viaRaw !== 'file') {
223
+ sendBadRequest(res, 'via must be one of: "email", "file"');
224
+ return;
225
+ }
226
+ try {
227
+ const loaded = await model.get(reportId);
228
+ if (!loaded.ok || !loaded.report) {
229
+ sendError(res, loaded.errorCode === 'NOT_FOUND' ? 404 : 400, 'mark_sent_report_not_found', loaded.error ?? 'draft not found', {
230
+ nextAction: '从草稿列表选择一个已保存的反馈再标记',
231
+ });
232
+ return;
233
+ }
234
+ // Honesty: do not re-stamp an already-submitted draft (alreadySubmitted).
235
+ if (loaded.report.status === 'submitted') {
236
+ sendSuccess(res, {
237
+ ok: true,
238
+ alreadySubmitted: true,
239
+ status: 'submitted',
240
+ submittedVia: loaded.report.submittedVia,
241
+ trackingId: loaded.report.trackingId,
242
+ externalUrl: loaded.report.externalUrl,
243
+ });
244
+ return;
245
+ }
246
+ const submittedAt = new Date().toISOString();
247
+ const wb = await model.update(reportId, { status: 'submitted', submittedAt, submittedVia: viaRaw });
248
+ if (!wb.ok || !wb.report) {
249
+ sendError(res, 500, 'mark_sent_write_back_failed', wb.error ?? 'write-back failed', {
250
+ nextAction: '标记失败,请稍后重试',
251
+ });
252
+ return;
253
+ }
254
+ sendSuccess(res, { ok: true, alreadySubmitted: false, status: 'submitted', submittedVia: viaRaw, submittedAt });
255
+ }
256
+ catch (err) {
257
+ sendError(res, 500, 'mark_sent_error', errorMessage(err), { nextAction: '标记失败,请稍后重试' });
258
+ }
259
+ return;
260
+ }
134
261
  // Single draft: /:id
135
262
  const id = subPath.replace(/^\//, '');
136
263
  if (id.length === 0) {
@@ -176,6 +303,30 @@ export async function handleFeedbackReportsRoute(req, res, ctx) {
176
303
  }
177
304
  sendMethodNotAllowed(res);
178
305
  }
306
+ /**
307
+ * GET /api/feedback/submit/channels — build the channel ladder availability
308
+ * (spec §8.1). When the feedback_channel flag is off, the whole submit area is
309
+ * hidden from the UI; the endpoint still probes but the client drops it.
310
+ */
311
+ export async function handleFeedbackChannelsRoute(req, res, ctx) {
312
+ const method = req.method ?? 'GET';
313
+ if (method !== 'GET') {
314
+ sendMethodNotAllowed(res);
315
+ return;
316
+ }
317
+ try {
318
+ const config = ctx.channelConfig ?? { ingestUrl: '', ingestToken: '', githubRepo: '', githubProxy: '' };
319
+ const result = await probeChannels(config, ctx.channelDeps ?? {}, ctx.maintainerEmail ?? '');
320
+ // Never leak the ingest token into a client-facing payload (probeChannels already omits it).
321
+ sendSuccess(res, { channels: result.channels });
322
+ }
323
+ catch (err) {
324
+ // rc-9: structured failure with nextAction; the client hides submit area.
325
+ sendError(res, 500, 'feedback_channels_probe_error', errorMessage(err), {
326
+ nextAction: '稍后重试;或直接使用导出文件通道',
327
+ });
328
+ }
329
+ }
179
330
  export function disposeFeedbackReportModels() {
180
331
  for (const model of models.values()) {
181
332
  model.dispose();
@@ -1,6 +1,6 @@
1
1
  import type { ApiResponse } from "../types.js";
2
2
  import { listActiveSignalKeywords, listPendingSignalTerms, fetchKeywordStore, fetchPendingTerms, updateKeywordStore, admitPendingTerm, rejectPendingTerm } from "./utils/signal-keywords-api.js";
3
- import type { FeedbackReportData, FeedbackDraftSummaryData, FeedbackDraftEnvelopeData, DeleteEnvelopeData, WorkspaceEntryData, RemovedEnvelopeData, SyncResultData, ConfigSummaryData, ConfigCatalogData, AgentBindingUpdateData, DefaultRuntimeUpdateData, RuntimeProfileMutationData, ReadinessCheckData, FeatureFlagUpdateData, OutputLanguageData, GovernanceQueueData, ActivationsData, DisableActivationData, LifecycleMetricsData, UpdateStatusData, UpdateHistoryData, ApplyUpdateResultData, RollbackResultData, ApprovalRecordData, PrinciplesListData, ApprovalsGroupedData, EvidenceChainData, TrajectoryData, IntentSummaryData, IntentDecisionRecordData, IntentDecisionResultData, IntentDecisionSummaryData, FollowUpResponseData, IntentInitResultData, IntentSaveResultData, IntentRawContentData, IntentVersionData } from "./utils/validators.js";
3
+ import type { FeedbackReportData, FeedbackDraftSummaryData, FeedbackDraftEnvelopeData, FeedbackChannelsData, FeedbackSubmitResultData, DeleteEnvelopeData, WorkspaceEntryData, RemovedEnvelopeData, SyncResultData, ConfigSummaryData, ConfigCatalogData, AgentBindingUpdateData, DefaultRuntimeUpdateData, RuntimeProfileMutationData, ReadinessCheckData, FeatureFlagUpdateData, OutputLanguageData, GovernanceQueueData, ActivationsData, DisableActivationData, LifecycleMetricsData, UpdateStatusData, UpdateHistoryData, ApplyUpdateResultData, RollbackResultData, ApprovalRecordData, PrinciplesListData, ApprovalsGroupedData, EvidenceChainData, TrajectoryData, IntentSummaryData, IntentDecisionRecordData, IntentDecisionResultData, IntentDecisionSummaryData, FollowUpResponseData, IntentInitResultData, IntentSaveResultData, IntentRawContentData, IntentVersionData } from "./utils/validators.js";
4
4
  declare function getToken(): string | null;
5
5
  declare function setToken(token: string): void;
6
6
  declare function clearToken(): void;
@@ -37,6 +37,14 @@ declare function listFeedbackReports(): Promise<ApiResponse<{
37
37
  }>>;
38
38
  declare function getFeedbackReport(id: string): Promise<ApiResponse<FeedbackDraftEnvelopeData>>;
39
39
  declare function deleteFeedbackReport(id: string): Promise<ApiResponse<DeleteEnvelopeData>>;
40
+ declare function fetchFeedbackChannels(): Promise<ApiResponse<FeedbackChannelsData>>;
41
+ declare function submitFeedbackReport(id: string, channel: 'ingest' | 'github'): Promise<ApiResponse<FeedbackSubmitResultData>>;
42
+ /**
43
+ * Manually mark a draft as sent (mailto/export channels, spec §11.4). The
44
+ * console cannot server-confirm delivery for these channels, so this is an
45
+ * honest user-declared status write-back — never a fake acknowledgement.
46
+ */
47
+ declare function markFeedbackReportSent(id: string, via: 'email' | 'file'): Promise<ApiResponse<FeedbackSubmitResultData>>;
40
48
  declare function fetchConfigSummary(): Promise<ApiResponse<ConfigSummaryData>>;
41
49
  declare function fetchConfigCatalog(): Promise<ApiResponse<ConfigCatalogData>>;
42
50
  declare function updateAgentBinding(agentName: string, runtimeProfile: string, enabled: boolean): Promise<ApiResponse<AgentBindingUpdateData>>;
@@ -148,8 +156,8 @@ export interface FollowUpPayload {
148
156
  candidateId?: string;
149
157
  }
150
158
  declare function dispatchFollowUp(decisionId: string, payload: FollowUpPayload): Promise<ApiResponse<FollowUpResponseData>>;
151
- export { request, listActiveSignalKeywords, listPendingSignalTerms, fetchKeywordStore, fetchPendingTerms, updateKeywordStore, admitPendingTerm, rejectPendingTerm, getToken, setToken, clearToken, checkAuth, approveApproval, rejectApproval, editApproval, fetchPrinciples, fetchPrincipleDetail, fetchPrincipleTrajectory, archivePrinciple, unarchivePrinciple, createFeedbackReport, listFeedbackReports, getFeedbackReport, deleteFeedbackReport, fetchConfigSummary, fetchConfigCatalog, updateAgentBinding, updateDefaultRuntime, createRuntimeProfile, updateRuntimeProfile, deleteRuntimeProfile, fetchAgentReadiness, patchFeatureFlag, fetchOutputLanguage, updateOutputLanguage, fetchWorkspaces, addWorkspace, removeWorkspace, syncWorkspace, fetchGovernanceQueue, fetchApprovalsGrouped, fetchAllActivations, disableActivation, fetchLifecycleMetrics, fetchUpdateStatus, fetchUpdateHistory, applyUpdate, applyFullUpdate, rollbackUpdate, fetchEvidenceChain, fetchIntentSummary, fetchIntentContent, createIntentTemplate, saveIntentContent, fetchIntentVersions, recordIntentDecision, listIntentDecisionsByPainId, listIntentDecisionsByTaskId, fetchIntentDecisionSummary, dispatchFollowUp, };
152
- export type { FeedbackReportData, FeedbackDraftSummaryData, FeedbackDraftEnvelopeData, DeleteEnvelopeData, WorkspaceEntryData, RemovedEnvelopeData, SyncResultData, ConfigSummaryData, ConfigCatalogData, AgentBindingUpdateData, DefaultRuntimeUpdateData, RuntimeProfileMutationData, ReadinessCheckData, FeatureFlagUpdateData, OutputLanguageData, GovernanceQueueData, ActivationsData, DisableActivationData, LifecycleMetricsData, UpdateStatusData, UpdateHistoryData, ApplyUpdateResultData, RollbackResultData, ApprovalRecordData, PrinciplesListData, ApprovalsGroupedData, EvidenceChainData, EvidenceChainRecordData, EvidenceChainStateData, TrajectoryData, TrajectoryStageData, IntentSummaryData, IntentSectionsData, IntentDocWarningData, IntentDecisionRecordData, IntentDecisionResultData, IntentDecisionSummaryData, FollowUpResponseData, LinkCandidateFollowUpData, GuideRulehostFollowUpData, GeneratePatchProposalFollowUpData, IntentVersionEntry, IntentVersionData, } from "./utils/validators.js";
159
+ export { request, listActiveSignalKeywords, listPendingSignalTerms, fetchKeywordStore, fetchPendingTerms, updateKeywordStore, admitPendingTerm, rejectPendingTerm, getToken, setToken, clearToken, checkAuth, approveApproval, rejectApproval, editApproval, fetchPrinciples, fetchPrincipleDetail, fetchPrincipleTrajectory, archivePrinciple, unarchivePrinciple, createFeedbackReport, listFeedbackReports, getFeedbackReport, deleteFeedbackReport, fetchFeedbackChannels, submitFeedbackReport, markFeedbackReportSent, fetchConfigSummary, fetchConfigCatalog, updateAgentBinding, updateDefaultRuntime, createRuntimeProfile, updateRuntimeProfile, deleteRuntimeProfile, fetchAgentReadiness, patchFeatureFlag, fetchOutputLanguage, updateOutputLanguage, fetchWorkspaces, addWorkspace, removeWorkspace, syncWorkspace, fetchGovernanceQueue, fetchApprovalsGrouped, fetchAllActivations, disableActivation, fetchLifecycleMetrics, fetchUpdateStatus, fetchUpdateHistory, applyUpdate, applyFullUpdate, rollbackUpdate, fetchEvidenceChain, fetchIntentSummary, fetchIntentContent, createIntentTemplate, saveIntentContent, fetchIntentVersions, recordIntentDecision, listIntentDecisionsByPainId, listIntentDecisionsByTaskId, fetchIntentDecisionSummary, dispatchFollowUp, };
160
+ export type { FeedbackReportData, FeedbackDraftSummaryData, FeedbackDraftEnvelopeData, FeedbackChannelsData, FeedbackSubmitResultData, DeleteEnvelopeData, WorkspaceEntryData, RemovedEnvelopeData, SyncResultData, ConfigSummaryData, ConfigCatalogData, AgentBindingUpdateData, DefaultRuntimeUpdateData, RuntimeProfileMutationData, ReadinessCheckData, FeatureFlagUpdateData, OutputLanguageData, GovernanceQueueData, ActivationsData, DisableActivationData, LifecycleMetricsData, UpdateStatusData, UpdateHistoryData, ApplyUpdateResultData, RollbackResultData, ApprovalRecordData, PrinciplesListData, ApprovalsGroupedData, EvidenceChainData, EvidenceChainRecordData, EvidenceChainStateData, TrajectoryData, TrajectoryStageData, IntentSummaryData, IntentSectionsData, IntentDocWarningData, IntentDecisionRecordData, IntentDecisionResultData, IntentDecisionSummaryData, FollowUpResponseData, LinkCandidateFollowUpData, GuideRulehostFollowUpData, GeneratePatchProposalFollowUpData, IntentVersionEntry, IntentVersionData, } from "./utils/validators.js";
153
161
  export type { SignalKeyword, PendingSignalTerm } from "./utils/signal-keywords-types.js";
154
162
  export type { ActivationRecordData as ActivationRecord } from "./utils/validators.js";
155
163
  export type { ApprovalRecordData as ApprovalRecord } from "./utils/validators.js";
@@ -1,5 +1,5 @@
1
1
  import { listActiveSignalKeywords, listPendingSignalTerms, fetchKeywordStore, fetchPendingTerms, updateKeywordStore, admitPendingTerm, rejectPendingTerm, } from "./utils/signal-keywords-api.js";
2
- import { validateErrorResponse, validateHeaders, validateFeedbackReport, validateFeedbackDraftsList, validateFeedbackDraftEnvelope, validateDeleteEnvelope, validateWorkspaceEntry, validateWorkspaceList, validateRemovedEnvelope, validateSyncResult, validateConfigSummary, validateConfigCatalog, validateAgentBindingUpdate, validateDefaultRuntimeUpdate, validateRuntimeProfileMutation, validateReadinessCheck, validateFeatureFlagUpdate, validateOutputLanguage, validateGovernanceQueue, validateActivations, validateDisableActivation, validateLifecycleMetrics, validateUpdateStatus, validateUpdateHistory, validateApplyUpdateResult, validateRollbackResult, validateApprovalRecordDirect, validatePrinciplesList, validateApprovalsGrouped, validateEvidenceChain, validateTrajectoryData, validateIntentSummary, validateIntentDecisionList, validateIntentDecisionResult, validateIntentDecisionSummary, validateFollowUpResponse, validateIntentInitResult, validateIntentSaveResult, validateIntentRawContent, validateIntentVersions, } from "./utils/validators.js";
2
+ import { validateErrorResponse, validateHeaders, validateFeedbackReport, validateFeedbackDraftsList, validateFeedbackDraftEnvelope, validateFeedbackChannels, validateFeedbackSubmitResult, validateDeleteEnvelope, validateWorkspaceEntry, validateWorkspaceList, validateRemovedEnvelope, validateSyncResult, validateConfigSummary, validateConfigCatalog, validateAgentBindingUpdate, validateDefaultRuntimeUpdate, validateRuntimeProfileMutation, validateReadinessCheck, validateFeatureFlagUpdate, validateOutputLanguage, validateGovernanceQueue, validateActivations, validateDisableActivation, validateLifecycleMetrics, validateUpdateStatus, validateUpdateHistory, validateApplyUpdateResult, validateRollbackResult, validateApprovalRecordDirect, validatePrinciplesList, validateApprovalsGrouped, validateEvidenceChain, validateTrajectoryData, validateIntentSummary, validateIntentDecisionList, validateIntentDecisionResult, validateIntentDecisionSummary, validateFollowUpResponse, validateIntentInitResult, validateIntentSaveResult, validateIntentRawContent, validateIntentVersions, } from "./utils/validators.js";
3
3
  // ── Auth ──────────────────────────────────────────────────────────────────────
4
4
  function getToken() {
5
5
  return sessionStorage.getItem("pd_token");
@@ -182,6 +182,27 @@ async function deleteFeedbackReport(id) {
182
182
  method: 'DELETE',
183
183
  }, validateDeleteEnvelope);
184
184
  }
185
+ // ── Feedback submit ladder (Slice 3, spec §8) ─────────────────────────────────
186
+ async function fetchFeedbackChannels() {
187
+ return request('/api/feedback/submit/channels', undefined, validateFeedbackChannels);
188
+ }
189
+ async function submitFeedbackReport(id, channel) {
190
+ return request('/api/feedback/reports/' + encodeURIComponent(id) + '/submit', {
191
+ method: 'POST',
192
+ body: JSON.stringify({ channel }),
193
+ }, validateFeedbackSubmitResult);
194
+ }
195
+ /**
196
+ * Manually mark a draft as sent (mailto/export channels, spec §11.4). The
197
+ * console cannot server-confirm delivery for these channels, so this is an
198
+ * honest user-declared status write-back — never a fake acknowledgement.
199
+ */
200
+ async function markFeedbackReportSent(id, via) {
201
+ return request('/api/feedback/reports/' + encodeURIComponent(id) + '/mark-sent', {
202
+ method: 'POST',
203
+ body: JSON.stringify({ via }),
204
+ }, validateFeedbackSubmitResult);
205
+ }
185
206
  // ── Config / Control Center API (PRI-303, PRI-309) ───────────────────────────
186
207
  async function fetchConfigSummary() {
187
208
  return request('/api/v1/config/summary', undefined, validateConfigSummary);
@@ -381,4 +402,6 @@ export { request,
381
402
  // signal-keywords (UI stubs, see signal-keywords-api.ts)
382
403
  listActiveSignalKeywords, listPendingSignalTerms, fetchKeywordStore, fetchPendingTerms, updateKeywordStore, admitPendingTerm, rejectPendingTerm,
383
404
  // auth
384
- getToken, setToken, clearToken, checkAuth, approveApproval, rejectApproval, editApproval, fetchPrinciples, fetchPrincipleDetail, fetchPrincipleTrajectory, archivePrinciple, unarchivePrinciple, createFeedbackReport, listFeedbackReports, getFeedbackReport, deleteFeedbackReport, fetchConfigSummary, fetchConfigCatalog, updateAgentBinding, updateDefaultRuntime, createRuntimeProfile, updateRuntimeProfile, deleteRuntimeProfile, fetchAgentReadiness, patchFeatureFlag, fetchOutputLanguage, updateOutputLanguage, fetchWorkspaces, addWorkspace, removeWorkspace, syncWorkspace, fetchGovernanceQueue, fetchApprovalsGrouped, fetchAllActivations, disableActivation, fetchLifecycleMetrics, fetchUpdateStatus, fetchUpdateHistory, applyUpdate, applyFullUpdate, rollbackUpdate, fetchEvidenceChain, fetchIntentSummary, fetchIntentContent, createIntentTemplate, saveIntentContent, fetchIntentVersions, recordIntentDecision, listIntentDecisionsByPainId, listIntentDecisionsByTaskId, fetchIntentDecisionSummary, dispatchFollowUp, };
405
+ getToken, setToken, clearToken, checkAuth, approveApproval, rejectApproval, editApproval, fetchPrinciples, fetchPrincipleDetail, fetchPrincipleTrajectory, archivePrinciple, unarchivePrinciple, createFeedbackReport, listFeedbackReports, getFeedbackReport, deleteFeedbackReport,
406
+ // Feedback submit ladder (Slice 3, spec §8)
407
+ fetchFeedbackChannels, submitFeedbackReport, markFeedbackReportSent, fetchConfigSummary, fetchConfigCatalog, updateAgentBinding, updateDefaultRuntime, createRuntimeProfile, updateRuntimeProfile, deleteRuntimeProfile, fetchAgentReadiness, patchFeatureFlag, fetchOutputLanguage, updateOutputLanguage, fetchWorkspaces, addWorkspace, removeWorkspace, syncWorkspace, fetchGovernanceQueue, fetchApprovalsGrouped, fetchAllActivations, disableActivation, fetchLifecycleMetrics, fetchUpdateStatus, fetchUpdateHistory, applyUpdate, applyFullUpdate, rollbackUpdate, fetchEvidenceChain, fetchIntentSummary, fetchIntentContent, createIntentTemplate, saveIntentContent, fetchIntentVersions, recordIntentDecision, listIntentDecisionsByPainId, listIntentDecisionsByTaskId, fetchIntentDecisionSummary, dispatchFollowUp, };