create-principles-disciple 1.95.0 → 1.97.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.
- package/console/dist/server/config/pd-config-store.js +49 -12
- package/console/dist/server/index.js +4 -3
- package/console/dist/server/models/IntentPageModel.d.ts +36 -2
- package/console/dist/server/models/IntentPageModel.js +213 -11
- package/console/dist/server/routes/intent.d.ts +5 -1
- package/console/dist/server/routes/intent.js +200 -12
- package/console/dist/types.d.ts +1 -0
- package/console/dist/ui/App.js +0 -53
- package/console/dist/ui/api.d.ts +18 -4
- package/console/dist/ui/api.js +38 -5
- package/console/dist/ui/components/layout/app-sidebar.js +12 -12
- package/console/dist/ui/components/ui/badge.d.ts +1 -1
- package/console/dist/ui/i18n/en.json +142 -3
- package/console/dist/ui/i18n/zh-CN.json +142 -3
- package/console/dist/ui/pages/control-center/AgentCard.d.ts +31 -0
- package/console/dist/ui/pages/control-center/AgentCard.js +156 -0
- package/console/dist/ui/pages/control-center/AgentGroup.d.ts +26 -0
- package/console/dist/ui/pages/control-center/AgentGroup.js +38 -0
- package/console/dist/ui/pages/control-center/ControlCenterPage.js +77 -41
- package/console/dist/ui/pages/control-center/WorkflowDiagram.d.ts +17 -0
- package/console/dist/ui/pages/control-center/WorkflowDiagram.js +55 -0
- package/console/dist/ui/pages/intent/IntentOnboarding.d.ts +67 -0
- package/console/dist/ui/pages/intent/IntentOnboarding.js +289 -0
- package/console/dist/ui/pages/intent/IntentPage.js +117 -6
- package/console/dist/ui/utils/agent-metadata.d.ts +58 -0
- package/console/dist/ui/utils/agent-metadata.js +275 -0
- package/console/dist/ui/utils/control-center-helpers.d.ts +19 -0
- package/console/dist/ui/utils/control-center-helpers.js +37 -0
- package/console/dist/ui/utils/validators.d.ts +37 -0
- package/console/dist/ui/utils/validators.js +130 -0
- package/console/dist/web/assets/app.css +360 -3
- package/console/dist/web/assets/app.js +2952 -1143
- package/console/package.json +1 -1
- package/core/dist/runtime-v2/__tests__/architecture-regression.test.js +27 -35
- package/core/dist/runtime-v2/__tests__/architecture-regression.test.js.map +1 -1
- package/core/dist/runtime-v2/__tests__/evaluator-runner-vslice-v2.test.js +85 -7
- package/core/dist/runtime-v2/__tests__/evaluator-runner-vslice-v2.test.js.map +1 -1
- package/core/dist/runtime-v2/__tests__/golden-trace-replay-validator.test.js +70 -0
- package/core/dist/runtime-v2/__tests__/golden-trace-replay-validator.test.js.map +1 -1
- package/core/dist/runtime-v2/__tests__/golden-trace.test.js +136 -1
- package/core/dist/runtime-v2/__tests__/golden-trace.test.js.map +1 -1
- package/core/dist/runtime-v2/__tests__/v2-adversarial-cases.test.d.ts +2 -0
- package/core/dist/runtime-v2/__tests__/v2-adversarial-cases.test.d.ts.map +1 -0
- package/core/dist/runtime-v2/__tests__/v2-adversarial-cases.test.js +211 -0
- package/core/dist/runtime-v2/__tests__/v2-adversarial-cases.test.js.map +1 -0
- package/core/dist/runtime-v2/activation/writers/__tests__/rule-host-writer.test.js +93 -0
- package/core/dist/runtime-v2/activation/writers/__tests__/rule-host-writer.test.js.map +1 -1
- package/core/dist/runtime-v2/activation/writers/rule-host-writer.d.ts +11 -0
- package/core/dist/runtime-v2/activation/writers/rule-host-writer.d.ts.map +1 -1
- package/core/dist/runtime-v2/activation/writers/rule-host-writer.js +22 -0
- package/core/dist/runtime-v2/activation/writers/rule-host-writer.js.map +1 -1
- package/core/dist/runtime-v2/feature-flags/__tests__/rulecode-context-v2-flag.test.d.ts +2 -0
- package/core/dist/runtime-v2/feature-flags/__tests__/rulecode-context-v2-flag.test.d.ts.map +1 -0
- package/core/dist/runtime-v2/feature-flags/__tests__/rulecode-context-v2-flag.test.js +74 -0
- package/core/dist/runtime-v2/feature-flags/__tests__/rulecode-context-v2-flag.test.js.map +1 -0
- package/core/dist/runtime-v2/feature-flags/__tests__/surface-guard-policy.test.d.ts +2 -0
- package/core/dist/runtime-v2/feature-flags/__tests__/surface-guard-policy.test.d.ts.map +1 -0
- package/core/dist/runtime-v2/feature-flags/__tests__/surface-guard-policy.test.js +246 -0
- package/core/dist/runtime-v2/feature-flags/__tests__/surface-guard-policy.test.js.map +1 -0
- package/core/dist/runtime-v2/feature-flags/feature-flag-contract.d.ts.map +1 -1
- package/core/dist/runtime-v2/feature-flags/feature-flag-contract.js +7 -0
- package/core/dist/runtime-v2/feature-flags/feature-flag-contract.js.map +1 -1
- package/core/dist/runtime-v2/feature-flags/index.d.ts +2 -0
- package/core/dist/runtime-v2/feature-flags/index.d.ts.map +1 -1
- package/core/dist/runtime-v2/feature-flags/index.js +2 -0
- package/core/dist/runtime-v2/feature-flags/index.js.map +1 -1
- package/{plugin/dist/core/surface-guard.d.ts → core/dist/runtime-v2/feature-flags/surface-guard-policy.d.ts} +15 -5
- package/core/dist/runtime-v2/feature-flags/surface-guard-policy.d.ts.map +1 -0
- package/{plugin/dist/core/surface-guard.js → core/dist/runtime-v2/feature-flags/surface-guard-policy.js} +14 -2
- package/core/dist/runtime-v2/feature-flags/surface-guard-policy.js.map +1 -0
- package/core/dist/runtime-v2/golden-trace-replay-validator.js +1 -1
- package/core/dist/runtime-v2/golden-trace-replay-validator.js.map +1 -1
- package/core/dist/runtime-v2/golden-trace.d.ts +19 -3
- package/core/dist/runtime-v2/golden-trace.d.ts.map +1 -1
- package/core/dist/runtime-v2/golden-trace.js +2 -0
- package/core/dist/runtime-v2/golden-trace.js.map +1 -1
- package/core/dist/runtime-v2/index.d.ts +15 -3
- package/core/dist/runtime-v2/index.d.ts.map +1 -1
- package/core/dist/runtime-v2/index.js +11 -2
- package/core/dist/runtime-v2/index.js.map +1 -1
- package/core/dist/runtime-v2/intent/__tests__/intent-doc-version.test.d.ts +2 -0
- package/core/dist/runtime-v2/intent/__tests__/intent-doc-version.test.d.ts.map +1 -0
- package/core/dist/runtime-v2/intent/__tests__/intent-doc-version.test.js +88 -0
- package/core/dist/runtime-v2/intent/__tests__/intent-doc-version.test.js.map +1 -0
- package/core/dist/runtime-v2/intent/__tests__/intent-doc.test.js +91 -1
- package/core/dist/runtime-v2/intent/__tests__/intent-doc.test.js.map +1 -1
- package/core/dist/runtime-v2/intent/index.d.ts +2 -0
- package/core/dist/runtime-v2/intent/index.d.ts.map +1 -1
- package/core/dist/runtime-v2/intent/index.js +2 -0
- package/core/dist/runtime-v2/intent/index.js.map +1 -1
- package/core/dist/runtime-v2/intent/intent-browser.d.ts +23 -0
- package/core/dist/runtime-v2/intent/intent-browser.d.ts.map +1 -0
- package/core/dist/runtime-v2/intent/intent-browser.js +21 -0
- package/core/dist/runtime-v2/intent/intent-browser.js.map +1 -0
- package/core/dist/runtime-v2/intent/intent-doc-version.d.ts +28 -0
- package/core/dist/runtime-v2/intent/intent-doc-version.d.ts.map +1 -0
- package/core/dist/runtime-v2/intent/intent-doc-version.js +21 -0
- package/core/dist/runtime-v2/intent/intent-doc-version.js.map +1 -0
- package/core/dist/runtime-v2/intent/intent-doc.d.ts +17 -1
- package/core/dist/runtime-v2/intent/intent-doc.d.ts.map +1 -1
- package/core/dist/runtime-v2/intent/intent-doc.js +95 -7
- package/core/dist/runtime-v2/intent/intent-doc.js.map +1 -1
- package/core/dist/runtime-v2/intent/intent-hash.d.ts +2 -0
- package/core/dist/runtime-v2/intent/intent-hash.d.ts.map +1 -0
- package/core/dist/runtime-v2/intent/intent-hash.js +10 -0
- package/core/dist/runtime-v2/intent/intent-hash.js.map +1 -0
- package/core/dist/runtime-v2/internalization/__tests__/artificer-prompt-builder.test.js +41 -1
- package/core/dist/runtime-v2/internalization/__tests__/artificer-prompt-builder.test.js.map +1 -1
- package/core/dist/runtime-v2/internalization/__tests__/artificer-rule-output.test.js +130 -2
- package/core/dist/runtime-v2/internalization/__tests__/artificer-rule-output.test.js.map +1 -1
- package/core/dist/runtime-v2/internalization/__tests__/behavior-example-pack.test.d.ts +2 -0
- package/core/dist/runtime-v2/internalization/__tests__/behavior-example-pack.test.d.ts.map +1 -0
- package/core/dist/runtime-v2/internalization/__tests__/behavior-example-pack.test.js +228 -0
- package/core/dist/runtime-v2/internalization/__tests__/behavior-example-pack.test.js.map +1 -0
- package/core/dist/runtime-v2/internalization/__tests__/diag-chain-e2e.test.js +41 -11
- package/core/dist/runtime-v2/internalization/__tests__/diag-chain-e2e.test.js.map +1 -1
- package/core/dist/runtime-v2/internalization/__tests__/refiner-sandbox-wrapper.test.js +135 -0
- package/core/dist/runtime-v2/internalization/__tests__/refiner-sandbox-wrapper.test.js.map +1 -1
- package/core/dist/runtime-v2/internalization/__tests__/rule-context-v2.test.d.ts +2 -0
- package/core/dist/runtime-v2/internalization/__tests__/rule-context-v2.test.d.ts.map +1 -0
- package/core/dist/runtime-v2/internalization/__tests__/rule-context-v2.test.js +449 -0
- package/core/dist/runtime-v2/internalization/__tests__/rule-context-v2.test.js.map +1 -0
- package/core/dist/runtime-v2/internalization/adversarial-case.d.ts.map +1 -1
- package/core/dist/runtime-v2/internalization/adversarial-case.js +24 -0
- package/core/dist/runtime-v2/internalization/adversarial-case.js.map +1 -1
- package/core/dist/runtime-v2/internalization/artificer-output.d.ts +10 -0
- package/core/dist/runtime-v2/internalization/artificer-output.d.ts.map +1 -1
- package/core/dist/runtime-v2/internalization/artificer-output.js +45 -0
- package/core/dist/runtime-v2/internalization/artificer-output.js.map +1 -1
- package/core/dist/runtime-v2/internalization/artificer-prompt-builder.d.ts +2 -2
- package/core/dist/runtime-v2/internalization/artificer-prompt-builder.d.ts.map +1 -1
- package/core/dist/runtime-v2/internalization/artificer-prompt-builder.js +7 -2
- package/core/dist/runtime-v2/internalization/artificer-prompt-builder.js.map +1 -1
- package/core/dist/runtime-v2/internalization/behavior-example-pack.d.ts +66 -0
- package/core/dist/runtime-v2/internalization/behavior-example-pack.d.ts.map +1 -0
- package/core/dist/runtime-v2/internalization/behavior-example-pack.js +183 -0
- package/core/dist/runtime-v2/internalization/behavior-example-pack.js.map +1 -0
- package/core/dist/runtime-v2/internalization/evaluator-output.d.ts +8 -0
- package/core/dist/runtime-v2/internalization/evaluator-output.d.ts.map +1 -1
- package/core/dist/runtime-v2/internalization/evaluator-output.js +10 -0
- package/core/dist/runtime-v2/internalization/evaluator-output.js.map +1 -1
- package/core/dist/runtime-v2/internalization/evaluator-runner.d.ts +17 -0
- package/core/dist/runtime-v2/internalization/evaluator-runner.d.ts.map +1 -1
- package/core/dist/runtime-v2/internalization/evaluator-runner.js +118 -24
- package/core/dist/runtime-v2/internalization/evaluator-runner.js.map +1 -1
- package/core/dist/runtime-v2/internalization/refiner-sandbox-wrapper.d.ts.map +1 -1
- package/core/dist/runtime-v2/internalization/refiner-sandbox-wrapper.js +1 -1
- package/core/dist/runtime-v2/internalization/refiner-sandbox-wrapper.js.map +1 -1
- package/core/dist/runtime-v2/internalization/rule-context-v2.d.ts +61 -0
- package/core/dist/runtime-v2/internalization/rule-context-v2.d.ts.map +1 -0
- package/core/dist/runtime-v2/internalization/rule-context-v2.js +301 -0
- package/core/dist/runtime-v2/internalization/rule-context-v2.js.map +1 -0
- package/core/dist/runtime-v2/internalization/rule-host-contracts.d.ts +9 -0
- package/core/dist/runtime-v2/internalization/rule-host-contracts.d.ts.map +1 -1
- package/core/dist/runtime-v2/internalization/v2-adversarial-cases.d.ts +55 -0
- package/core/dist/runtime-v2/internalization/v2-adversarial-cases.d.ts.map +1 -0
- package/core/dist/runtime-v2/internalization/v2-adversarial-cases.js +131 -0
- package/core/dist/runtime-v2/internalization/v2-adversarial-cases.js.map +1 -0
- package/core/dist/runtime-v2/pain-signal-observability.d.ts.map +1 -1
- package/core/dist/runtime-v2/pain-signal-observability.js +24 -1
- package/core/dist/runtime-v2/pain-signal-observability.js.map +1 -1
- package/core/dist/runtime-v2/risk/__tests__/risk-calculator-policy.test.d.ts +2 -0
- package/core/dist/runtime-v2/risk/__tests__/risk-calculator-policy.test.d.ts.map +1 -0
- package/core/dist/runtime-v2/risk/__tests__/risk-calculator-policy.test.js +95 -0
- package/core/dist/runtime-v2/risk/__tests__/risk-calculator-policy.test.js.map +1 -0
- package/core/dist/runtime-v2/risk/index.d.ts +7 -0
- package/core/dist/runtime-v2/risk/index.d.ts.map +1 -0
- package/core/dist/runtime-v2/risk/index.js +6 -0
- package/core/dist/runtime-v2/risk/index.js.map +1 -0
- package/core/dist/runtime-v2/risk/risk-calculator-policy.d.ts +18 -0
- package/core/dist/runtime-v2/risk/risk-calculator-policy.d.ts.map +1 -0
- package/core/dist/runtime-v2/risk/risk-calculator-policy.js +47 -0
- package/core/dist/runtime-v2/risk/risk-calculator-policy.js.map +1 -0
- package/core/dist/runtime-v2/store/intent/__tests__/sqlite-intent-doc-version-store.test.d.ts +2 -0
- package/core/dist/runtime-v2/store/intent/__tests__/sqlite-intent-doc-version-store.test.d.ts.map +1 -0
- package/core/dist/runtime-v2/store/intent/__tests__/sqlite-intent-doc-version-store.test.js +83 -0
- package/core/dist/runtime-v2/store/intent/__tests__/sqlite-intent-doc-version-store.test.js.map +1 -0
- package/core/dist/runtime-v2/store/intent/index.d.ts +2 -1
- package/core/dist/runtime-v2/store/intent/index.d.ts.map +1 -1
- package/core/dist/runtime-v2/store/intent/index.js +2 -1
- package/core/dist/runtime-v2/store/intent/index.js.map +1 -1
- package/core/dist/runtime-v2/store/intent/sqlite-intent-doc-version-store.d.ts +28 -0
- package/core/dist/runtime-v2/store/intent/sqlite-intent-doc-version-store.d.ts.map +1 -0
- package/core/dist/runtime-v2/store/intent/sqlite-intent-doc-version-store.js +88 -0
- package/core/dist/runtime-v2/store/intent/sqlite-intent-doc-version-store.js.map +1 -0
- package/core/dist/runtime-v2/store/sqlite-connection.d.ts.map +1 -1
- package/core/dist/runtime-v2/store/sqlite-connection.js +14 -0
- package/core/dist/runtime-v2/store/sqlite-connection.js.map +1 -1
- package/core/dist/runtime-v2/thinking-models/__tests__/thinking-models-policy.test.d.ts +2 -0
- package/core/dist/runtime-v2/thinking-models/__tests__/thinking-models-policy.test.d.ts.map +1 -0
- package/core/dist/runtime-v2/thinking-models/__tests__/thinking-models-policy.test.js +149 -0
- package/core/dist/runtime-v2/thinking-models/__tests__/thinking-models-policy.test.js.map +1 -0
- package/core/dist/runtime-v2/thinking-models/index.d.ts +3 -0
- package/core/dist/runtime-v2/thinking-models/index.d.ts.map +1 -0
- package/core/dist/runtime-v2/thinking-models/index.js +3 -0
- package/core/dist/runtime-v2/thinking-models/index.js.map +1 -0
- package/core/dist/runtime-v2/thinking-models/thinking-models-policy.d.ts +73 -0
- package/core/dist/runtime-v2/thinking-models/thinking-models-policy.d.ts.map +1 -0
- package/core/dist/runtime-v2/thinking-models/thinking-models-policy.js +227 -0
- package/core/dist/runtime-v2/thinking-models/thinking-models-policy.js.map +1 -0
- package/core/dist/runtime-v2/tools/__tests__/artificer-l2-tool-contract.test.js +40 -1
- package/core/dist/runtime-v2/tools/__tests__/artificer-l2-tool-contract.test.js.map +1 -1
- package/core/dist/runtime-v2/tools/artificer-l2-tool-contract.d.ts.map +1 -1
- package/core/dist/runtime-v2/tools/artificer-l2-tool-contract.js +28 -1
- package/core/dist/runtime-v2/tools/artificer-l2-tool-contract.js.map +1 -1
- package/core/dist/runtime-v2/tools/artificer-output-typebox.d.ts +3 -0
- package/core/dist/runtime-v2/tools/artificer-output-typebox.d.ts.map +1 -1
- package/core/dist/runtime-v2/tools/artificer-output-typebox.js +3 -0
- package/core/dist/runtime-v2/tools/artificer-output-typebox.js.map +1 -1
- package/core/package.json +4 -0
- package/package.json +1 -1
- package/pd-cli/dist/commands/__tests__/intent-flag-wiring.test.js +74 -3
- package/pd-cli/dist/commands/__tests__/intent-flag-wiring.test.js.map +1 -1
- package/pd-cli/dist/commands/intent.d.ts +21 -4
- package/pd-cli/dist/commands/intent.d.ts.map +1 -1
- package/pd-cli/dist/commands/intent.js +88 -27
- package/pd-cli/dist/commands/intent.js.map +1 -1
- package/plugin/dist/commands/pain.js +2 -2
- package/plugin/dist/core/behavior-example-pack-assembler.d.ts +43 -0
- package/plugin/dist/core/behavior-example-pack-assembler.js +247 -0
- package/plugin/dist/core/intent-doc-reader-adapter.d.ts +13 -5
- package/plugin/dist/core/intent-doc-reader-adapter.js +25 -5
- package/plugin/dist/core/intent-doc-reader.d.ts +13 -4
- package/plugin/dist/core/intent-doc-reader.js +45 -24
- package/plugin/dist/core/rule-context-assembler.d.ts +45 -0
- package/plugin/dist/core/rule-context-assembler.js +148 -0
- package/plugin/dist/core/thinking-models.d.ts +12 -57
- package/plugin/dist/core/thinking-models.js +18 -224
- package/plugin/dist/core/trajectory-types.d.ts +18 -0
- package/plugin/dist/core/trajectory.d.ts +34 -2
- package/plugin/dist/core/trajectory.js +103 -0
- package/plugin/dist/hooks/gate.js +59 -2
- package/plugin/dist/hooks/pain.js +2 -2
- package/plugin/dist/hooks/prompt.js +2 -1
- package/plugin/dist/index.js +1 -1
- package/plugin/package.json +1 -1
- package/plugin/dist/core/evolution-migration.d.ts +0 -5
- package/plugin/dist/core/evolution-migration.js +0 -65
- package/plugin/dist/core/risk-calculator.d.ts +0 -22
- package/plugin/dist/core/risk-calculator.js +0 -88
- package/plugin/dist/core/rule-host-helpers.d.ts +0 -8
- package/plugin/dist/core/rule-host-helpers.js +0 -7
- package/plugin/dist/core/rule-host-types.d.ts +0 -7
- package/plugin/dist/core/rule-host-types.js +0 -7
- package/plugin/dist/core/schema/db-types.d.ts +0 -11
- package/plugin/dist/core/schema/db-types.js +0 -5
- package/plugin/dist/core/schema/index.d.ts +0 -17
- package/plugin/dist/core/schema/index.js +0 -15
- package/plugin/dist/core/schema/migration-runner.d.ts +0 -53
- package/plugin/dist/core/schema/migration-runner.js +0 -170
- package/plugin/dist/core/schema/migrations/001-init-trajectory.d.ts +0 -6
- package/plugin/dist/core/schema/migrations/001-init-trajectory.js +0 -190
- package/plugin/dist/core/schema/migrations/002-init-central.d.ts +0 -5
- package/plugin/dist/core/schema/migrations/002-init-central.js +0 -108
- package/plugin/dist/core/schema/migrations/003-init-workflow.d.ts +0 -5
- package/plugin/dist/core/schema/migrations/003-init-workflow.js +0 -50
- package/plugin/dist/core/schema/migrations/004-add-thinking-and-gfi.d.ts +0 -8
- package/plugin/dist/core/schema/migrations/004-add-thinking-and-gfi.js +0 -66
- package/plugin/dist/core/schema/migrations/index.d.ts +0 -16
- package/plugin/dist/core/schema/migrations/index.js +0 -27
- package/plugin/dist/core/schema/schema-definitions.d.ts +0 -46
- package/plugin/dist/core/schema/schema-definitions.js +0 -599
- 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.
|
|
697
|
-
//
|
|
698
|
-
|
|
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:
|
|
702
|
-
error: '
|
|
703
|
-
message: `Cannot update feature flag: .pd/config.yaml has
|
|
704
|
-
|
|
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.
|
|
708
|
-
|
|
709
|
-
|
|
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: ${
|
|
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 = { ...
|
|
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
|
-
|
|
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:
|
|
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,
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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:
|
|
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
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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');
|