create-principles-disciple 1.145.25 → 1.145.27

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 (191) hide show
  1. package/_release/product-identity.json +1 -1
  2. package/console/dist/server/config/pd-config-store.d.ts +1 -0
  3. package/console/dist/server/config/pd-config-store.js +28 -6
  4. package/console/dist/server/models/RuleCandidateBindingConsoleModel.d.ts +26 -0
  5. package/console/dist/server/models/RuleCandidateBindingConsoleModel.js +309 -19
  6. package/console/dist/server/routes/config.js +7 -3
  7. package/console/dist/server/routes/rule-candidate-bindings.js +26 -1
  8. package/console/dist/shared/rule-candidate-binding-contract.d.ts +93 -8
  9. package/console/dist/shared/rule-candidate-binding-contract.js +4 -0
  10. package/console/dist/ui/api.d.ts +28 -0
  11. package/console/dist/ui/api.js +13 -1
  12. package/console/dist/ui/i18n/en.json +93 -25
  13. package/console/dist/ui/i18n/zh-CN.json +79 -11
  14. package/console/dist/ui/pages/control-center/RuntimeProfileManager.js +32 -67
  15. package/console/dist/ui/pages/focus/RuleCandidateBindingPanel.d.ts +29 -0
  16. package/console/dist/ui/pages/focus/RuleCandidateBindingPanel.js +229 -3
  17. package/console/dist/ui/utils/rule-binding-validators.d.ts +5 -0
  18. package/console/dist/ui/utils/rule-binding-validators.js +149 -7
  19. package/console/dist/web/assets/app.css +6 -0
  20. package/console/dist/web/assets/app.js +32 -32
  21. package/core/dist/runtime-v2/adapter/index.d.ts +0 -2
  22. package/core/dist/runtime-v2/adapter/index.d.ts.map +1 -1
  23. package/core/dist/runtime-v2/adapter/index.js +0 -1
  24. package/core/dist/runtime-v2/adapter/index.js.map +1 -1
  25. package/core/dist/runtime-v2/adapter/output-schema-registry.d.ts +3 -3
  26. package/core/dist/runtime-v2/adapter/pi-ai-runtime-adapter.d.ts.map +1 -1
  27. package/core/dist/runtime-v2/adapter/pi-ai-runtime-adapter.js +1 -2
  28. package/core/dist/runtime-v2/adapter/pi-ai-runtime-adapter.js.map +1 -1
  29. package/core/dist/runtime-v2/cli/probe.d.ts +4 -15
  30. package/core/dist/runtime-v2/cli/probe.d.ts.map +1 -1
  31. package/core/dist/runtime-v2/cli/probe.js +24 -44
  32. package/core/dist/runtime-v2/cli/probe.js.map +1 -1
  33. package/core/dist/runtime-v2/config/index.d.ts +1 -1
  34. package/core/dist/runtime-v2/config/index.d.ts.map +1 -1
  35. package/core/dist/runtime-v2/config/index.js.map +1 -1
  36. package/core/dist/runtime-v2/config/pd-config-agent-binding.d.ts +10 -17
  37. package/core/dist/runtime-v2/config/pd-config-agent-binding.d.ts.map +1 -1
  38. package/core/dist/runtime-v2/config/pd-config-agent-binding.js +23 -27
  39. package/core/dist/runtime-v2/config/pd-config-agent-binding.js.map +1 -1
  40. package/core/dist/runtime-v2/config/pd-config-defaults.d.ts +3 -5
  41. package/core/dist/runtime-v2/config/pd-config-defaults.d.ts.map +1 -1
  42. package/core/dist/runtime-v2/config/pd-config-defaults.js +17 -20
  43. package/core/dist/runtime-v2/config/pd-config-defaults.js.map +1 -1
  44. package/core/dist/runtime-v2/config/pd-config-effective.d.ts.map +1 -1
  45. package/core/dist/runtime-v2/config/pd-config-effective.js +4 -1
  46. package/core/dist/runtime-v2/config/pd-config-effective.js.map +1 -1
  47. package/core/dist/runtime-v2/config/pd-config-redaction.d.ts.map +1 -1
  48. package/core/dist/runtime-v2/config/pd-config-redaction.js +4 -7
  49. package/core/dist/runtime-v2/config/pd-config-redaction.js.map +1 -1
  50. package/core/dist/runtime-v2/config/pd-config-types.d.ts +5 -1
  51. package/core/dist/runtime-v2/config/pd-config-types.d.ts.map +1 -1
  52. package/core/dist/runtime-v2/config/pd-config-types.js +6 -0
  53. package/core/dist/runtime-v2/config/pd-config-types.js.map +1 -1
  54. package/core/dist/runtime-v2/config/pd-config-validate.d.ts.map +1 -1
  55. package/core/dist/runtime-v2/config/pd-config-validate.js +6 -0
  56. package/core/dist/runtime-v2/config/pd-config-validate.js.map +1 -1
  57. package/core/dist/runtime-v2/golden-trace.d.ts.map +1 -1
  58. package/core/dist/runtime-v2/golden-trace.js +8 -1
  59. package/core/dist/runtime-v2/golden-trace.js.map +1 -1
  60. package/core/dist/runtime-v2/index.d.ts +4 -4
  61. package/core/dist/runtime-v2/index.d.ts.map +1 -1
  62. package/core/dist/runtime-v2/index.js +2 -3
  63. package/core/dist/runtime-v2/index.js.map +1 -1
  64. package/core/dist/runtime-v2/internalization/artificer-intent-preflight.d.ts.map +1 -1
  65. package/core/dist/runtime-v2/internalization/artificer-intent-preflight.js +26 -95
  66. package/core/dist/runtime-v2/internalization/artificer-intent-preflight.js.map +1 -1
  67. package/core/dist/runtime-v2/internalization/bash-risk.d.ts +32 -0
  68. package/core/dist/runtime-v2/internalization/bash-risk.d.ts.map +1 -0
  69. package/core/dist/runtime-v2/internalization/bash-risk.js +785 -0
  70. package/core/dist/runtime-v2/internalization/bash-risk.js.map +1 -0
  71. package/core/dist/runtime-v2/internalization/evaluator-prompt-builder.d.ts +25 -2
  72. package/core/dist/runtime-v2/internalization/evaluator-prompt-builder.d.ts.map +1 -1
  73. package/core/dist/runtime-v2/internalization/evaluator-prompt-builder.js +177 -14
  74. package/core/dist/runtime-v2/internalization/evaluator-prompt-builder.js.map +1 -1
  75. package/core/dist/runtime-v2/internalization/evaluator-runner.d.ts.map +1 -1
  76. package/core/dist/runtime-v2/internalization/evaluator-runner.js +14 -2
  77. package/core/dist/runtime-v2/internalization/evaluator-runner.js.map +1 -1
  78. package/core/dist/runtime-v2/internalization/internalization-orchestrator.d.ts +47 -0
  79. package/core/dist/runtime-v2/internalization/internalization-orchestrator.d.ts.map +1 -1
  80. package/core/dist/runtime-v2/internalization/internalization-orchestrator.js +164 -3
  81. package/core/dist/runtime-v2/internalization/internalization-orchestrator.js.map +1 -1
  82. package/core/dist/runtime-v2/internalization/peer-runner-contracts.d.ts +9 -1
  83. package/core/dist/runtime-v2/internalization/peer-runner-contracts.d.ts.map +1 -1
  84. package/core/dist/runtime-v2/internalization/peer-runner-contracts.js.map +1 -1
  85. package/core/dist/runtime-v2/internalization/pitask-metadata.d.ts +39 -0
  86. package/core/dist/runtime-v2/internalization/pitask-metadata.d.ts.map +1 -1
  87. package/core/dist/runtime-v2/internalization/pitask-metadata.js +59 -0
  88. package/core/dist/runtime-v2/internalization/pitask-metadata.js.map +1 -1
  89. package/core/dist/runtime-v2/internalization/rule-source-correction.d.ts +60 -0
  90. package/core/dist/runtime-v2/internalization/rule-source-correction.d.ts.map +1 -0
  91. package/core/dist/runtime-v2/internalization/rule-source-correction.js +214 -0
  92. package/core/dist/runtime-v2/internalization/rule-source-correction.js.map +1 -0
  93. package/core/dist/runtime-v2/internalization/rulecode-verification-obligations.d.ts.map +1 -1
  94. package/core/dist/runtime-v2/internalization/rulecode-verification-obligations.js +65 -0
  95. package/core/dist/runtime-v2/internalization/rulecode-verification-obligations.js.map +1 -1
  96. package/core/dist/runtime-v2/internalization/scribe-output.d.ts +14 -2
  97. package/core/dist/runtime-v2/internalization/scribe-output.d.ts.map +1 -1
  98. package/core/dist/runtime-v2/internalization/scribe-output.js +57 -2
  99. package/core/dist/runtime-v2/internalization/scribe-output.js.map +1 -1
  100. package/core/dist/runtime-v2/internalization/scribe-prompt-builder.d.ts +13 -1
  101. package/core/dist/runtime-v2/internalization/scribe-prompt-builder.d.ts.map +1 -1
  102. package/core/dist/runtime-v2/internalization/scribe-prompt-builder.js +15 -3
  103. package/core/dist/runtime-v2/internalization/scribe-prompt-builder.js.map +1 -1
  104. package/core/dist/runtime-v2/internalization/scribe-runner.d.ts +34 -0
  105. package/core/dist/runtime-v2/internalization/scribe-runner.d.ts.map +1 -1
  106. package/core/dist/runtime-v2/internalization/scribe-runner.js +128 -4
  107. package/core/dist/runtime-v2/internalization/scribe-runner.js.map +1 -1
  108. package/core/dist/runtime-v2/internalization/textual-basis-anchor.d.ts +72 -0
  109. package/core/dist/runtime-v2/internalization/textual-basis-anchor.d.ts.map +1 -0
  110. package/core/dist/runtime-v2/internalization/textual-basis-anchor.js +100 -0
  111. package/core/dist/runtime-v2/internalization/textual-basis-anchor.js.map +1 -0
  112. package/core/dist/runtime-v2/pain-signal-runtime-factory.d.ts +9 -7
  113. package/core/dist/runtime-v2/pain-signal-runtime-factory.d.ts.map +1 -1
  114. package/core/dist/runtime-v2/pain-signal-runtime-factory.js +59 -121
  115. package/core/dist/runtime-v2/pain-signal-runtime-factory.js.map +1 -1
  116. package/core/dist/runtime-v2/runtime-protocol.d.ts +2 -3
  117. package/core/dist/runtime-v2/runtime-protocol.d.ts.map +1 -1
  118. package/core/dist/runtime-v2/runtime-protocol.js +15 -10
  119. package/core/dist/runtime-v2/runtime-protocol.js.map +1 -1
  120. package/core/dist/runtime-v2/store/lifecycle/lease-manager.d.ts.map +1 -1
  121. package/core/dist/runtime-v2/store/lifecycle/lease-manager.js +11 -2
  122. package/core/dist/runtime-v2/store/lifecycle/lease-manager.js.map +1 -1
  123. package/core/dist/runtime-v2/tools/artificer-l2-tool-contract.d.ts.map +1 -1
  124. package/core/dist/runtime-v2/tools/artificer-l2-tool-contract.js +26 -0
  125. package/core/dist/runtime-v2/tools/artificer-l2-tool-contract.js.map +1 -1
  126. package/core/dist/telemetry-event.d.ts +2 -2
  127. package/core/dist/telemetry-event.d.ts.map +1 -1
  128. package/core/dist/telemetry-event.js +5 -0
  129. package/core/dist/telemetry-event.js.map +1 -1
  130. package/core/dist/workflow-funnel-loader.d.ts +0 -2
  131. package/core/dist/workflow-funnel-loader.d.ts.map +1 -1
  132. package/core/dist/workflow-funnel-loader.js.map +1 -1
  133. package/core/package.json +5 -1
  134. package/dist/mvp-config.d.ts.map +1 -1
  135. package/dist/mvp-config.js +8 -10
  136. package/dist/mvp-config.js.map +1 -1
  137. package/host-runtime/dist/internalization-consumer-cycle.js +4 -7
  138. package/host-runtime/dist/production-rulehost-gate.js +11 -1
  139. package/host-runtime/package.json +1 -1
  140. package/package.json +2 -2
  141. package/pd-cli/dist/commands/diagnose.d.ts +0 -3
  142. package/pd-cli/dist/commands/diagnose.d.ts.map +1 -1
  143. package/pd-cli/dist/commands/diagnose.js +20 -57
  144. package/pd-cli/dist/commands/diagnose.js.map +1 -1
  145. package/pd-cli/dist/commands/pain-retry.d.ts +0 -3
  146. package/pd-cli/dist/commands/pain-retry.d.ts.map +1 -1
  147. package/pd-cli/dist/commands/pain-retry.js +11 -39
  148. package/pd-cli/dist/commands/pain-retry.js.map +1 -1
  149. package/pd-cli/dist/commands/runtime-internalization-enqueue-successors.d.ts.map +1 -1
  150. package/pd-cli/dist/commands/runtime-internalization-enqueue-successors.js +32 -0
  151. package/pd-cli/dist/commands/runtime-internalization-enqueue-successors.js.map +1 -1
  152. package/pd-cli/dist/commands/runtime-internalization-run-once.d.ts.map +1 -1
  153. package/pd-cli/dist/commands/runtime-internalization-run-once.js +4 -2
  154. package/pd-cli/dist/commands/runtime-internalization-run-once.js.map +1 -1
  155. package/pd-cli/dist/commands/runtime.d.ts +1 -4
  156. package/pd-cli/dist/commands/runtime.d.ts.map +1 -1
  157. package/pd-cli/dist/commands/runtime.js +25 -95
  158. package/pd-cli/dist/commands/runtime.js.map +1 -1
  159. package/pd-cli/dist/index.js +2 -5
  160. package/pd-cli/dist/index.js.map +1 -1
  161. package/pd-cli/dist/services/config-doctor.d.ts.map +1 -1
  162. package/pd-cli/dist/services/config-doctor.js +9 -21
  163. package/pd-cli/dist/services/config-doctor.js.map +1 -1
  164. package/pd-cli/dist/services/mainline-snapshot-assembler.d.ts.map +1 -1
  165. package/pd-cli/dist/services/mainline-snapshot-assembler.js +2 -2
  166. package/pd-cli/dist/services/mainline-snapshot-assembler.js.map +1 -1
  167. package/pd-cli/dist/services/resolve-runtime-from-pd-config.d.ts +0 -7
  168. package/pd-cli/dist/services/resolve-runtime-from-pd-config.d.ts.map +1 -1
  169. package/pd-cli/dist/services/resolve-runtime-from-pd-config.js +0 -5
  170. package/pd-cli/dist/services/resolve-runtime-from-pd-config.js.map +1 -1
  171. package/pd-cli/dist/services/runtime-adapter-resolver.d.ts +1 -8
  172. package/pd-cli/dist/services/runtime-adapter-resolver.d.ts.map +1 -1
  173. package/pd-cli/dist/services/runtime-adapter-resolver.js +13 -39
  174. package/pd-cli/dist/services/runtime-adapter-resolver.js.map +1 -1
  175. package/pd-cli/package.json +1 -1
  176. package/plugin/dist/bundle.js +468 -458
  177. package/plugin/dist/openclaw.plugin.json +1 -1
  178. package/plugin/openclaw.plugin.json +1 -1
  179. package/plugin/package.json +1 -1
  180. package/release-manager/dist/mvp-config.d.ts.map +1 -1
  181. package/release-manager/dist/mvp-config.js +8 -10
  182. package/release-manager/dist/mvp-config.js.map +1 -1
  183. package/release-manager/package.json +2 -2
  184. package/core/dist/runtime-v2/adapter/openclaw-cli-runtime-adapter.d.ts +0 -72
  185. package/core/dist/runtime-v2/adapter/openclaw-cli-runtime-adapter.d.ts.map +0 -1
  186. package/core/dist/runtime-v2/adapter/openclaw-cli-runtime-adapter.js +0 -837
  187. package/core/dist/runtime-v2/adapter/openclaw-cli-runtime-adapter.js.map +0 -1
  188. package/core/dist/runtime-v2/utils/cli-process-runner.d.ts +0 -35
  189. package/core/dist/runtime-v2/utils/cli-process-runner.d.ts.map +0 -1
  190. package/core/dist/runtime-v2/utils/cli-process-runner.js +0 -263
  191. package/core/dist/runtime-v2/utils/cli-process-runner.js.map +0 -1
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
3
  "productVersion": "2.3.13",
4
- "sourceCommit": "8699502b4558837c908be45d3b71190335645a54"
4
+ "sourceCommit": "0abb21264600cd66a57c7960cd089cf854f0768b"
5
5
  }
@@ -247,6 +247,7 @@ export declare function createRuntimeProfile(workspaceDir: string, profileId: st
247
247
  * Update an existing runtime profile in .pd/config.yaml.
248
248
  *
249
249
  * - Rejects non-existent profile (404 not_found)
250
+ * - Rejects retired legacy profiles (400 retired_profile_type — read-only per ADR-0029)
250
251
  * - Rejects type changes (400 bad_request — type change = delete + create)
251
252
  * - Merges patch into existing profile, only known fields are persisted
252
253
  * - Persists atomically, preserving unknown config sections
@@ -1083,6 +1083,17 @@ export function createRuntimeProfile(workspaceDir, profileId, payload) {
1083
1083
  if (!structResult.ok) {
1084
1084
  return { ok: false, statusCode: 400, error: structResult.error, message: structResult.message };
1085
1085
  }
1086
+ // ADR-0029: the 'openclaw' profile type is read-only legacy. Existing legacy
1087
+ // profiles keep parsing and stay listed, but PD must never write a new one.
1088
+ if (structResult.type === 'openclaw') {
1089
+ return {
1090
+ ok: false,
1091
+ statusCode: 400,
1092
+ error: 'retired_profile_type',
1093
+ message: `Runtime profile type 'openclaw' is retired (ADR-0029) and cannot be created.`,
1094
+ nextAction: 'Create a pi-ai profile instead (provider + model + apiKeyEnv).',
1095
+ };
1096
+ }
1086
1097
  // 3. Load existing config — refuse to write on malformed (rc-9)
1087
1098
  const loadResult = loadPdConfig(workspaceDir);
1088
1099
  if (!loadResult.ok) {
@@ -1151,6 +1162,7 @@ export function createRuntimeProfile(workspaceDir, profileId, payload) {
1151
1162
  * Update an existing runtime profile in .pd/config.yaml.
1152
1163
  *
1153
1164
  * - Rejects non-existent profile (404 not_found)
1165
+ * - Rejects retired legacy profiles (400 retired_profile_type — read-only per ADR-0029)
1154
1166
  * - Rejects type changes (400 bad_request — type change = delete + create)
1155
1167
  * - Merges patch into existing profile, only known fields are persisted
1156
1168
  * - Persists atomically, preserving unknown config sections
@@ -1194,7 +1206,17 @@ export function updateRuntimeProfile(workspaceDir, profileId, patch) {
1194
1206
  message: `Profile '${profileId}' existence check passed but value is undefined.`,
1195
1207
  nextAction: 'This is a bug in PD config resolution. Please report it.' };
1196
1208
  }
1197
- // 5. Reject type change (type change = delete + create)
1209
+ // 5. Reject retired legacy profiles — ADR-0029: accepted on read, never written by PD
1210
+ if (existingProfile.type === 'openclaw') {
1211
+ return {
1212
+ ok: false,
1213
+ statusCode: 400,
1214
+ error: 'retired_profile_type',
1215
+ message: `Runtime profile type 'openclaw' is retired (ADR-0029) and cannot be updated. The profile stays readable for legacy compatibility only.`,
1216
+ nextAction: `DELETE /api/v1/config/profiles/${encodeURIComponent(profileId)} and create a pi-ai profile instead (provider + model + apiKeyEnv)`,
1217
+ };
1218
+ }
1219
+ // 6. Reject type change (type change = delete + create)
1198
1220
  if (Object.hasOwn(patch, 'type')) {
1199
1221
  const patchType = patch.type;
1200
1222
  if (typeof patchType !== 'string' || patchType !== existingProfile.type) {
@@ -1207,10 +1229,10 @@ export function updateRuntimeProfile(workspaceDir, profileId, patch) {
1207
1229
  };
1208
1230
  }
1209
1231
  }
1210
- // 6. Merge patch into existing profile, then extract only known fields
1232
+ // 7. Merge patch into existing profile, then extract only known fields
1211
1233
  const mergedSource = { ...existingProfile, ...patch };
1212
1234
  const mergedRecord = extractProfileFields(mergedSource, existingProfile.type);
1213
- // 7. Read raw config to preserve unknown sections, update the profile
1235
+ // 8. Read raw config to preserve unknown sections, update the profile
1214
1236
  const configPath = getPdConfigPath(workspaceDir);
1215
1237
  const rawConfig = readRawConfig(configPath);
1216
1238
  const profilesMap = isRecord(rawConfig.runtimeProfiles)
@@ -1218,7 +1240,7 @@ export function updateRuntimeProfile(workspaceDir, profileId, patch) {
1218
1240
  : { ...effective.config.runtimeProfiles };
1219
1241
  profilesMap[profileId] = mergedRecord;
1220
1242
  rawConfig.runtimeProfiles = profilesMap;
1221
- // 8. Validate the full updated config
1243
+ // 9. Validate the full updated config
1222
1244
  const validation = validatePdConfig(rawConfig);
1223
1245
  if (!validation.ok) {
1224
1246
  return {
@@ -1229,7 +1251,7 @@ export function updateRuntimeProfile(workspaceDir, profileId, patch) {
1229
1251
  nextAction: 'Fix profile fields and retry',
1230
1252
  };
1231
1253
  }
1232
- // 9. Atomic write
1254
+ // 10. Atomic write
1233
1255
  try {
1234
1256
  writeConfigAtomic(configPath, rawConfig);
1235
1257
  }
@@ -1237,7 +1259,7 @@ export function updateRuntimeProfile(workspaceDir, profileId, patch) {
1237
1259
  const message = err instanceof Error ? err.message : String(err);
1238
1260
  return { ok: false, statusCode: 500, error: 'write_error', message: `Failed to write config: ${message}`, nextAction: 'Check disk space and file permissions for .pd/config.yaml' };
1239
1261
  }
1240
- // 10. Return the validated updated profile
1262
+ // 11. Return the validated updated profile
1241
1263
  // Defensive guard: Object.hasOwn() doesn't narrow TS type.
1242
1264
  const updatedProfile = validation.value.runtimeProfiles[profileId];
1243
1265
  if (!updatedProfile) {
@@ -7,6 +7,19 @@ export declare class RuleCandidateBindingConsoleModel {
7
7
  private readonly workspaceDir;
8
8
  constructor(workspaceDir: string);
9
9
  private ledger;
10
+ /** One bounded readonly batch read of the pain events behind the queue.
11
+ * `degraded` names an unreadable trajectory store so lookups that found
12
+ * nothing are labeled 不可读 rather than 未记录 (rc-9 — never misattribute). */
13
+ private painSources;
14
+ /** Identity stays with the ledger adapter (the identity authority); the
15
+ * principle entry / decidedAt come from the caller's single file snapshot.
16
+ * bindingRevision rides along so the source-correction preview can anchor
17
+ * its eligibility to the CURRENT binding (PRI-1048 R6 #4). */
18
+ private queueBinding;
19
+ /** PRI-1048 R6 #4: read-only eligibility preview for source-correct-once.
20
+ * Only the bound+blocked shape can offer it; the click-time authority
21
+ * (reviseRuleSourceOnce) re-checks everything server-side. */
22
+ private sourceCorrectionPreview;
10
23
  list(): Promise<RuleBindingQueueView>;
11
24
  review(candidateId: string, search: string): Promise<{
12
25
  ok: true;
@@ -37,4 +50,17 @@ export declare class RuleCandidateBindingConsoleModel {
37
50
  scribeTaskId: string;
38
51
  taskRevisionEpoch: number;
39
52
  }>;
53
+ /**
54
+ * SPEC-PRI-1047 修复 B: 认证入口的一次 Owner source-correction。区别于
55
+ * revise(binding 修订预算):这是「修正上游材料」的独立一次预算,回执
56
+ * 绑定真实 Owner actor/request。调用侧(路由)负责 ownerIdentity 门。
57
+ */
58
+ correctSource(candidateId: string, input: {
59
+ expectedBindingRevision: number;
60
+ expectedScribeTaskId: string;
61
+ expectedArtificerTaskId: string;
62
+ expectedScribeArtifactDigest: string;
63
+ reason: string;
64
+ requestId: string;
65
+ }, actorId: string): Promise<import("@principles/core/runtime-v2").ReviseRuleSourceOnceResult>;
40
66
  }
@@ -1,6 +1,8 @@
1
1
  import path from 'node:path';
2
2
  import { existsSync } from 'node:fs';
3
- import { createRuntimeStateHandle, PrincipleTreeLedgerAdapter, reviseRuleBindingOnce, } from '@principles/core/runtime-v2';
3
+ import Database from 'better-sqlite3';
4
+ import { createRuntimeStateHandle, PrincipleTreeLedgerAdapter, reviseRuleBindingOnce, resolveRuleBindingRevisionTarget, reviseRuleSourceOnce, previewRuleSourceCorrection, hydratePITaskRecord, candidateIdFromDreamerSeed, } from '@principles/core/runtime-v2';
5
+ import { readLedgerFileState } from '@principles/core/principle-tree-ledger';
4
6
  import { ActivationsConsoleModel } from './ActivationsConsoleModel.js';
5
7
  function record(value) {
6
8
  return value !== null && typeof value === 'object' && !Array.isArray(value);
@@ -8,6 +10,108 @@ function record(value) {
8
10
  function stringField(value, key) {
9
11
  return record(value) && typeof value[key] === 'string' ? value[key] : '';
10
12
  }
13
+ /** Internalization chain order; the queue projection walks it read-only. */
14
+ const CHAIN_KINDS = ['dreamer', 'philosopher', 'scribe', 'artificer', 'evaluator', 'rollout_reviewer'];
15
+ /** PRI-1048: bounded queue — a governance workspace carries tens of candidates;
16
+ * the cap keeps the derived scan bounded and the response honest via `truncated`. */
17
+ const MAX_QUEUE_ITEMS = 200;
18
+ function buildChainContext(allTasks) {
19
+ const byId = new Map(allTasks.map(task => [task.taskId, task]));
20
+ const dreamersByCandidate = new Map();
21
+ const dependentsByTaskId = new Map();
22
+ for (const task of allTasks) {
23
+ if (task.taskKind === 'dreamer') {
24
+ const seed = candidateIdFromDreamerSeed(task.diagnosticJson ?? null);
25
+ if (seed) {
26
+ const list = dreamersByCandidate.get(seed) ?? [];
27
+ list.push(task);
28
+ dreamersByCandidate.set(seed, list);
29
+ }
30
+ }
31
+ const pi = hydratePITaskRecord(task);
32
+ if (!pi)
33
+ continue;
34
+ for (const dependencyId of pi.dependencyTaskIds) {
35
+ const list = dependentsByTaskId.get(dependencyId) ?? [];
36
+ list.push(task);
37
+ dependentsByTaskId.set(dependencyId, list);
38
+ }
39
+ }
40
+ return { byId, dreamersByCandidate, dependentsByTaskId };
41
+ }
42
+ function walkChain(dreamer, context) {
43
+ let current = dreamer;
44
+ let kind = 'dreamer';
45
+ for (;;) {
46
+ if (!current)
47
+ return { stage: 'advanced', stageKind: kind, reasonCode: null, detail: null };
48
+ if (current.status === 'needs_human_review' || current.status === 'failed') {
49
+ const pi = hydratePITaskRecord(current);
50
+ const review = pi?.humanReviewContext;
51
+ return {
52
+ stage: 'blocked', stageKind: kind,
53
+ reasonCode: review && typeof review.reasonCode === 'string' ? review.reasonCode : null,
54
+ detail: review && typeof review.detail === 'string' ? review.detail : (current.lastError ?? null),
55
+ };
56
+ }
57
+ if (current.status !== 'succeeded')
58
+ return { stage: 'in_progress', stageKind: kind, reasonCode: null, detail: null };
59
+ const nextIndex = CHAIN_KINDS.indexOf(kind) + 1;
60
+ const nextKind = nextIndex < CHAIN_KINDS.length ? CHAIN_KINDS[nextIndex] : undefined;
61
+ if (!nextKind)
62
+ return { stage: 'advanced', stageKind: kind, reasonCode: null, detail: null };
63
+ const next = (context.dependentsByTaskId.get(current.taskId) ?? []).find(task => task.taskKind === nextKind);
64
+ if (!next)
65
+ return { stage: 'advanced', stageKind: kind, reasonCode: null, detail: null };
66
+ current = next;
67
+ kind = nextKind;
68
+ }
69
+ }
70
+ function deriveGeneration(candidateId, context) {
71
+ const dreamers = context.dreamersByCandidate.get(candidateId) ?? [];
72
+ if (dreamers.length === 0)
73
+ return { stage: 'not_built', stageKind: null, reasonCode: null, detail: null };
74
+ const walks = dreamers.map(dreamer => walkChain(dreamer, context));
75
+ const [first] = walks;
76
+ if (!first)
77
+ return { stage: 'ambiguous', stageKind: null, reasonCode: null, detail: null };
78
+ const sameWalk = (a, b) => a.stage === b.stage && a.stageKind === b.stageKind && a.reasonCode === b.reasonCode;
79
+ if (walks.every(walk => sameWalk(walk, first)))
80
+ return first;
81
+ return { stage: 'ambiguous', stageKind: null, reasonCode: null, detail: null };
82
+ }
83
+ /**
84
+ * Real backend eligibility only: the production resolver plus the exact
85
+ * budget rule reviseRuleBindingOnce enforces on the same scribe task. The
86
+ * revise-once route still authoritatively re-checks at click time.
87
+ */
88
+ async function deriveResume(stateManager, candidateId) {
89
+ const target = await resolveRuleBindingRevisionTarget(stateManager, candidateId);
90
+ if (target.status !== 'ready')
91
+ return { status: 'refused', reason: target.reason, scribeTaskId: null, taskRevisionEpoch: null };
92
+ const scribe = await stateManager.getTask(target.scribeTaskId);
93
+ const pi = scribe ? hydratePITaskRecord(scribe) : null;
94
+ if (!scribe || !pi)
95
+ return { status: 'refused', reason: 'scribe_missing', scribeTaskId: target.scribeTaskId, taskRevisionEpoch: target.taskRevisionEpoch };
96
+ if (pi.ruleBindingRevisionCauseId || pi.revisionCauseId?.startsWith('rule-binding:')) {
97
+ return { status: 'refused', reason: 'revision_budget_exhausted', scribeTaskId: target.scribeTaskId, taskRevisionEpoch: target.taskRevisionEpoch };
98
+ }
99
+ return { status: 'ready', reason: null, scribeTaskId: target.scribeTaskId, taskRevisionEpoch: target.taskRevisionEpoch };
100
+ }
101
+ /** Uses the caller's ONE ledger snapshot: decidedAt + principle entry, no extra file read. */
102
+ function boundPrincipleView(state, lookup) {
103
+ if (state.availability.status === 'unreadable')
104
+ return null;
105
+ const principle = Object.hasOwn(state.ledger.tree.principles, lookup.principleId) ? state.ledger.tree.principles[lookup.principleId] : undefined;
106
+ if (!principle)
107
+ return null;
108
+ const binding = Object.hasOwn(state.ledger.tree.ruleCandidateBindings ?? {}, lookup.candidateId)
109
+ ? state.ledger.tree.ruleCandidateBindings?.[lookup.candidateId]?.revisions.at(-1) : undefined;
110
+ return {
111
+ principleId: lookup.principleId, text: principle.text, status: principle.status,
112
+ version: principle.version, revision: lookup.revision, decidedAt: binding?.decidedAt ?? '',
113
+ };
114
+ }
11
115
  /** Derived review only. Binding authority stays inside the locked core Ledger writer. */
12
116
  export class RuleCandidateBindingConsoleModel {
13
117
  workspaceDir;
@@ -32,31 +136,172 @@ export class RuleCandidateBindingConsoleModel {
32
136
  },
33
137
  });
34
138
  }
139
+ /** One bounded readonly batch read of the pain events behind the queue.
140
+ * `degraded` names an unreadable trajectory store so lookups that found
141
+ * nothing are labeled 不可读 rather than 未记录 (rc-9 — never misattribute). */
142
+ painSources(painIds) {
143
+ const result = new Map();
144
+ const distinct = [...new Set(painIds.filter(id => id !== ''))];
145
+ if (distinct.length === 0)
146
+ return { sources: result, degraded: null };
147
+ const dbPath = path.join(this.workspaceDir, '.state', 'trajectory.db');
148
+ if (!existsSync(dbPath))
149
+ return { sources: result, degraded: null };
150
+ let db = null;
151
+ try {
152
+ db = new Database(dbPath, { readonly: true });
153
+ const placeholders = distinct.map(() => '?').join(',');
154
+ const rows = db.prepare(`SELECT canonical_pain_id AS painId, source, text, created_at AS createdAt
155
+ FROM pain_events WHERE canonical_pain_id IN (${placeholders})`).all(...distinct);
156
+ // Multiple events may share a canonical id; keep the newest (same policy
157
+ // as the pain list ordering — newest first).
158
+ for (const row of rows) {
159
+ if (!record(row))
160
+ continue;
161
+ const painId = typeof row.painId === 'string' ? row.painId : '';
162
+ if (painId === '')
163
+ continue;
164
+ const existing = result.get(painId);
165
+ const createdAt = typeof row.createdAt === 'string' ? row.createdAt : null;
166
+ if (existing && existing.createdAt && createdAt && existing.createdAt >= createdAt)
167
+ continue;
168
+ result.set(painId, {
169
+ painId,
170
+ text: typeof row.text === 'string' && row.text.trim() !== '' ? row.text : null,
171
+ source: typeof row.source === 'string' ? row.source : 'unknown',
172
+ createdAt,
173
+ });
174
+ }
175
+ }
176
+ catch {
177
+ // Read failure must not masquerade as never-recorded: the degraded flag
178
+ // turns missing lookups into 记录暂不可读 (rc-9 observable degradation).
179
+ return { sources: new Map(), degraded: 'trajectory_unreadable' };
180
+ }
181
+ finally {
182
+ db?.close();
183
+ }
184
+ return { sources: result, degraded: null };
185
+ }
186
+ /** Identity stays with the ledger adapter (the identity authority); the
187
+ * principle entry / decidedAt come from the caller's single file snapshot.
188
+ * bindingRevision rides along so the source-correction preview can anchor
189
+ * its eligibility to the CURRENT binding (PRI-1048 R6 #4). */
190
+ queueBinding(stateManager, ledgerState, candidateId) {
191
+ const identity = this.ledger(stateManager).resolveCandidatePrincipleIdentity(candidateId, 'rule');
192
+ if (identity.status === 'resolved') {
193
+ const view = boundPrincipleView(ledgerState, { candidateId, principleId: identity.principleId, revision: identity.bindingRevision ?? 0 });
194
+ if (view)
195
+ return { view: { status: 'bound', principle: view }, bindingRevision: identity.bindingRevision ?? null };
196
+ // Bound in the ledger but the principle entry itself is unreadable or
197
+ // gone — surface as attention, never silently unlinked.
198
+ return { view: { status: 'attention', reason: 'bound_principle_unreadable' }, bindingRevision: identity.bindingRevision ?? null };
199
+ }
200
+ if (identity.reason === 'missing')
201
+ return { view: { status: 'unlinked' }, bindingRevision: null };
202
+ if (identity.reason === 'binding_revoked')
203
+ return { view: { status: 'binding_revoked' }, bindingRevision: null };
204
+ return { view: { status: 'attention', reason: identity.reason }, bindingRevision: null };
205
+ }
206
+ /** PRI-1048 R6 #4: read-only eligibility preview for source-correct-once.
207
+ * Only the bound+blocked shape can offer it; the click-time authority
208
+ * (reviseRuleSourceOnce) re-checks everything server-side. */
209
+ async sourceCorrectionPreview(stateManager, candidateId, bindingRevision) {
210
+ const ledger = this.ledger(stateManager);
211
+ const preview = await previewRuleSourceCorrection({
212
+ stateManager,
213
+ ledger: { resolveForCandidate: async (id) => ledger.resolveCandidatePrincipleIdentity(id, 'rule') },
214
+ candidateId,
215
+ expectedBindingRevision: bindingRevision,
216
+ });
217
+ return preview.status === 'ready'
218
+ ? {
219
+ status: 'ready', reason: null,
220
+ scribeTaskId: preview.scribeTaskId, artificerTaskId: preview.artificerTaskId,
221
+ scribeArtifactDigest: preview.scribeArtifactDigest, expectedBindingRevision: preview.expectedBindingRevision,
222
+ }
223
+ : { status: 'unavailable', reason: preview.reason, scribeTaskId: null, artificerTaskId: null, scribeArtifactDigest: null, expectedBindingRevision: null };
224
+ }
35
225
  async list() {
36
226
  // A fresh workspace with no state.db simply has no binding queue yet;
37
227
  // any other storage problem still surfaces as an error (503 at the route).
38
- if (!existsSync(path.join(this.workspaceDir, '.pd', 'state.db')))
39
- return { items: [] };
228
+ if (!existsSync(path.join(this.workspaceDir, '.pd', 'state.db'))) {
229
+ return { items: [], summary: { painGroups: 0, candidates: 0, bound: 0, unlinked: 0, blocked: 0 }, truncated: false };
230
+ }
40
231
  const handle = await createRuntimeStateHandle({ workspaceDir: this.workspaceDir, readonly: true, bootstrapIfMissing: false });
41
232
  try {
42
- const ledger = this.ledger(handle.stateManager);
233
+ const { stateManager } = handle;
234
+ const allTasks = await stateManager.listTasks();
235
+ const taskById = new Map(allTasks.map(task => [task.taskId, task]));
43
236
  // Split diagnoses commit their rule candidates on diag_router sub-tasks;
44
- // the queue must surface both provenance shapes, not only direct
45
- // diagnostician commits.
46
- const tasks = [
47
- ...await handle.stateManager.listTasks({ taskKind: 'diagnostician' }),
48
- ...await handle.stateManager.listTasks({ taskKind: 'diag_router' }),
49
- ];
50
- const items = [];
51
- for (const task of tasks) {
52
- for (const candidate of await handle.stateManager.getCandidatesByTaskId(task.taskId)) {
237
+ // getCandidatesByTaskId already spans "own + child" tasks, so traversing
238
+ // BOTH task lists surfaces router-committed candidates twice. The queue
239
+ // aggregation owner (this model) dedupes by canonical candidateId; the
240
+ // shared store query keeps its semantics for every other consumer.
241
+ const diagnosisTasks = allTasks.filter(task => task.taskKind === 'diagnostician' || task.taskKind === 'diag_router');
242
+ const collected = new Map();
243
+ for (const task of diagnosisTasks) {
244
+ for (const candidate of await stateManager.getCandidatesByTaskId(task.taskId)) {
53
245
  if (candidate.rawRecommendationKind !== 'rule')
54
246
  continue;
55
- const identity = ledger.resolveCandidatePrincipleIdentity(candidate.candidateId, 'rule');
56
- items.push({ candidateId: candidate.candidateId, text: candidate.description || candidate.title, reason: identity.status === 'unresolved' ? identity.reason : 'bound' });
247
+ if (collected.has(candidate.candidateId))
248
+ continue;
249
+ // Source pain: a router candidate resolves through its parent
250
+ // diagnostician; a direct candidate reads its own inputRef.
251
+ const candidateTask = taskById.get(candidate.taskId) ?? task;
252
+ const parentTask = candidateTask.taskKind === 'diag_router' && candidateTask.inputRef
253
+ ? taskById.get(candidateTask.inputRef) : undefined;
254
+ const diagnosisTaskId = parentTask?.taskId ?? candidateTask.taskId;
255
+ const painId = (parentTask?.inputRef ?? candidateTask.inputRef) ?? '';
256
+ collected.set(candidate.candidateId, {
257
+ candidateId: candidate.candidateId,
258
+ text: candidate.description || candidate.title,
259
+ painId, diagnosisTaskId,
260
+ });
57
261
  }
58
262
  }
59
- return { items };
263
+ const truncated = collected.size > MAX_QUEUE_ITEMS;
264
+ const entries = [...collected.values()].slice(0, MAX_QUEUE_ITEMS);
265
+ const painLookup = this.painSources(entries.map(entry => entry.painId));
266
+ const chainContext = buildChainContext(allTasks);
267
+ const ledgerState = readLedgerFileState(path.join(this.workspaceDir, '.state'));
268
+ const items = [];
269
+ for (const entry of entries) {
270
+ const diagnosisTask = chainContext.byId.get(entry.diagnosisTaskId) ?? null;
271
+ const { view: binding, bindingRevision } = this.queueBinding(stateManager, ledgerState, entry.candidateId);
272
+ const generation = binding.status === 'unlinked' ? null : deriveGeneration(entry.candidateId, chainContext);
273
+ const resume = binding.status === 'bound' ? await deriveResume(stateManager, entry.candidateId) : null;
274
+ // PRI-1048 R6 #4: the source-correction entry applies to the bound+blocked
275
+ // shape only (the binding revision budget is typically already spent there).
276
+ const sourceCorrection = binding.status === 'bound' && generation?.stage === 'blocked' && bindingRevision !== null
277
+ ? await this.sourceCorrectionPreview(stateManager, entry.candidateId, bindingRevision)
278
+ : null;
279
+ items.push({
280
+ candidateId: entry.candidateId,
281
+ text: entry.text,
282
+ pain: painLookup.sources.get(entry.painId) ?? {
283
+ painId: entry.painId, text: null,
284
+ source: painLookup.degraded === 'trajectory_unreadable' && entry.painId !== '' ? 'unreadable' : 'unknown',
285
+ createdAt: null,
286
+ },
287
+ diagnosisStatus: diagnosisTask?.status ?? 'unknown',
288
+ binding,
289
+ generation,
290
+ resume,
291
+ sourceCorrection,
292
+ });
293
+ }
294
+ return {
295
+ items,
296
+ summary: {
297
+ painGroups: new Set(items.map(item => item.pain.painId === '' ? `candidate:${item.candidateId}` : item.pain.painId)).size,
298
+ candidates: items.length,
299
+ bound: items.filter(item => item.binding.status === 'bound').length,
300
+ unlinked: items.filter(item => item.binding.status === 'unlinked').length,
301
+ blocked: items.filter(item => item.generation?.stage === 'blocked').length,
302
+ },
303
+ truncated,
304
+ };
60
305
  }
61
306
  finally {
62
307
  await handle.close();
@@ -65,20 +310,44 @@ export class RuleCandidateBindingConsoleModel {
65
310
  async review(candidateId, search) {
66
311
  const handle = await createRuntimeStateHandle({ workspaceDir: this.workspaceDir, readonly: true, bootstrapIfMissing: false });
67
312
  try {
68
- const result = await this.ledger(handle.stateManager).createRuleCandidateBindingReview(candidateId, { search, limit: 20 });
313
+ const { stateManager } = handle;
314
+ const result = await this.ledger(stateManager).createRuleCandidateBindingReview(candidateId, { search, limit: 20 });
69
315
  if (!result.ok)
70
316
  return result;
71
317
  const { candidateMaterial: material } = result;
318
+ const parentTaskInputRef = stringField(Reflect.get(material, 'parentTask'), 'inputRef');
319
+ const painId = parentTaskInputRef || stringField(material.task, 'inputRef');
320
+ const painLookup = this.painSources([painId]);
321
+ const pain = painLookup.sources.get(painId) ?? {
322
+ painId, text: null,
323
+ source: painLookup.degraded === 'trajectory_unreadable' && painId !== '' ? 'unreadable' : 'unknown',
324
+ createdAt: null,
325
+ };
326
+ const { currentBinding } = result;
327
+ const isBound = currentBinding?.action === 'bind';
328
+ const boundPrinciple = isBound && currentBinding
329
+ ? boundPrincipleView(readLedgerFileState(path.join(this.workspaceDir, '.state')), { candidateId, principleId: currentBinding.principleId, revision: currentBinding.revision })
330
+ : null;
331
+ const chainContext = buildChainContext(await stateManager.listTasks());
332
+ const generation = isBound ? deriveGeneration(candidateId, chainContext) : null;
333
+ const resume = isBound ? await deriveResume(stateManager, candidateId) : null;
334
+ const sourceCorrection = isBound && generation?.stage === 'blocked' && currentBinding
335
+ ? await this.sourceCorrectionPreview(stateManager, candidateId, currentBinding.revision)
336
+ : null;
72
337
  return { ok: true, review: {
73
338
  candidateId, candidateText: stringField(material.candidate, 'description') || stringField(material.candidate, 'title'),
74
- sourcePainId: stringField(Reflect.get(material, 'parentTask'), 'inputRef') || stringField(material.task, 'inputRef'),
339
+ sourcePain: pain,
75
340
  evidence: stringField(material.artifact, 'contentJson') || stringField(material.candidate, 'sourceRecommendationJson'),
76
341
  candidateDigest: result.reviewedCandidateDigest,
77
342
  currentRevision: result.currentRevision,
78
- currentBinding: result.currentBinding,
343
+ currentBinding,
344
+ boundPrinciple,
79
345
  eligibleTargets: result.eligibleTargets.map(({ principle, reviewedPrincipleDigest }) => ({
80
346
  principleId: principle.id, text: principle.text, status: principle.status, version: principle.version, digest: reviewedPrincipleDigest,
81
347
  })),
348
+ generation,
349
+ resume,
350
+ sourceCorrection,
82
351
  resumeNotice: '关联后自动启动一次现有配置的正文修订与代码生成、验证;沿用现有调用预算和超时。失败会保留关联与旧工件,不会自动循环,也不会批准或启用规则。',
83
352
  } };
84
353
  }
@@ -127,4 +396,25 @@ export class RuleCandidateBindingConsoleModel {
127
396
  await handle.close();
128
397
  }
129
398
  }
399
+ /**
400
+ * SPEC-PRI-1047 修复 B: 认证入口的一次 Owner source-correction。区别于
401
+ * revise(binding 修订预算):这是「修正上游材料」的独立一次预算,回执
402
+ * 绑定真实 Owner actor/request。调用侧(路由)负责 ownerIdentity 门。
403
+ */
404
+ async correctSource(candidateId, input, actorId) {
405
+ const handle = await createRuntimeStateHandle({ workspaceDir: this.workspaceDir });
406
+ try {
407
+ const ledger = this.ledger(handle.stateManager);
408
+ return await reviseRuleSourceOnce({
409
+ stateManager: handle.stateManager,
410
+ ledger: { resolveForCandidate: async (id) => ledger.resolveCandidatePrincipleIdentity(id, 'rule') },
411
+ candidateId,
412
+ ...input,
413
+ actor: actorId,
414
+ });
415
+ }
416
+ finally {
417
+ await handle.close();
418
+ }
419
+ }
130
420
  }
@@ -53,6 +53,10 @@ function safeParseBody(text) {
53
53
  function isObject(value) {
54
54
  return typeof value === 'object' && value !== null && !Array.isArray(value);
55
55
  }
56
+ /** Runtime-profile CRUD errors carry an operator next action — never drop it (rc-9). */
57
+ function sendProfileCrudError(res, result) {
58
+ sendError(res, result.statusCode, result.error, result.message, result.nextAction ? { nextAction: result.nextAction } : undefined);
59
+ }
56
60
  // ── Route Handler ────────────────────────────────────────────────────────────
57
61
  export async function handleConfigRoute(req, res, ctx) {
58
62
  const { workspaceDir, subPath } = ctx;
@@ -301,7 +305,7 @@ export async function handleConfigRoute(req, res, ctx) {
301
305
  }
302
306
  const result = createRuntimeProfile(workspaceDir, body.id, body.profile);
303
307
  if (!result.ok) {
304
- sendError(res, result.statusCode, result.error, result.message);
308
+ sendProfileCrudError(res, result);
305
309
  return;
306
310
  }
307
311
  // 201 Created for new resource
@@ -335,7 +339,7 @@ export async function handleConfigRoute(req, res, ctx) {
335
339
  }
336
340
  const result = updateRuntimeProfile(workspaceDir, profileId, body);
337
341
  if (!result.ok) {
338
- sendError(res, result.statusCode, result.error, result.message);
342
+ sendProfileCrudError(res, result);
339
343
  return;
340
344
  }
341
345
  sendSuccess(res, { profileId: result.profileId, profile: result.profile });
@@ -344,7 +348,7 @@ export async function handleConfigRoute(req, res, ctx) {
344
348
  if (method === 'DELETE') {
345
349
  const result = deleteRuntimeProfile(workspaceDir, profileId);
346
350
  if (!result.ok) {
347
- sendError(res, result.statusCode, result.error, result.message);
351
+ sendProfileCrudError(res, result);
348
352
  return;
349
353
  }
350
354
  sendSuccess(res, { profileId: result.profileId, profile: result.profile });
@@ -43,7 +43,7 @@ export async function handleRuleCandidateBindingsRoute(req, res, ctx) {
43
43
  return;
44
44
  }
45
45
  const model = new RuleCandidateBindingConsoleModel(ctx.workspaceDir);
46
- const match = /^\/([^/]+)(?:\/(review|revise-once))?$/.exec(ctx.subPath);
46
+ const match = /^\/([^/]+)(?:\/(review|revise-once|source-correct-once))?$/.exec(ctx.subPath);
47
47
  const url = new URL(req.url ?? '/', 'http://localhost');
48
48
  try {
49
49
  if ((ctx.subPath === '' || ctx.subPath === '/') && req.method === 'GET') {
@@ -84,6 +84,31 @@ export async function handleRuleCandidateBindingsRoute(req, res, ctx) {
84
84
  sendBadRequest(res, '请求必须是有效的 JSON。');
85
85
  return;
86
86
  }
87
+ if (match[2] === 'source-correct-once') {
88
+ // SPEC-PRI-1047: one bounded Owner source-correction per candidate. Closed
89
+ // field set: chain identity + digest precondition + reason/requestId —
90
+ // never identity-fabrication or approval fields.
91
+ if (!closed(raw, ['expectedBindingRevision', 'expectedScribeTaskId', 'expectedArtificerTaskId', 'expectedScribeArtifactDigest', 'reason', 'requestId'])
92
+ || !revision(raw.expectedBindingRevision) || raw.expectedBindingRevision < 1
93
+ || !text(raw.expectedScribeTaskId, 512) || !text(raw.expectedArtificerTaskId, 512)
94
+ || !digest(raw.expectedScribeArtifactDigest) || !text(raw.reason, 2000) || !text(raw.requestId, 200)) {
95
+ sendBadRequest(res, '请提交当前关联、原候选链任务与工件摘要;不接受身份或批准字段。');
96
+ return;
97
+ }
98
+ const result = await model.correctSource(candidateId, {
99
+ expectedBindingRevision: raw.expectedBindingRevision,
100
+ expectedScribeTaskId: raw.expectedScribeTaskId,
101
+ expectedArtificerTaskId: raw.expectedArtificerTaskId,
102
+ expectedScribeArtifactDigest: raw.expectedScribeArtifactDigest,
103
+ reason: raw.reason,
104
+ requestId: raw.requestId,
105
+ }, ctx.ownerIdentity.ownerId);
106
+ if (result.status === 'refused')
107
+ failure(res, result);
108
+ else
109
+ sendSuccess(res, result);
110
+ return;
111
+ }
87
112
  if (match[2] === 'revise-once') {
88
113
  if (!closed(raw, ['expectedBindingRevision', 'expectedScribeTaskId', 'expectedTaskRevisionEpoch', 'requestId'])
89
114
  || !revision(raw.expectedBindingRevision) || raw.expectedBindingRevision < 1