create-principles-disciple 1.95.0 → 1.96.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 (261) hide show
  1. package/console/dist/server/config/pd-config-store.js +49 -12
  2. package/console/dist/server/index.js +4 -3
  3. package/console/dist/server/models/IntentPageModel.d.ts +36 -2
  4. package/console/dist/server/models/IntentPageModel.js +213 -11
  5. package/console/dist/server/routes/intent.d.ts +5 -1
  6. package/console/dist/server/routes/intent.js +200 -12
  7. package/console/dist/types.d.ts +1 -0
  8. package/console/dist/ui/App.js +0 -53
  9. package/console/dist/ui/api.d.ts +18 -4
  10. package/console/dist/ui/api.js +38 -5
  11. package/console/dist/ui/components/layout/app-sidebar.js +12 -12
  12. package/console/dist/ui/components/ui/badge.d.ts +1 -1
  13. package/console/dist/ui/i18n/en.json +142 -3
  14. package/console/dist/ui/i18n/zh-CN.json +142 -3
  15. package/console/dist/ui/pages/control-center/AgentCard.d.ts +31 -0
  16. package/console/dist/ui/pages/control-center/AgentCard.js +156 -0
  17. package/console/dist/ui/pages/control-center/AgentGroup.d.ts +26 -0
  18. package/console/dist/ui/pages/control-center/AgentGroup.js +38 -0
  19. package/console/dist/ui/pages/control-center/ControlCenterPage.js +77 -41
  20. package/console/dist/ui/pages/control-center/WorkflowDiagram.d.ts +17 -0
  21. package/console/dist/ui/pages/control-center/WorkflowDiagram.js +55 -0
  22. package/console/dist/ui/pages/intent/IntentOnboarding.d.ts +67 -0
  23. package/console/dist/ui/pages/intent/IntentOnboarding.js +289 -0
  24. package/console/dist/ui/pages/intent/IntentPage.js +117 -6
  25. package/console/dist/ui/utils/agent-metadata.d.ts +58 -0
  26. package/console/dist/ui/utils/agent-metadata.js +275 -0
  27. package/console/dist/ui/utils/control-center-helpers.d.ts +19 -0
  28. package/console/dist/ui/utils/control-center-helpers.js +37 -0
  29. package/console/dist/ui/utils/validators.d.ts +37 -0
  30. package/console/dist/ui/utils/validators.js +130 -0
  31. package/console/dist/web/assets/app.css +360 -3
  32. package/console/dist/web/assets/app.js +2703 -894
  33. package/core/dist/runtime-v2/__tests__/architecture-regression.test.js +27 -35
  34. package/core/dist/runtime-v2/__tests__/architecture-regression.test.js.map +1 -1
  35. package/core/dist/runtime-v2/__tests__/evaluator-runner-vslice-v2.test.js +85 -7
  36. package/core/dist/runtime-v2/__tests__/evaluator-runner-vslice-v2.test.js.map +1 -1
  37. package/core/dist/runtime-v2/__tests__/golden-trace-replay-validator.test.js +70 -0
  38. package/core/dist/runtime-v2/__tests__/golden-trace-replay-validator.test.js.map +1 -1
  39. package/core/dist/runtime-v2/__tests__/golden-trace.test.js +136 -1
  40. package/core/dist/runtime-v2/__tests__/golden-trace.test.js.map +1 -1
  41. package/core/dist/runtime-v2/__tests__/v2-adversarial-cases.test.d.ts +2 -0
  42. package/core/dist/runtime-v2/__tests__/v2-adversarial-cases.test.d.ts.map +1 -0
  43. package/core/dist/runtime-v2/__tests__/v2-adversarial-cases.test.js +211 -0
  44. package/core/dist/runtime-v2/__tests__/v2-adversarial-cases.test.js.map +1 -0
  45. package/core/dist/runtime-v2/activation/writers/__tests__/rule-host-writer.test.js +93 -0
  46. package/core/dist/runtime-v2/activation/writers/__tests__/rule-host-writer.test.js.map +1 -1
  47. package/core/dist/runtime-v2/activation/writers/rule-host-writer.d.ts +11 -0
  48. package/core/dist/runtime-v2/activation/writers/rule-host-writer.d.ts.map +1 -1
  49. package/core/dist/runtime-v2/activation/writers/rule-host-writer.js +22 -0
  50. package/core/dist/runtime-v2/activation/writers/rule-host-writer.js.map +1 -1
  51. package/core/dist/runtime-v2/feature-flags/__tests__/rulecode-context-v2-flag.test.d.ts +2 -0
  52. package/core/dist/runtime-v2/feature-flags/__tests__/rulecode-context-v2-flag.test.d.ts.map +1 -0
  53. package/core/dist/runtime-v2/feature-flags/__tests__/rulecode-context-v2-flag.test.js +74 -0
  54. package/core/dist/runtime-v2/feature-flags/__tests__/rulecode-context-v2-flag.test.js.map +1 -0
  55. package/core/dist/runtime-v2/feature-flags/__tests__/surface-guard-policy.test.d.ts +2 -0
  56. package/core/dist/runtime-v2/feature-flags/__tests__/surface-guard-policy.test.d.ts.map +1 -0
  57. package/core/dist/runtime-v2/feature-flags/__tests__/surface-guard-policy.test.js +246 -0
  58. package/core/dist/runtime-v2/feature-flags/__tests__/surface-guard-policy.test.js.map +1 -0
  59. package/core/dist/runtime-v2/feature-flags/feature-flag-contract.d.ts.map +1 -1
  60. package/core/dist/runtime-v2/feature-flags/feature-flag-contract.js +7 -0
  61. package/core/dist/runtime-v2/feature-flags/feature-flag-contract.js.map +1 -1
  62. package/core/dist/runtime-v2/feature-flags/index.d.ts +2 -0
  63. package/core/dist/runtime-v2/feature-flags/index.d.ts.map +1 -1
  64. package/core/dist/runtime-v2/feature-flags/index.js +2 -0
  65. package/core/dist/runtime-v2/feature-flags/index.js.map +1 -1
  66. package/{plugin/dist/core/surface-guard.d.ts → core/dist/runtime-v2/feature-flags/surface-guard-policy.d.ts} +15 -5
  67. package/core/dist/runtime-v2/feature-flags/surface-guard-policy.d.ts.map +1 -0
  68. package/{plugin/dist/core/surface-guard.js → core/dist/runtime-v2/feature-flags/surface-guard-policy.js} +14 -2
  69. package/core/dist/runtime-v2/feature-flags/surface-guard-policy.js.map +1 -0
  70. package/core/dist/runtime-v2/golden-trace-replay-validator.js +1 -1
  71. package/core/dist/runtime-v2/golden-trace-replay-validator.js.map +1 -1
  72. package/core/dist/runtime-v2/golden-trace.d.ts +19 -3
  73. package/core/dist/runtime-v2/golden-trace.d.ts.map +1 -1
  74. package/core/dist/runtime-v2/golden-trace.js +2 -0
  75. package/core/dist/runtime-v2/golden-trace.js.map +1 -1
  76. package/core/dist/runtime-v2/index.d.ts +15 -3
  77. package/core/dist/runtime-v2/index.d.ts.map +1 -1
  78. package/core/dist/runtime-v2/index.js +11 -2
  79. package/core/dist/runtime-v2/index.js.map +1 -1
  80. package/core/dist/runtime-v2/intent/__tests__/intent-doc-version.test.d.ts +2 -0
  81. package/core/dist/runtime-v2/intent/__tests__/intent-doc-version.test.d.ts.map +1 -0
  82. package/core/dist/runtime-v2/intent/__tests__/intent-doc-version.test.js +88 -0
  83. package/core/dist/runtime-v2/intent/__tests__/intent-doc-version.test.js.map +1 -0
  84. package/core/dist/runtime-v2/intent/__tests__/intent-doc.test.js +91 -1
  85. package/core/dist/runtime-v2/intent/__tests__/intent-doc.test.js.map +1 -1
  86. package/core/dist/runtime-v2/intent/index.d.ts +2 -0
  87. package/core/dist/runtime-v2/intent/index.d.ts.map +1 -1
  88. package/core/dist/runtime-v2/intent/index.js +2 -0
  89. package/core/dist/runtime-v2/intent/index.js.map +1 -1
  90. package/core/dist/runtime-v2/intent/intent-browser.d.ts +23 -0
  91. package/core/dist/runtime-v2/intent/intent-browser.d.ts.map +1 -0
  92. package/core/dist/runtime-v2/intent/intent-browser.js +21 -0
  93. package/core/dist/runtime-v2/intent/intent-browser.js.map +1 -0
  94. package/core/dist/runtime-v2/intent/intent-doc-version.d.ts +28 -0
  95. package/core/dist/runtime-v2/intent/intent-doc-version.d.ts.map +1 -0
  96. package/core/dist/runtime-v2/intent/intent-doc-version.js +21 -0
  97. package/core/dist/runtime-v2/intent/intent-doc-version.js.map +1 -0
  98. package/core/dist/runtime-v2/intent/intent-doc.d.ts +17 -1
  99. package/core/dist/runtime-v2/intent/intent-doc.d.ts.map +1 -1
  100. package/core/dist/runtime-v2/intent/intent-doc.js +95 -7
  101. package/core/dist/runtime-v2/intent/intent-doc.js.map +1 -1
  102. package/core/dist/runtime-v2/intent/intent-hash.d.ts +2 -0
  103. package/core/dist/runtime-v2/intent/intent-hash.d.ts.map +1 -0
  104. package/core/dist/runtime-v2/intent/intent-hash.js +10 -0
  105. package/core/dist/runtime-v2/intent/intent-hash.js.map +1 -0
  106. package/core/dist/runtime-v2/internalization/__tests__/artificer-prompt-builder.test.js +41 -1
  107. package/core/dist/runtime-v2/internalization/__tests__/artificer-prompt-builder.test.js.map +1 -1
  108. package/core/dist/runtime-v2/internalization/__tests__/artificer-rule-output.test.js +130 -2
  109. package/core/dist/runtime-v2/internalization/__tests__/artificer-rule-output.test.js.map +1 -1
  110. package/core/dist/runtime-v2/internalization/__tests__/behavior-example-pack.test.d.ts +2 -0
  111. package/core/dist/runtime-v2/internalization/__tests__/behavior-example-pack.test.d.ts.map +1 -0
  112. package/core/dist/runtime-v2/internalization/__tests__/behavior-example-pack.test.js +228 -0
  113. package/core/dist/runtime-v2/internalization/__tests__/behavior-example-pack.test.js.map +1 -0
  114. package/core/dist/runtime-v2/internalization/__tests__/diag-chain-e2e.test.js +41 -11
  115. package/core/dist/runtime-v2/internalization/__tests__/diag-chain-e2e.test.js.map +1 -1
  116. package/core/dist/runtime-v2/internalization/__tests__/refiner-sandbox-wrapper.test.js +135 -0
  117. package/core/dist/runtime-v2/internalization/__tests__/refiner-sandbox-wrapper.test.js.map +1 -1
  118. package/core/dist/runtime-v2/internalization/__tests__/rule-context-v2.test.d.ts +2 -0
  119. package/core/dist/runtime-v2/internalization/__tests__/rule-context-v2.test.d.ts.map +1 -0
  120. package/core/dist/runtime-v2/internalization/__tests__/rule-context-v2.test.js +449 -0
  121. package/core/dist/runtime-v2/internalization/__tests__/rule-context-v2.test.js.map +1 -0
  122. package/core/dist/runtime-v2/internalization/adversarial-case.d.ts.map +1 -1
  123. package/core/dist/runtime-v2/internalization/adversarial-case.js +24 -0
  124. package/core/dist/runtime-v2/internalization/adversarial-case.js.map +1 -1
  125. package/core/dist/runtime-v2/internalization/artificer-output.d.ts +10 -0
  126. package/core/dist/runtime-v2/internalization/artificer-output.d.ts.map +1 -1
  127. package/core/dist/runtime-v2/internalization/artificer-output.js +45 -0
  128. package/core/dist/runtime-v2/internalization/artificer-output.js.map +1 -1
  129. package/core/dist/runtime-v2/internalization/artificer-prompt-builder.d.ts +2 -2
  130. package/core/dist/runtime-v2/internalization/artificer-prompt-builder.d.ts.map +1 -1
  131. package/core/dist/runtime-v2/internalization/artificer-prompt-builder.js +7 -2
  132. package/core/dist/runtime-v2/internalization/artificer-prompt-builder.js.map +1 -1
  133. package/core/dist/runtime-v2/internalization/behavior-example-pack.d.ts +66 -0
  134. package/core/dist/runtime-v2/internalization/behavior-example-pack.d.ts.map +1 -0
  135. package/core/dist/runtime-v2/internalization/behavior-example-pack.js +183 -0
  136. package/core/dist/runtime-v2/internalization/behavior-example-pack.js.map +1 -0
  137. package/core/dist/runtime-v2/internalization/evaluator-output.d.ts +8 -0
  138. package/core/dist/runtime-v2/internalization/evaluator-output.d.ts.map +1 -1
  139. package/core/dist/runtime-v2/internalization/evaluator-output.js +10 -0
  140. package/core/dist/runtime-v2/internalization/evaluator-output.js.map +1 -1
  141. package/core/dist/runtime-v2/internalization/evaluator-runner.d.ts +17 -0
  142. package/core/dist/runtime-v2/internalization/evaluator-runner.d.ts.map +1 -1
  143. package/core/dist/runtime-v2/internalization/evaluator-runner.js +118 -24
  144. package/core/dist/runtime-v2/internalization/evaluator-runner.js.map +1 -1
  145. package/core/dist/runtime-v2/internalization/refiner-sandbox-wrapper.d.ts.map +1 -1
  146. package/core/dist/runtime-v2/internalization/refiner-sandbox-wrapper.js +1 -1
  147. package/core/dist/runtime-v2/internalization/refiner-sandbox-wrapper.js.map +1 -1
  148. package/core/dist/runtime-v2/internalization/rule-context-v2.d.ts +61 -0
  149. package/core/dist/runtime-v2/internalization/rule-context-v2.d.ts.map +1 -0
  150. package/core/dist/runtime-v2/internalization/rule-context-v2.js +301 -0
  151. package/core/dist/runtime-v2/internalization/rule-context-v2.js.map +1 -0
  152. package/core/dist/runtime-v2/internalization/rule-host-contracts.d.ts +9 -0
  153. package/core/dist/runtime-v2/internalization/rule-host-contracts.d.ts.map +1 -1
  154. package/core/dist/runtime-v2/internalization/v2-adversarial-cases.d.ts +55 -0
  155. package/core/dist/runtime-v2/internalization/v2-adversarial-cases.d.ts.map +1 -0
  156. package/core/dist/runtime-v2/internalization/v2-adversarial-cases.js +131 -0
  157. package/core/dist/runtime-v2/internalization/v2-adversarial-cases.js.map +1 -0
  158. package/core/dist/runtime-v2/pain-signal-observability.d.ts.map +1 -1
  159. package/core/dist/runtime-v2/pain-signal-observability.js +24 -1
  160. package/core/dist/runtime-v2/pain-signal-observability.js.map +1 -1
  161. package/core/dist/runtime-v2/risk/__tests__/risk-calculator-policy.test.d.ts +2 -0
  162. package/core/dist/runtime-v2/risk/__tests__/risk-calculator-policy.test.d.ts.map +1 -0
  163. package/core/dist/runtime-v2/risk/__tests__/risk-calculator-policy.test.js +95 -0
  164. package/core/dist/runtime-v2/risk/__tests__/risk-calculator-policy.test.js.map +1 -0
  165. package/core/dist/runtime-v2/risk/index.d.ts +7 -0
  166. package/core/dist/runtime-v2/risk/index.d.ts.map +1 -0
  167. package/core/dist/runtime-v2/risk/index.js +6 -0
  168. package/core/dist/runtime-v2/risk/index.js.map +1 -0
  169. package/core/dist/runtime-v2/risk/risk-calculator-policy.d.ts +18 -0
  170. package/core/dist/runtime-v2/risk/risk-calculator-policy.d.ts.map +1 -0
  171. package/core/dist/runtime-v2/risk/risk-calculator-policy.js +47 -0
  172. package/core/dist/runtime-v2/risk/risk-calculator-policy.js.map +1 -0
  173. package/core/dist/runtime-v2/store/intent/__tests__/sqlite-intent-doc-version-store.test.d.ts +2 -0
  174. package/core/dist/runtime-v2/store/intent/__tests__/sqlite-intent-doc-version-store.test.d.ts.map +1 -0
  175. package/core/dist/runtime-v2/store/intent/__tests__/sqlite-intent-doc-version-store.test.js +83 -0
  176. package/core/dist/runtime-v2/store/intent/__tests__/sqlite-intent-doc-version-store.test.js.map +1 -0
  177. package/core/dist/runtime-v2/store/intent/index.d.ts +2 -1
  178. package/core/dist/runtime-v2/store/intent/index.d.ts.map +1 -1
  179. package/core/dist/runtime-v2/store/intent/index.js +2 -1
  180. package/core/dist/runtime-v2/store/intent/index.js.map +1 -1
  181. package/core/dist/runtime-v2/store/intent/sqlite-intent-doc-version-store.d.ts +28 -0
  182. package/core/dist/runtime-v2/store/intent/sqlite-intent-doc-version-store.d.ts.map +1 -0
  183. package/core/dist/runtime-v2/store/intent/sqlite-intent-doc-version-store.js +88 -0
  184. package/core/dist/runtime-v2/store/intent/sqlite-intent-doc-version-store.js.map +1 -0
  185. package/core/dist/runtime-v2/store/sqlite-connection.d.ts.map +1 -1
  186. package/core/dist/runtime-v2/store/sqlite-connection.js +14 -0
  187. package/core/dist/runtime-v2/store/sqlite-connection.js.map +1 -1
  188. package/core/dist/runtime-v2/thinking-models/__tests__/thinking-models-policy.test.d.ts +2 -0
  189. package/core/dist/runtime-v2/thinking-models/__tests__/thinking-models-policy.test.d.ts.map +1 -0
  190. package/core/dist/runtime-v2/thinking-models/__tests__/thinking-models-policy.test.js +149 -0
  191. package/core/dist/runtime-v2/thinking-models/__tests__/thinking-models-policy.test.js.map +1 -0
  192. package/core/dist/runtime-v2/thinking-models/index.d.ts +3 -0
  193. package/core/dist/runtime-v2/thinking-models/index.d.ts.map +1 -0
  194. package/core/dist/runtime-v2/thinking-models/index.js +3 -0
  195. package/core/dist/runtime-v2/thinking-models/index.js.map +1 -0
  196. package/core/dist/runtime-v2/thinking-models/thinking-models-policy.d.ts +73 -0
  197. package/core/dist/runtime-v2/thinking-models/thinking-models-policy.d.ts.map +1 -0
  198. package/core/dist/runtime-v2/thinking-models/thinking-models-policy.js +227 -0
  199. package/core/dist/runtime-v2/thinking-models/thinking-models-policy.js.map +1 -0
  200. package/core/dist/runtime-v2/tools/__tests__/artificer-l2-tool-contract.test.js +40 -1
  201. package/core/dist/runtime-v2/tools/__tests__/artificer-l2-tool-contract.test.js.map +1 -1
  202. package/core/dist/runtime-v2/tools/artificer-l2-tool-contract.d.ts.map +1 -1
  203. package/core/dist/runtime-v2/tools/artificer-l2-tool-contract.js +28 -1
  204. package/core/dist/runtime-v2/tools/artificer-l2-tool-contract.js.map +1 -1
  205. package/core/dist/runtime-v2/tools/artificer-output-typebox.d.ts +3 -0
  206. package/core/dist/runtime-v2/tools/artificer-output-typebox.d.ts.map +1 -1
  207. package/core/dist/runtime-v2/tools/artificer-output-typebox.js +3 -0
  208. package/core/dist/runtime-v2/tools/artificer-output-typebox.js.map +1 -1
  209. package/core/package.json +4 -0
  210. package/package.json +1 -1
  211. package/pd-cli/dist/commands/__tests__/intent-flag-wiring.test.js +74 -3
  212. package/pd-cli/dist/commands/__tests__/intent-flag-wiring.test.js.map +1 -1
  213. package/pd-cli/dist/commands/intent.d.ts +21 -4
  214. package/pd-cli/dist/commands/intent.d.ts.map +1 -1
  215. package/pd-cli/dist/commands/intent.js +88 -27
  216. package/pd-cli/dist/commands/intent.js.map +1 -1
  217. package/plugin/dist/commands/pain.js +2 -2
  218. package/plugin/dist/core/behavior-example-pack-assembler.d.ts +43 -0
  219. package/plugin/dist/core/behavior-example-pack-assembler.js +247 -0
  220. package/plugin/dist/core/intent-doc-reader-adapter.d.ts +13 -5
  221. package/plugin/dist/core/intent-doc-reader-adapter.js +25 -5
  222. package/plugin/dist/core/intent-doc-reader.d.ts +13 -4
  223. package/plugin/dist/core/intent-doc-reader.js +45 -24
  224. package/plugin/dist/core/rule-context-assembler.d.ts +45 -0
  225. package/plugin/dist/core/rule-context-assembler.js +148 -0
  226. package/plugin/dist/core/thinking-models.d.ts +12 -57
  227. package/plugin/dist/core/thinking-models.js +18 -224
  228. package/plugin/dist/core/trajectory-types.d.ts +18 -0
  229. package/plugin/dist/core/trajectory.d.ts +34 -2
  230. package/plugin/dist/core/trajectory.js +103 -0
  231. package/plugin/dist/hooks/gate.js +59 -2
  232. package/plugin/dist/hooks/pain.js +2 -2
  233. package/plugin/dist/hooks/prompt.js +2 -1
  234. package/plugin/dist/index.js +1 -1
  235. package/plugin/dist/core/evolution-migration.d.ts +0 -5
  236. package/plugin/dist/core/evolution-migration.js +0 -65
  237. package/plugin/dist/core/risk-calculator.d.ts +0 -22
  238. package/plugin/dist/core/risk-calculator.js +0 -88
  239. package/plugin/dist/core/rule-host-helpers.d.ts +0 -8
  240. package/plugin/dist/core/rule-host-helpers.js +0 -7
  241. package/plugin/dist/core/rule-host-types.d.ts +0 -7
  242. package/plugin/dist/core/rule-host-types.js +0 -7
  243. package/plugin/dist/core/schema/db-types.d.ts +0 -11
  244. package/plugin/dist/core/schema/db-types.js +0 -5
  245. package/plugin/dist/core/schema/index.d.ts +0 -17
  246. package/plugin/dist/core/schema/index.js +0 -15
  247. package/plugin/dist/core/schema/migration-runner.d.ts +0 -53
  248. package/plugin/dist/core/schema/migration-runner.js +0 -170
  249. package/plugin/dist/core/schema/migrations/001-init-trajectory.d.ts +0 -6
  250. package/plugin/dist/core/schema/migrations/001-init-trajectory.js +0 -190
  251. package/plugin/dist/core/schema/migrations/002-init-central.d.ts +0 -5
  252. package/plugin/dist/core/schema/migrations/002-init-central.js +0 -108
  253. package/plugin/dist/core/schema/migrations/003-init-workflow.d.ts +0 -5
  254. package/plugin/dist/core/schema/migrations/003-init-workflow.js +0 -50
  255. package/plugin/dist/core/schema/migrations/004-add-thinking-and-gfi.d.ts +0 -8
  256. package/plugin/dist/core/schema/migrations/004-add-thinking-and-gfi.js +0 -66
  257. package/plugin/dist/core/schema/migrations/index.d.ts +0 -16
  258. package/plugin/dist/core/schema/migrations/index.js +0 -27
  259. package/plugin/dist/core/schema/schema-definitions.d.ts +0 -46
  260. package/plugin/dist/core/schema/schema-definitions.js +0 -599
  261. package/plugin/scripts/db-migrate.mjs +0 -170
@@ -693,29 +693,66 @@ export function updateFeatureFlag(workspaceDir, featureName, enabled) {
693
693
  else {
694
694
  rawConfig = {};
695
695
  }
696
- // 4. Refuse to CREATE a features section if absent (spec §13.4: 不允许新增)
697
- // Use Object.hasOwn for untrusted key check (ERR-013)
698
- if (!Object.hasOwn(rawConfig, 'features') || !isRecord(rawConfig.features)) {
696
+ // 4. PRI-477 onboarding: auto-create `features:` section ONLY when absent.
697
+ // Original spec §13.4 "不允许新增" applied to UNREGISTERED flags —
698
+ // auto-creating the section for registered flags (validated in step 1) is
699
+ // safe and required for the FlagToggleCard one-click enable flow on fresh
700
+ // installs where .pd/config.yaml has no features: section yet.
701
+ //
702
+ // IMPORTANT — Runtime Contract `rc-9-no-silent-fallback` (ERR-002):
703
+ // If `features` EXISTS but is NOT a record/object, we must NOT silently
704
+ // overwrite the user's malformed value with defaults — that would hide a
705
+ // config error and lose the original mistake. Return 409 conflict so the
706
+ // Owner can fix the config first. Without this guard, a config typo like
707
+ // `features: "oops"` would be silently reset on the first flag toggle.
708
+ //
709
+ // Use Object.hasOwn for untrusted key check (ERR-013).
710
+ let featuresSection;
711
+ if (!Object.hasOwn(rawConfig, 'features')) {
712
+ // Section absent — seed with defaults from the registered flag set so the
713
+ // subsequent validatePdConfig passes (it requires prompt/code_tool_hook /
714
+ // defer_archive as registered flags). Computed once at module load.
715
+ featuresSection = {};
716
+ for (const [flagId, flag] of Object.entries(_DEFAULT_FLAGS_RESULT.flags)) {
717
+ featuresSection[flagId] = { category: flag.category, enabled: flag.enabled };
718
+ }
719
+ rawConfig.features = featuresSection;
720
+ }
721
+ else if (isRecord(rawConfig.features)) {
722
+ featuresSection = rawConfig.features;
723
+ }
724
+ else {
725
+ // features: exists but is malformed (string, array, number, null, etc.).
726
+ // Reject rather than silently resetting — rc-9-no-silent-fallback.
727
+ const gotType = rawConfig.features === null ? 'null' : typeof rawConfig.features;
699
728
  return {
700
729
  ok: false,
701
- statusCode: 422,
702
- error: 'no_features_section',
703
- message: `Cannot update feature flag: .pd/config.yaml has no 'features:' section. This API does not create new feature sections.`,
704
- nextAction: 'Manually add a features: section to .pd/config.yaml first, or set the flag via yaml directly.',
730
+ statusCode: 409,
731
+ error: 'conflict',
732
+ message: `Cannot update feature flag: .pd/config.yaml has a 'features:' section that ` +
733
+ `is not an object/map (got ${gotType}). Fix the config file so 'features:' ` +
734
+ `is a map of flag objects before retrying, or remove the 'features:' key ` +
735
+ `entirely so PD can seed registered-flag defaults.`,
736
+ nextAction: "Open .pd/config.yaml and set 'features: {}' (or remove the 'features:' line).",
705
737
  };
706
738
  }
707
- // 5. Load existing config if malformed, refuse to write
708
- const loadResult = loadPdConfig(workspaceDir);
709
- if (!loadResult.ok) {
739
+ // 5. Validate the in-memory rawConfig (with auto-created features section).
740
+ // NOTE: We do NOT call loadPdConfig(workspaceDir) here because that reads
741
+ // from DISK, where the features section may still be missing (the exact
742
+ // case we just auto-created for). Validating rawConfig in memory gives
743
+ // the same malformed-config protection without the false-positive on
744
+ // fresh installs. PRI-477.
745
+ const existingConfigValidation = validatePdConfig(rawConfig);
746
+ if (!existingConfigValidation.ok) {
710
747
  return {
711
748
  ok: false,
712
749
  statusCode: 409,
713
750
  error: 'conflict',
714
- message: `Cannot update feature flag: existing .pd/config.yaml is malformed. Fix config errors first. Errors: ${loadResult.errors.map(e => e.reason).join('; ')}`,
751
+ message: `Cannot update feature flag: existing .pd/config.yaml is malformed. Fix config errors first. Errors: ${existingConfigValidation.errors.map(e => e.reason).join('; ')}`,
715
752
  };
716
753
  }
717
754
  // 6. Merge: update features[featureName].enabled, preserving other flags
718
- const existingFeatures = { ...rawConfig.features };
755
+ const existingFeatures = { ...featuresSection };
719
756
  const existingEntry = Object.hasOwn(existingFeatures, featureName)
720
757
  ? existingFeatures[featureName]
721
758
  : undefined;
@@ -328,9 +328,10 @@ function handleRequest(services) {
328
328
  asyncHandler(() => handleGovernanceRoute(req, res, services.workspaceDir))(req, res);
329
329
  return;
330
330
  }
331
- // PRI-466: GET /api/v1/intent
332
- if (urlPath === '/api/v1/intent') {
333
- asyncHandler(() => handleIntentRoute(req, res, services.workspaceDir))(req, res);
331
+ // PRI-466/477: GET/POST/PUT /api/v1/intent (+ /init, /content sub-paths)
332
+ if (urlPath === '/api/v1/intent' || urlPath.startsWith('/api/v1/intent/')) {
333
+ const subPath = urlPath === '/api/v1/intent' ? '' : urlPath.slice('/api/v1/intent'.length);
334
+ asyncHandler(() => handleIntentRoute(req, res, { workspaceDir: services.workspaceDir, subPath }))(req, res);
334
335
  return;
335
336
  }
336
337
  // PRI-470: IntentDecisionRecord (POST/GET /api/v1/intent-decisions, /:id, /summary)
@@ -1,4 +1,4 @@
1
- import { type IntentDocWarning } from '@principles/core/runtime-v2';
1
+ import { type IntentDocWarning, type IntentDocVersion, type IntentLang } from '@principles/core/runtime-v2';
2
2
  export interface IntentPageResult {
3
3
  ok: boolean;
4
4
  found: boolean;
@@ -11,8 +11,42 @@ export interface IntentPageResult {
11
11
  reason?: string;
12
12
  nextAction?: string;
13
13
  }
14
+ export interface IntentInitResult {
15
+ ok: boolean;
16
+ created: boolean;
17
+ path?: string;
18
+ reason?: string;
19
+ nextAction?: string;
20
+ }
21
+ export interface IntentSaveResult {
22
+ ok: boolean;
23
+ saved: boolean;
24
+ path?: string;
25
+ contentHash?: string;
26
+ lastEditedAt?: string;
27
+ warnings?: IntentDocWarning[];
28
+ reason?: string;
29
+ nextAction?: string;
30
+ }
31
+ export interface IntentRawContentResult {
32
+ ok: boolean;
33
+ content?: string;
34
+ path?: string;
35
+ reason?: string;
36
+ nextAction?: string;
37
+ }
38
+ export interface IntentVersionResult {
39
+ ok: boolean;
40
+ versions?: IntentDocVersion[];
41
+ reason?: string;
42
+ nextAction?: string;
43
+ }
14
44
  export declare class IntentPageModel {
15
45
  private readonly workspaceDir;
16
46
  constructor(workspaceDir: string);
17
- getSummary(flagEnabled: boolean): Promise<IntentPageResult>;
47
+ getSummary(flagEnabled: boolean, lang: IntentLang): Promise<IntentPageResult>;
48
+ getRawContent(flagEnabled: boolean, lang: IntentLang): Promise<IntentRawContentResult>;
49
+ createTemplate(flagEnabled: boolean, force: boolean, lang: IntentLang): Promise<IntentInitResult>;
50
+ saveContent(flagEnabled: boolean, content: unknown, lang: IntentLang): Promise<IntentSaveResult>;
51
+ getVersions(lang: IntentLang): Promise<IntentVersionResult>;
18
52
  }
@@ -1,14 +1,40 @@
1
1
  import * as fs from 'node:fs';
2
2
  import * as path from 'node:path';
3
- import { INTENT_MAX_BYTES, parseIntentDocSections, computeIntentContentHash, validateIntentDocSections, } from '@principles/core/runtime-v2';
4
- const INTENT_FILENAME = 'INTENT.md';
3
+ import { INTENT_MAX_BYTES, getIntentFilename, createIntentTemplate, parseIntentDocSections, computeIntentContentHash, validateIntentDocSections, SqliteConnection, SqliteIntentDocVersionStore, } from '@principles/core/runtime-v2';
5
4
  const INTENT_DIR = '.principles';
5
+ function stateDbExists(workspaceDir) {
6
+ return fs.existsSync(path.join(workspaceDir, '.pd', 'state.db'));
7
+ }
8
+ /** Best-effort version record — never blocks save on failure. */
9
+ function recordVersion(args) {
10
+ // Note: no stateDbExists short-circuit. SqliteConnection creates the .pd
11
+ // dir and state.db file (with intent_doc_versions table via CREATE TABLE
12
+ // IF NOT EXISTS) on first open. Short-circuiting here would silently drop
13
+ // the first-write version history on fresh workspaces — the operator
14
+ // would see an empty history panel after their very first save.
15
+ try {
16
+ const connection = new SqliteConnection({ workspaceDir: args.workspaceDir });
17
+ try {
18
+ const store = new SqliteIntentDocVersionStore(connection);
19
+ store.saveVersion({ lang: args.lang, content: args.content, reason: args.reason });
20
+ }
21
+ finally {
22
+ try {
23
+ connection.close();
24
+ }
25
+ catch { /* best-effort */ }
26
+ }
27
+ }
28
+ catch {
29
+ // Version recording is best-effort — never block save on failure.
30
+ }
31
+ }
6
32
  export class IntentPageModel {
7
33
  workspaceDir;
8
34
  constructor(workspaceDir) {
9
35
  this.workspaceDir = workspaceDir;
10
36
  }
11
- async getSummary(flagEnabled) {
37
+ async getSummary(flagEnabled, lang) {
12
38
  if (!flagEnabled) {
13
39
  return {
14
40
  ok: false,
@@ -16,10 +42,10 @@ export class IntentPageModel {
16
42
  flagEnabled: false,
17
43
  warnings: [],
18
44
  reason: 'flag_disabled',
19
- nextAction: 'Enable the intent_engineering feature flag in .pd/config.yaml to read INTENT.md.',
45
+ nextAction: `Enable the intent_engineering feature flag in .pd/config.yaml to read ${getIntentFilename(lang)}.`,
20
46
  };
21
47
  }
22
- const filePath = path.join(this.workspaceDir, INTENT_DIR, INTENT_FILENAME);
48
+ const filePath = path.join(this.workspaceDir, INTENT_DIR, getIntentFilename(lang));
23
49
  try {
24
50
  if (!fs.existsSync(filePath)) {
25
51
  return {
@@ -28,7 +54,7 @@ export class IntentPageModel {
28
54
  flagEnabled: true,
29
55
  warnings: [],
30
56
  reason: 'not_found',
31
- nextAction: 'Create .principles/INTENT.md using "pd intent init".',
57
+ nextAction: `Create .principles/${getIntentFilename(lang)} using "pd intent init".`,
32
58
  };
33
59
  }
34
60
  const stat = fs.statSync(filePath);
@@ -39,7 +65,7 @@ export class IntentPageModel {
39
65
  flagEnabled: true,
40
66
  warnings: [],
41
67
  reason: 'oversized',
42
- nextAction: `INTENT.md exceeds ${INTENT_MAX_BYTES} bytes (${stat.size} bytes). Reduce content.`,
68
+ nextAction: `${getIntentFilename(lang)} exceeds ${INTENT_MAX_BYTES} bytes (${stat.size} bytes). Reduce content.`,
43
69
  };
44
70
  }
45
71
  const raw = fs.readFileSync(filePath, 'utf8');
@@ -74,16 +100,192 @@ export class IntentPageModel {
74
100
  };
75
101
  }
76
102
  catch (err) {
77
- // Runtime Contract Rule #9: log underlying error for observability
78
- // while preserving the never-throws contract.
79
- console.error('[IntentPageModel] failed to read INTENT.md:', err);
103
+ console.error(`[IntentPageModel] failed to read ${getIntentFilename(lang)}:`, err);
80
104
  return {
81
105
  ok: false,
82
106
  found: false,
83
107
  flagEnabled: true,
84
108
  warnings: [],
85
109
  reason: 'read_error',
86
- nextAction: 'Check filesystem permissions for .principles/INTENT.md.',
110
+ nextAction: `Check filesystem permissions for .principles/${getIntentFilename(lang)}.`,
111
+ };
112
+ }
113
+ }
114
+ async getRawContent(flagEnabled, lang) {
115
+ if (!flagEnabled) {
116
+ return {
117
+ ok: false,
118
+ reason: 'flag_disabled',
119
+ nextAction: 'Enable the intent_engineering feature flag first.',
120
+ };
121
+ }
122
+ const filePath = path.join(this.workspaceDir, INTENT_DIR, getIntentFilename(lang));
123
+ try {
124
+ if (!fs.existsSync(filePath)) {
125
+ return {
126
+ ok: false,
127
+ reason: 'not_found',
128
+ nextAction: `${getIntentFilename(lang)} does not exist. Create it first using POST /api/v1/intent/init.`,
129
+ };
130
+ }
131
+ const stat = fs.statSync(filePath);
132
+ if (stat.size > INTENT_MAX_BYTES) {
133
+ return {
134
+ ok: false,
135
+ reason: 'oversized',
136
+ nextAction: `${getIntentFilename(lang)} exceeds ${INTENT_MAX_BYTES} bytes (${stat.size} bytes). Reduce content before opening the editor.`,
137
+ };
138
+ }
139
+ const content = fs.readFileSync(filePath, 'utf8');
140
+ return { ok: true, content, path: filePath };
141
+ }
142
+ catch (err) {
143
+ const message = err instanceof Error ? err.message : String(err);
144
+ console.error(`[IntentPageModel] failed to read raw ${getIntentFilename(lang)}:`, err);
145
+ return {
146
+ ok: false,
147
+ reason: 'read_error',
148
+ nextAction: `Could not read ${getIntentFilename(lang)}: ${message}. Check filesystem permissions.`,
149
+ };
150
+ }
151
+ }
152
+ async createTemplate(flagEnabled, force, lang) {
153
+ if (!flagEnabled) {
154
+ return {
155
+ ok: false,
156
+ created: false,
157
+ reason: 'flag_disabled',
158
+ nextAction: 'Enable the intent_engineering feature flag first.',
159
+ };
160
+ }
161
+ const dirPath = path.join(this.workspaceDir, INTENT_DIR);
162
+ const filePath = path.join(dirPath, getIntentFilename(lang));
163
+ try {
164
+ if (!fs.existsSync(dirPath)) {
165
+ fs.mkdirSync(dirPath, { recursive: true });
166
+ }
167
+ if (fs.existsSync(filePath) && !force) {
168
+ return {
169
+ ok: true,
170
+ created: false,
171
+ path: filePath,
172
+ reason: 'already_exists',
173
+ nextAction: 'Use force=true to overwrite, or edit the existing file instead.',
174
+ };
175
+ }
176
+ const templateContent = createIntentTemplate(lang);
177
+ fs.writeFileSync(filePath, templateContent, 'utf8');
178
+ recordVersion({ workspaceDir: this.workspaceDir, lang, content: templateContent, reason: 'init' });
179
+ return {
180
+ ok: true,
181
+ created: true,
182
+ path: filePath,
183
+ };
184
+ }
185
+ catch (err) {
186
+ console.error(`[IntentPageModel] failed to create ${getIntentFilename(lang)}:`, err);
187
+ return {
188
+ ok: false,
189
+ created: false,
190
+ reason: 'write_error',
191
+ nextAction: 'Check filesystem permissions for .principles/ directory.',
192
+ };
193
+ }
194
+ }
195
+ async saveContent(flagEnabled, content, lang) {
196
+ if (!flagEnabled) {
197
+ return {
198
+ ok: false,
199
+ saved: false,
200
+ reason: 'flag_disabled',
201
+ nextAction: 'Enable the intent_engineering feature flag first.',
202
+ };
203
+ }
204
+ if (typeof content !== 'string') {
205
+ return {
206
+ ok: false,
207
+ saved: false,
208
+ reason: 'invalid_content',
209
+ nextAction: 'Content must be a string.',
210
+ };
211
+ }
212
+ if (content.length === 0) {
213
+ return {
214
+ ok: false,
215
+ saved: false,
216
+ reason: 'empty_content',
217
+ nextAction: 'Content cannot be empty.',
218
+ };
219
+ }
220
+ const byteLength = Buffer.byteLength(content, 'utf8');
221
+ if (byteLength > INTENT_MAX_BYTES) {
222
+ return {
223
+ ok: false,
224
+ saved: false,
225
+ reason: 'oversized',
226
+ nextAction: `Content exceeds ${INTENT_MAX_BYTES} bytes (${byteLength} bytes). Reduce content.`,
227
+ };
228
+ }
229
+ const dirPath = path.join(this.workspaceDir, INTENT_DIR);
230
+ const filePath = path.join(dirPath, getIntentFilename(lang));
231
+ try {
232
+ if (!fs.existsSync(dirPath)) {
233
+ fs.mkdirSync(dirPath, { recursive: true });
234
+ }
235
+ fs.writeFileSync(filePath, content, 'utf8');
236
+ const sections = parseIntentDocSections(content);
237
+ const warnings = validateIntentDocSections(sections);
238
+ const contentHash = computeIntentContentHash(content);
239
+ const stat = fs.statSync(filePath);
240
+ recordVersion({ workspaceDir: this.workspaceDir, lang, content, reason: 'save' });
241
+ return {
242
+ ok: true,
243
+ saved: true,
244
+ path: filePath,
245
+ contentHash,
246
+ lastEditedAt: stat.mtime.toISOString(),
247
+ warnings,
248
+ };
249
+ }
250
+ catch (err) {
251
+ console.error(`[IntentPageModel] failed to save ${getIntentFilename(lang)}:`, err);
252
+ return {
253
+ ok: false,
254
+ saved: false,
255
+ reason: 'write_error',
256
+ nextAction: `Check filesystem permissions for .principles/${getIntentFilename(lang)}.`,
257
+ };
258
+ }
259
+ }
260
+ async getVersions(lang) {
261
+ if (!stateDbExists(this.workspaceDir)) {
262
+ return {
263
+ ok: false,
264
+ reason: 'state_db_not_found',
265
+ nextAction: 'Run a PD command that creates state.db first (e.g. pd pain list).',
266
+ };
267
+ }
268
+ try {
269
+ const connection = new SqliteConnection({ workspaceDir: this.workspaceDir, readonly: true });
270
+ try {
271
+ const store = new SqliteIntentDocVersionStore(connection);
272
+ const versions = await store.listVersions(lang);
273
+ return { ok: true, versions };
274
+ }
275
+ finally {
276
+ try {
277
+ connection.close();
278
+ }
279
+ catch { /* best-effort */ }
280
+ }
281
+ }
282
+ catch (err) {
283
+ const message = err instanceof Error ? err.message : String(err);
284
+ console.error(`[IntentPageModel] failed to list versions:`, err);
285
+ return {
286
+ ok: false,
287
+ reason: 'read_error',
288
+ nextAction: `Could not read version history: ${message}`,
87
289
  };
88
290
  }
89
291
  }
@@ -1,3 +1,7 @@
1
1
  import type { IncomingMessage, ServerResponse } from 'node:http';
2
- export declare function handleIntentRoute(req: IncomingMessage, res: ServerResponse, workspaceDir: string): Promise<void>;
2
+ export interface IntentRouteContext {
3
+ workspaceDir: string;
4
+ subPath: string;
5
+ }
6
+ export declare function handleIntentRoute(req: IncomingMessage, res: ServerResponse, ctx: IntentRouteContext): Promise<void>;
3
7
  export declare function disposeIntentModels(): void;
@@ -1,6 +1,13 @@
1
1
  import { IntentPageModel } from '../models/IntentPageModel.js';
2
- import { sendSuccess, sendError } from '../utils/response.js';
2
+ import { sendSuccess, sendError, sendJson } from '../utils/response.js';
3
3
  import { loadPdConfig, computeFlagsFromLoadResult } from '../config/pd-config-store.js';
4
+ import { readBody } from '../utils/request.js';
5
+ /** Parse lang from query string; default 'zh-CN' if missing/invalid. */
6
+ function parseLang(req) {
7
+ const url = new URL(req.url ?? '', 'http://localhost');
8
+ const lang = url.searchParams.get('lang');
9
+ return lang === 'en' ? 'en' : 'zh-CN';
10
+ }
4
11
  const MODEL_CACHE_TTL_MS = 30 * 1000;
5
12
  const models = new Map();
6
13
  function getModel(workspaceDir) {
@@ -12,18 +19,199 @@ function getModel(workspaceDir) {
12
19
  models.set(workspaceDir, { model, cachedAt: Date.now() });
13
20
  return model;
14
21
  }
15
- export async function handleIntentRoute(req, res, workspaceDir) {
16
- if (req.method !== 'GET') {
17
- sendError(res, 405, 'method_not_allowed', 'Only GET method is allowed');
18
- return;
19
- }
22
+ /** Load the intent_engineering flag state from config. */
23
+ function loadFlagEnabled(workspaceDir) {
24
+ const configResult = loadPdConfig(workspaceDir);
25
+ const flagsResult = computeFlagsFromLoadResult(configResult);
26
+ return flagsResult.flags.intent_engineering?.enabled === true;
27
+ }
28
+ /** Send a structured error with reason + nextAction (Runtime Contract Rule #9). */
29
+ function sendStructuredError(res, payload) {
30
+ sendError(res, payload.statusCode, payload.error, payload.reason, {
31
+ reason: payload.reason,
32
+ nextAction: payload.nextAction,
33
+ });
34
+ }
35
+ export async function handleIntentRoute(req, res, ctx) {
36
+ const { workspaceDir, subPath } = ctx;
37
+ const lang = parseLang(req);
20
38
  try {
21
- const configResult = loadPdConfig(workspaceDir);
22
- const flagsResult = computeFlagsFromLoadResult(configResult);
23
- const flagEnabled = flagsResult.flags.intent_engineering?.enabled === true;
24
- const model = getModel(workspaceDir);
25
- const summary = await model.getSummary(flagEnabled);
26
- sendSuccess(res, summary);
39
+ // ── GET /api/v1/intent — read Intent summary ──────────────────────────
40
+ if (req.method === 'GET' && subPath === '') {
41
+ const flagEnabled = loadFlagEnabled(workspaceDir);
42
+ const model = getModel(workspaceDir);
43
+ const summary = await model.getSummary(flagEnabled, lang);
44
+ sendSuccess(res, summary);
45
+ return;
46
+ }
47
+ // ── GET /api/v1/intent/content — read raw INTENT.md for editing ─────────
48
+ if (req.method === 'GET' && subPath === '/content') {
49
+ const flagEnabled = loadFlagEnabled(workspaceDir);
50
+ if (!flagEnabled) {
51
+ sendStructuredError(res, {
52
+ statusCode: 403,
53
+ error: 'flag_disabled',
54
+ reason: 'flag_disabled',
55
+ nextAction: 'Enable the intent_engineering feature flag first.',
56
+ });
57
+ return;
58
+ }
59
+ const model = getModel(workspaceDir);
60
+ const result = await model.getRawContent(flagEnabled, lang);
61
+ if (!result.ok) {
62
+ // Map model error reason → HTTP status (rc-9-no-silent-fallback).
63
+ // not_found → 404; oversized → 413 (Payload Too Large, matches the
64
+ // byte-budget contract with getSummary); read_error / unknown → 500.
65
+ const statusCode = result.reason === 'not_found' ? 404 :
66
+ result.reason === 'oversized' ? 413 :
67
+ result.reason === 'flag_disabled' ? 403 :
68
+ 500;
69
+ sendStructuredError(res, {
70
+ statusCode,
71
+ error: result.reason ?? 'intent_content_error',
72
+ reason: result.reason ?? 'unknown',
73
+ nextAction: result.nextAction,
74
+ });
75
+ return;
76
+ }
77
+ sendSuccess(res, { content: result.content, path: result.path });
78
+ return;
79
+ }
80
+ // ── GET /api/v1/intent/versions — list version history ────────────────
81
+ if (req.method === 'GET' && subPath === '/versions') {
82
+ const model = getModel(workspaceDir);
83
+ const result = await model.getVersions(lang);
84
+ if (!result.ok) {
85
+ sendSuccess(res, { versions: [], reason: result.reason, nextAction: result.nextAction });
86
+ return;
87
+ }
88
+ sendSuccess(res, { versions: result.versions });
89
+ return;
90
+ }
91
+ // ── POST /api/v1/intent/init — create INTENT.md template ──────────────
92
+ if (req.method === 'POST' && subPath === '/init') {
93
+ const flagEnabled = loadFlagEnabled(workspaceDir);
94
+ if (!flagEnabled) {
95
+ sendStructuredError(res, {
96
+ statusCode: 403,
97
+ error: 'flag_disabled',
98
+ reason: 'flag_disabled',
99
+ nextAction: 'Enable the intent_engineering feature flag first.',
100
+ });
101
+ return;
102
+ }
103
+ const body = await readBody(req);
104
+ let parsedBody = {};
105
+ if (body.trim().length > 0) {
106
+ try {
107
+ parsedBody = JSON.parse(body);
108
+ }
109
+ catch {
110
+ sendStructuredError(res, {
111
+ statusCode: 400,
112
+ error: 'bad_request',
113
+ reason: 'invalid_json',
114
+ nextAction: 'Request body must be valid JSON.',
115
+ });
116
+ return;
117
+ }
118
+ }
119
+ // Extract force flag (default false — don't overwrite existing file)
120
+ let force = false;
121
+ if (parsedBody !== null && typeof parsedBody === 'object' && Object.hasOwn(parsedBody, 'force')) {
122
+ const forceVal = parsedBody.force;
123
+ if (typeof forceVal === 'boolean') {
124
+ force = forceVal;
125
+ }
126
+ }
127
+ const model = getModel(workspaceDir);
128
+ const result = await model.createTemplate(flagEnabled, force, lang);
129
+ if (result.ok) {
130
+ if (result.created) {
131
+ sendJson(res, 201, { success: true, data: result });
132
+ }
133
+ else {
134
+ // already_exists — return 200 with reason
135
+ sendSuccess(res, result);
136
+ }
137
+ }
138
+ else {
139
+ sendStructuredError(res, {
140
+ statusCode: 500,
141
+ error: 'intent_init_error',
142
+ reason: result.reason ?? 'unknown',
143
+ nextAction: result.nextAction,
144
+ });
145
+ }
146
+ return;
147
+ }
148
+ // ── PUT /api/v1/intent/content — save INTENT.md content ───────────────
149
+ if (req.method === 'PUT' && subPath === '/content') {
150
+ const flagEnabled = loadFlagEnabled(workspaceDir);
151
+ if (!flagEnabled) {
152
+ sendStructuredError(res, {
153
+ statusCode: 403,
154
+ error: 'flag_disabled',
155
+ reason: 'flag_disabled',
156
+ nextAction: 'Enable the intent_engineering feature flag first.',
157
+ });
158
+ return;
159
+ }
160
+ const body = await readBody(req);
161
+ let parsedBody;
162
+ try {
163
+ parsedBody = JSON.parse(body);
164
+ }
165
+ catch {
166
+ sendStructuredError(res, {
167
+ statusCode: 400,
168
+ error: 'bad_request',
169
+ reason: 'invalid_json',
170
+ nextAction: 'Request body must be valid JSON.',
171
+ });
172
+ return;
173
+ }
174
+ // Runtime Contract Rule #1: treat as unknown, validate with Object.hasOwn
175
+ if (parsedBody === null || typeof parsedBody !== 'object' || Array.isArray(parsedBody)) {
176
+ sendStructuredError(res, {
177
+ statusCode: 400,
178
+ error: 'bad_request',
179
+ reason: 'invalid_body',
180
+ nextAction: 'Request body must be a JSON object with a "content" field.',
181
+ });
182
+ return;
183
+ }
184
+ const bodyObj = parsedBody;
185
+ if (!Object.hasOwn(bodyObj, 'content')) {
186
+ sendStructuredError(res, {
187
+ statusCode: 400,
188
+ error: 'bad_request',
189
+ reason: 'missing_content',
190
+ nextAction: 'Request body must include a "content" string field.',
191
+ });
192
+ return;
193
+ }
194
+ const model = getModel(workspaceDir);
195
+ const result = await model.saveContent(flagEnabled, bodyObj.content, lang);
196
+ if (result.ok) {
197
+ sendSuccess(res, result);
198
+ }
199
+ else {
200
+ // Map known reasons to HTTP status codes
201
+ const statusCode = result.reason === 'invalid_content' || result.reason === 'empty_content' || result.reason === 'oversized'
202
+ ? 400
203
+ : 500;
204
+ sendStructuredError(res, {
205
+ statusCode,
206
+ error: 'intent_save_error',
207
+ reason: result.reason ?? 'unknown',
208
+ nextAction: result.nextAction,
209
+ });
210
+ }
211
+ return;
212
+ }
213
+ // ── Unknown sub-path ──────────────────────────────────────────────────
214
+ sendError(res, 404, 'not_found', `Route /api/v1/intent${subPath} not found`);
27
215
  }
28
216
  catch (err) {
29
217
  sendError(res, 500, 'intent_route_error', err instanceof Error ? err.message : 'Unknown error');
@@ -82,5 +82,6 @@ export type ApiResponse<T> = {
82
82
  } | {
83
83
  success: false;
84
84
  error: string;
85
+ reason?: string;
85
86
  nextAction?: string;
86
87
  };