create-principles-disciple 1.76.0 → 1.78.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/feature-flags.d.ts +25 -0
- package/console/dist/server/config/feature-flags.js +101 -0
- package/console/dist/server/index.js +16 -0
- package/console/dist/server/models/FeedbackReportConsoleModel.d.ts +49 -0
- package/console/dist/server/models/FeedbackReportConsoleModel.js +242 -0
- package/console/dist/server/routes/feedback-reports.d.ts +18 -0
- package/console/dist/server/routes/feedback-reports.js +165 -0
- package/console/dist/ui/App.js +2 -1
- package/console/dist/ui/api.d.ts +26 -2
- package/console/dist/ui/api.js +18 -1
- package/console/dist/ui/components/app-sidebar.js +2 -1
- package/console/dist/ui/components/error-boundary.js +9 -2
- package/console/dist/ui/i18n/en.json +67 -1
- package/console/dist/ui/i18n/zh-CN.json +67 -1
- package/console/dist/ui/pages/ReportProblemPage.d.ts +1 -0
- package/console/dist/ui/pages/ReportProblemPage.js +186 -0
- package/console/dist/ui/pages/ReportProblemValidators.d.ts +51 -0
- package/console/dist/ui/pages/ReportProblemValidators.js +114 -0
- package/console/dist/web/assets/app.css +18 -0
- package/console/dist/web/assets/app.js +1047 -125
- package/console/package.json +1 -0
- package/core/dist/runtime-v2/__tests__/architecture-regression.test.js +24 -1
- package/core/dist/runtime-v2/__tests__/architecture-regression.test.js.map +1 -1
- package/core/dist/runtime-v2/__tests__/feedback/create-report.test.d.ts +2 -0
- package/core/dist/runtime-v2/__tests__/feedback/create-report.test.d.ts.map +1 -0
- package/core/dist/runtime-v2/__tests__/feedback/create-report.test.js +402 -0
- package/core/dist/runtime-v2/__tests__/feedback/create-report.test.js.map +1 -0
- package/core/dist/runtime-v2/__tests__/feedback/privacy-preview.test.d.ts +2 -0
- package/core/dist/runtime-v2/__tests__/feedback/privacy-preview.test.d.ts.map +1 -0
- package/core/dist/runtime-v2/__tests__/feedback/privacy-preview.test.js +128 -0
- package/core/dist/runtime-v2/__tests__/feedback/privacy-preview.test.js.map +1 -0
- package/core/dist/runtime-v2/__tests__/feedback/redact-sensitive.test.d.ts +2 -0
- package/core/dist/runtime-v2/__tests__/feedback/redact-sensitive.test.d.ts.map +1 -0
- package/core/dist/runtime-v2/__tests__/feedback/redact-sensitive.test.js +231 -0
- package/core/dist/runtime-v2/__tests__/feedback/redact-sensitive.test.js.map +1 -0
- package/core/dist/runtime-v2/__tests__/feedback/render-github-url.test.d.ts +2 -0
- package/core/dist/runtime-v2/__tests__/feedback/render-github-url.test.d.ts.map +1 -0
- package/core/dist/runtime-v2/__tests__/feedback/render-github-url.test.js +102 -0
- package/core/dist/runtime-v2/__tests__/feedback/render-github-url.test.js.map +1 -0
- package/core/dist/runtime-v2/__tests__/feedback/render-markdown.test.d.ts +2 -0
- package/core/dist/runtime-v2/__tests__/feedback/render-markdown.test.d.ts.map +1 -0
- package/core/dist/runtime-v2/__tests__/feedback/render-markdown.test.js +130 -0
- package/core/dist/runtime-v2/__tests__/feedback/render-markdown.test.js.map +1 -0
- package/core/dist/runtime-v2/__tests__/workspace-guidance-migration.test.d.ts +2 -0
- package/core/dist/runtime-v2/__tests__/workspace-guidance-migration.test.d.ts.map +1 -0
- package/core/dist/runtime-v2/__tests__/workspace-guidance-migration.test.js +328 -0
- package/core/dist/runtime-v2/__tests__/workspace-guidance-migration.test.js.map +1 -0
- package/core/dist/runtime-v2/activation/__tests__/prompt-activation-reader-contract.test.d.ts +2 -0
- package/core/dist/runtime-v2/activation/__tests__/prompt-activation-reader-contract.test.d.ts.map +1 -0
- package/core/dist/runtime-v2/activation/__tests__/prompt-activation-reader-contract.test.js +343 -0
- package/core/dist/runtime-v2/activation/__tests__/prompt-activation-reader-contract.test.js.map +1 -0
- package/core/dist/runtime-v2/activation/index.d.ts +2 -0
- package/core/dist/runtime-v2/activation/index.d.ts.map +1 -1
- package/core/dist/runtime-v2/activation/index.js +1 -0
- package/core/dist/runtime-v2/activation/index.js.map +1 -1
- package/core/dist/runtime-v2/activation/prompt-activation-reader-contract.d.ts +34 -0
- package/core/dist/runtime-v2/activation/prompt-activation-reader-contract.d.ts.map +1 -0
- package/core/dist/runtime-v2/activation/prompt-activation-reader-contract.js +109 -0
- package/core/dist/runtime-v2/activation/prompt-activation-reader-contract.js.map +1 -0
- package/core/dist/runtime-v2/feature-flags/__tests__/feature-flag-contract.test.js +13 -0
- package/core/dist/runtime-v2/feature-flags/__tests__/feature-flag-contract.test.js.map +1 -1
- package/core/dist/runtime-v2/feature-flags/__tests__/plugin-surface-registry.test.d.ts +2 -0
- package/core/dist/runtime-v2/feature-flags/__tests__/plugin-surface-registry.test.d.ts.map +1 -0
- package/core/dist/runtime-v2/feature-flags/__tests__/plugin-surface-registry.test.js +141 -0
- package/core/dist/runtime-v2/feature-flags/__tests__/plugin-surface-registry.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 +4 -1
- 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 +1 -0
- package/core/dist/runtime-v2/feature-flags/index.js.map +1 -1
- package/core/dist/runtime-v2/feature-flags/plugin-surface-registry.d.ts +25 -0
- package/core/dist/runtime-v2/feature-flags/plugin-surface-registry.d.ts.map +1 -0
- package/core/dist/runtime-v2/feature-flags/plugin-surface-registry.js +233 -0
- package/core/dist/runtime-v2/feature-flags/plugin-surface-registry.js.map +1 -0
- package/core/dist/runtime-v2/feedback/create-report.d.ts +14 -0
- package/core/dist/runtime-v2/feedback/create-report.d.ts.map +1 -0
- package/core/dist/runtime-v2/feedback/create-report.js +257 -0
- package/core/dist/runtime-v2/feedback/create-report.js.map +1 -0
- package/core/dist/runtime-v2/feedback/feedback-types.d.ts +120 -0
- package/core/dist/runtime-v2/feedback/feedback-types.d.ts.map +1 -0
- package/core/dist/runtime-v2/feedback/feedback-types.js +243 -0
- package/core/dist/runtime-v2/feedback/feedback-types.js.map +1 -0
- package/core/dist/runtime-v2/feedback/index.d.ts +9 -0
- package/core/dist/runtime-v2/feedback/index.d.ts.map +1 -0
- package/core/dist/runtime-v2/feedback/index.js +11 -0
- package/core/dist/runtime-v2/feedback/index.js.map +1 -0
- package/core/dist/runtime-v2/feedback/internal-guards.d.ts +2 -0
- package/core/dist/runtime-v2/feedback/internal-guards.d.ts.map +1 -0
- package/core/dist/runtime-v2/feedback/internal-guards.js +6 -0
- package/core/dist/runtime-v2/feedback/internal-guards.js.map +1 -0
- package/core/dist/runtime-v2/feedback/privacy-preview.d.ts +14 -0
- package/core/dist/runtime-v2/feedback/privacy-preview.d.ts.map +1 -0
- package/core/dist/runtime-v2/feedback/privacy-preview.js +128 -0
- package/core/dist/runtime-v2/feedback/privacy-preview.js.map +1 -0
- package/core/dist/runtime-v2/feedback/redact-sensitive.d.ts +37 -0
- package/core/dist/runtime-v2/feedback/redact-sensitive.d.ts.map +1 -0
- package/core/dist/runtime-v2/feedback/redact-sensitive.js +272 -0
- package/core/dist/runtime-v2/feedback/redact-sensitive.js.map +1 -0
- package/core/dist/runtime-v2/feedback/render-github-url.d.ts +21 -0
- package/core/dist/runtime-v2/feedback/render-github-url.d.ts.map +1 -0
- package/core/dist/runtime-v2/feedback/render-github-url.js +57 -0
- package/core/dist/runtime-v2/feedback/render-github-url.js.map +1 -0
- package/core/dist/runtime-v2/feedback/render-markdown.d.ts +12 -0
- package/core/dist/runtime-v2/feedback/render-markdown.d.ts.map +1 -0
- package/core/dist/runtime-v2/feedback/render-markdown.js +166 -0
- package/core/dist/runtime-v2/feedback/render-markdown.js.map +1 -0
- package/core/dist/runtime-v2/feedback/safe-stringify.d.ts +6 -0
- package/core/dist/runtime-v2/feedback/safe-stringify.d.ts.map +1 -0
- package/core/dist/runtime-v2/feedback/safe-stringify.js +115 -0
- package/core/dist/runtime-v2/feedback/safe-stringify.js.map +1 -0
- package/core/dist/runtime-v2/index.d.ts +8 -1
- package/core/dist/runtime-v2/index.d.ts.map +1 -1
- package/core/dist/runtime-v2/index.js +12 -1
- package/core/dist/runtime-v2/index.js.map +1 -1
- package/core/dist/runtime-v2/workspace-guidance-migration.d.ts +12 -0
- package/core/dist/runtime-v2/workspace-guidance-migration.d.ts.map +1 -0
- package/core/dist/runtime-v2/workspace-guidance-migration.js +193 -0
- package/core/dist/runtime-v2/workspace-guidance-migration.js.map +1 -0
- package/core/package.json +4 -0
- package/package.json +1 -1
- package/plugin/dist/commands/context.js +5 -5
- package/plugin/dist/commands/strategy.d.ts +0 -1
- package/plugin/dist/commands/strategy.js +0 -14
- package/plugin/dist/core/runtime-v2-prompt-activation-reader.d.ts +4 -13
- package/plugin/dist/core/runtime-v2-prompt-activation-reader.js +36 -74
- package/plugin/dist/core/surface-guard.d.ts +27 -0
- package/plugin/dist/core/surface-guard.js +82 -0
- package/plugin/dist/core/workspace-guidance-migrator.d.ts +12 -0
- package/plugin/dist/core/workspace-guidance-migrator.js +141 -0
- package/plugin/dist/hooks/prompt.js +20 -143
- package/plugin/dist/i18n/commands.js +2 -14
- package/plugin/dist/index.d.ts +27 -0
- package/plugin/dist/index.js +180 -65
- package/plugin/dist/service/correction-observer-service.d.ts +11 -0
- package/plugin/dist/service/correction-observer-service.js +172 -0
- package/plugin/dist/service/evolution-worker.js +1 -110
- package/plugin/dist/types.d.ts +2 -2
- package/plugin/dist/types.js +3 -3
- package/plugin/templates/langs/en/core/BOOTSTRAP.md +5 -19
- package/plugin/templates/langs/en/principles/THINKING_OS.md +1 -1
- package/plugin/templates/langs/en/skills/admin/SKILL.md +2 -2
- package/plugin/templates/langs/en/skills/bootstrap-tools/SKILL.md +1 -1
- package/plugin/templates/langs/en/skills/init-strategy/SKILL.md +1 -1
- package/plugin/templates/langs/en/skills/pd-mentor/SKILL.md +8 -23
- package/plugin/templates/langs/en/skills/reflection/SKILL.md +2 -2
- package/plugin/templates/langs/en/skills/report/SKILL.md +1 -1
- package/plugin/templates/langs/zh/core/BOOTSTRAP.md +3 -16
- package/plugin/templates/langs/zh/principles/THINKING_OS.md +1 -1
- package/plugin/templates/langs/zh/skills/admin/SKILL.md +2 -2
- package/plugin/templates/langs/zh/skills/bootstrap-tools/SKILL.md +1 -1
- package/plugin/templates/langs/zh/skills/init-strategy/SKILL.md +1 -1
- package/plugin/templates/langs/zh/skills/pd-mentor/SKILL.md +7 -22
- package/plugin/templates/langs/zh/skills/reflection/SKILL.md +2 -2
- package/plugin/templates/langs/zh/skills/report/SKILL.md +1 -1
- package/plugin/templates/workspace/.principles/THINKING_OS.md +1 -1
- package/templates/langs/en/core/BOOTSTRAP.md +1 -1
- package/templates/langs/en/principles/THINKING_OS.md +1 -1
- package/templates/langs/en/skills/admin/SKILL.md +2 -2
- package/templates/langs/en/skills/pd-grooming/SKILL.md +1 -1
- package/templates/langs/en/skills/reflection/SKILL.md +2 -2
- package/templates/langs/en/skills/report/SKILL.md +1 -1
- package/templates/langs/zh/core/BOOTSTRAP.md +1 -1
- package/templates/langs/zh/principles/THINKING_OS.md +1 -1
- package/templates/langs/zh/skills/admin/SKILL.md +2 -2
- package/templates/langs/zh/skills/pd-grooming/SKILL.md +1 -1
- package/templates/langs/zh/skills/reflection/SKILL.md +2 -2
- package/templates/langs/zh/skills/report/SKILL.md +1 -1
- package/plugin/templates/langs/en/skills/ai-sprint-orchestration/EXAMPLES.md +0 -63
- package/plugin/templates/langs/en/skills/ai-sprint-orchestration/REFERENCE.md +0 -136
- package/plugin/templates/langs/en/skills/ai-sprint-orchestration/SKILL.md +0 -67
- package/plugin/templates/langs/en/skills/ai-sprint-orchestration/references/agent-registry.json +0 -143
- package/plugin/templates/langs/en/skills/ai-sprint-orchestration/references/specs/bugfix-complex-template.json +0 -107
- package/plugin/templates/langs/en/skills/ai-sprint-orchestration/references/specs/feature-complex-template.json +0 -107
- package/plugin/templates/langs/en/skills/ai-sprint-orchestration/references/specs/workflow-validation-minimal-verify.json +0 -95
- package/plugin/templates/langs/en/skills/ai-sprint-orchestration/references/specs/workflow-validation-minimal.json +0 -98
- package/plugin/templates/langs/en/skills/ai-sprint-orchestration/references/workflow-v1-acceptance-checklist.md +0 -58
- package/plugin/templates/langs/en/skills/ai-sprint-orchestration/references/workflow-v1.4-work-unit-handoff.md +0 -190
- package/plugin/templates/langs/en/skills/ai-sprint-orchestration/scripts/lib/archive.mjs +0 -310
- package/plugin/templates/langs/en/skills/ai-sprint-orchestration/scripts/lib/contract-enforcement.mjs +0 -683
- package/plugin/templates/langs/en/skills/ai-sprint-orchestration/scripts/lib/decision.mjs +0 -604
- package/plugin/templates/langs/en/skills/ai-sprint-orchestration/scripts/lib/state-store.mjs +0 -32
- package/plugin/templates/langs/en/skills/ai-sprint-orchestration/scripts/lib/task-specs.mjs +0 -707
- package/plugin/templates/langs/en/skills/ai-sprint-orchestration/scripts/run.mjs +0 -3455
- package/plugin/templates/langs/en/skills/evolve-system/SKILL.md +0 -46
- package/plugin/templates/langs/en/skills/manage-okr/SKILL.md +0 -96
- package/plugin/templates/langs/en/skills/pd-daily/SKILL.md +0 -199
- package/plugin/templates/langs/en/skills/pd-grooming/SKILL.md +0 -46
- package/plugin/templates/langs/zh/skills/ai-sprint-orchestration/EXAMPLES.md +0 -63
- package/plugin/templates/langs/zh/skills/ai-sprint-orchestration/REFERENCE.md +0 -136
- package/plugin/templates/langs/zh/skills/ai-sprint-orchestration/SKILL.md +0 -67
- package/plugin/templates/langs/zh/skills/ai-sprint-orchestration/references/agent-registry.json +0 -143
- package/plugin/templates/langs/zh/skills/ai-sprint-orchestration/references/specs/bugfix-complex-template.json +0 -107
- package/plugin/templates/langs/zh/skills/ai-sprint-orchestration/references/specs/feature-complex-template.json +0 -107
- package/plugin/templates/langs/zh/skills/ai-sprint-orchestration/references/specs/nocturnal-trinity-quality-enhancement.json +0 -111
- package/plugin/templates/langs/zh/skills/ai-sprint-orchestration/references/specs/workflow-validation-minimal-verify.json +0 -95
- package/plugin/templates/langs/zh/skills/ai-sprint-orchestration/references/specs/workflow-validation-minimal.json +0 -98
- package/plugin/templates/langs/zh/skills/ai-sprint-orchestration/references/workflow-v1-acceptance-checklist.md +0 -58
- package/plugin/templates/langs/zh/skills/ai-sprint-orchestration/references/workflow-v1.4-work-unit-handoff.md +0 -190
- package/plugin/templates/langs/zh/skills/ai-sprint-orchestration/runtime/.gitignore +0 -2
- package/plugin/templates/langs/zh/skills/ai-sprint-orchestration/scripts/lib/archive.mjs +0 -310
- package/plugin/templates/langs/zh/skills/ai-sprint-orchestration/scripts/lib/contract-enforcement.mjs +0 -683
- package/plugin/templates/langs/zh/skills/ai-sprint-orchestration/scripts/lib/decision.mjs +0 -604
- package/plugin/templates/langs/zh/skills/ai-sprint-orchestration/scripts/lib/state-store.mjs +0 -32
- package/plugin/templates/langs/zh/skills/ai-sprint-orchestration/scripts/lib/task-specs.mjs +0 -707
- package/plugin/templates/langs/zh/skills/ai-sprint-orchestration/scripts/run.mjs +0 -3455
- package/plugin/templates/langs/zh/skills/ai-sprint-orchestration/test/archive.test.mjs +0 -230
- package/plugin/templates/langs/zh/skills/ai-sprint-orchestration/test/contract-enforcement.test.mjs +0 -672
- package/plugin/templates/langs/zh/skills/ai-sprint-orchestration/test/decision.test.mjs +0 -1321
- package/plugin/templates/langs/zh/skills/ai-sprint-orchestration/test/run.test.mjs +0 -1435
- package/plugin/templates/langs/zh/skills/evolve-system/SKILL.md +0 -46
- package/plugin/templates/langs/zh/skills/manage-okr/SKILL.md +0 -109
- package/plugin/templates/langs/zh/skills/pd-daily/SKILL.md +0 -283
- package/plugin/templates/langs/zh/skills/pd-grooming/SKILL.md +0 -46
- package/plugin/templates/workspace/PLAN.md +0 -2
- package/templates/workspace/PLAN.md +0 -2
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { EffectiveFeatureFlags } from '@principles/core/runtime-v2';
|
|
2
|
+
export interface FeatureFlagLoadResult {
|
|
3
|
+
ok: true;
|
|
4
|
+
flags: EffectiveFeatureFlags;
|
|
5
|
+
}
|
|
6
|
+
export interface FeatureFlagLoadError {
|
|
7
|
+
ok: false;
|
|
8
|
+
reason: string;
|
|
9
|
+
nextAction: string;
|
|
10
|
+
}
|
|
11
|
+
export type LoadFeatureFlagsResult = FeatureFlagLoadResult | FeatureFlagLoadError;
|
|
12
|
+
/**
|
|
13
|
+
* Load effective feature flags for the given workspace.
|
|
14
|
+
*
|
|
15
|
+
* Returns ok:false with reason+nextAction on failure (fail-closed).
|
|
16
|
+
* Does NOT throw — callers must check .ok.
|
|
17
|
+
*/
|
|
18
|
+
export declare function loadWorkspaceFeatureFlags(workspaceDir: string): LoadFeatureFlagsResult;
|
|
19
|
+
/**
|
|
20
|
+
* Build the minimal featureFlags object expected by handleFeedbackReportsRoute.
|
|
21
|
+
* Fail-closed: if loading fails, returns feedback_channel enabled=false.
|
|
22
|
+
*/
|
|
23
|
+
export declare function buildFeedbackChannelFlags(loadResult: LoadFeatureFlagsResult): Record<string, {
|
|
24
|
+
enabled: boolean;
|
|
25
|
+
}>;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Feature flag loader for pd-console server.
|
|
3
|
+
*
|
|
4
|
+
* Reads <workspaceDir>/.pd/feature-flags.yaml and merges with
|
|
5
|
+
* core defaults via computeEffectiveFlags.
|
|
6
|
+
*
|
|
7
|
+
* Fail-closed: if loading fails, the returned result indicates failure
|
|
8
|
+
* so callers can deny write paths that depend on feature flags.
|
|
9
|
+
*/
|
|
10
|
+
import * as fs from 'fs';
|
|
11
|
+
import * as path from 'path';
|
|
12
|
+
import * as yaml from 'js-yaml';
|
|
13
|
+
import { computeEffectiveFlags, DEFAULT_FEATURE_FLAGS, } from '@principles/core/runtime-v2';
|
|
14
|
+
const FEATURE_FLAGS_CONFIG_FILENAME = 'feature-flags.yaml';
|
|
15
|
+
const FEATURE_FLAGS_CONFIG_DIR = '.pd';
|
|
16
|
+
const DANGEROUS_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
|
|
17
|
+
function isRecord(value) {
|
|
18
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
19
|
+
}
|
|
20
|
+
function readConfigFile(configPath) {
|
|
21
|
+
try {
|
|
22
|
+
return { ok: true, content: fs.readFileSync(configPath, 'utf8') };
|
|
23
|
+
}
|
|
24
|
+
catch (err) {
|
|
25
|
+
return {
|
|
26
|
+
ok: false,
|
|
27
|
+
reason: `Failed to read feature flags config: ${err instanceof Error ? err.message : String(err)}`,
|
|
28
|
+
nextAction: 'Verify file permissions on .pd/feature-flags.yaml and retry',
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function parseYaml(content) {
|
|
33
|
+
try {
|
|
34
|
+
return { ok: true, value: yaml.load(content, { schema: yaml.JSON_SCHEMA }) };
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return {
|
|
38
|
+
ok: false,
|
|
39
|
+
reason: 'feature-flags.yaml: YAML parse error',
|
|
40
|
+
nextAction: 'Fix YAML syntax in .pd/feature-flags.yaml or delete the file to use defaults',
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Load effective feature flags for the given workspace.
|
|
46
|
+
*
|
|
47
|
+
* Returns ok:false with reason+nextAction on failure (fail-closed).
|
|
48
|
+
* Does NOT throw — callers must check .ok.
|
|
49
|
+
*/
|
|
50
|
+
export function loadWorkspaceFeatureFlags(workspaceDir) {
|
|
51
|
+
const configPath = path.join(workspaceDir, FEATURE_FLAGS_CONFIG_DIR, FEATURE_FLAGS_CONFIG_FILENAME);
|
|
52
|
+
if (!fs.existsSync(configPath)) {
|
|
53
|
+
// No config file — use defaults (feedback_channel enabled by default)
|
|
54
|
+
return {
|
|
55
|
+
ok: true,
|
|
56
|
+
flags: computeEffectiveFlags({}, DEFAULT_FEATURE_FLAGS, configPath),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
const readResult = readConfigFile(configPath);
|
|
60
|
+
if (!readResult.ok) {
|
|
61
|
+
return readResult;
|
|
62
|
+
}
|
|
63
|
+
const parseResult = parseYaml(readResult.content);
|
|
64
|
+
if (!parseResult.ok) {
|
|
65
|
+
return parseResult;
|
|
66
|
+
}
|
|
67
|
+
if (!isRecord(parseResult.value)) {
|
|
68
|
+
return {
|
|
69
|
+
ok: false,
|
|
70
|
+
reason: 'feature-flags.yaml: expected a YAML mapping, got ' + (Array.isArray(parseResult.value) ? 'array' : typeof parseResult.value),
|
|
71
|
+
nextAction: 'Replace .pd/feature-flags.yaml content with a valid YAML mapping or delete it',
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
const safeRecord = Object.create(null);
|
|
75
|
+
for (const key of Object.keys(parseResult.value)) {
|
|
76
|
+
if (DANGEROUS_KEYS.has(key)) {
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if (Object.hasOwn(parseResult.value, key)) {
|
|
80
|
+
safeRecord[key] = parseResult.value[key];
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
ok: true,
|
|
85
|
+
flags: computeEffectiveFlags(safeRecord, DEFAULT_FEATURE_FLAGS, configPath),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Build the minimal featureFlags object expected by handleFeedbackReportsRoute.
|
|
90
|
+
* Fail-closed: if loading fails, returns feedback_channel enabled=false.
|
|
91
|
+
*/
|
|
92
|
+
export function buildFeedbackChannelFlags(loadResult) {
|
|
93
|
+
if (!loadResult.ok) {
|
|
94
|
+
// Fail closed — disable feedback_channel when flags cannot be loaded
|
|
95
|
+
return { feedback_channel: { enabled: false } };
|
|
96
|
+
}
|
|
97
|
+
const feedbackDef = loadResult.flags.flags.feedback_channel;
|
|
98
|
+
return {
|
|
99
|
+
feedback_channel: { enabled: feedbackDef?.enabled ?? false },
|
|
100
|
+
};
|
|
101
|
+
}
|
|
@@ -9,6 +9,8 @@ import { WorkspaceService } from './models/WorkspaceService.js';
|
|
|
9
9
|
import { handleOverviewRoute, disposeOverviewModels } from './routes/overview.js';
|
|
10
10
|
import { handleGatesRoute, disposeGateModels } from './routes/gates.js';
|
|
11
11
|
import { handleFeedbackRoute, disposeFeedbackModels } from './routes/feedback.js';
|
|
12
|
+
import { handleFeedbackReportsRoute, disposeFeedbackReportModels } from './routes/feedback-reports.js';
|
|
13
|
+
import { loadWorkspaceFeatureFlags, buildFeedbackChannelFlags, } from './config/feature-flags.js';
|
|
12
14
|
import { handleSamplesRoute, disposeSampleModels } from './routes/samples.js';
|
|
13
15
|
import { handleApprovalsRoute, disposeApprovalsModels } from './routes/approvals.js';
|
|
14
16
|
import { handleEvolutionRoute, disposeEvolutionModels } from './routes/evolution.js';
|
|
@@ -201,6 +203,12 @@ async function initServices(workspaceDir, authConfig) {
|
|
|
201
203
|
});
|
|
202
204
|
const configStore = new WorkspaceConfigStore();
|
|
203
205
|
const workspaceService = new WorkspaceService(configStore);
|
|
206
|
+
// Load feature flags — fail-closed: on error, feedback_channel is disabled
|
|
207
|
+
const flagLoadResult = loadWorkspaceFeatureFlags(workspaceDir);
|
|
208
|
+
const feedbackFlags = buildFeedbackChannelFlags(flagLoadResult);
|
|
209
|
+
if (!flagLoadResult.ok) {
|
|
210
|
+
console.warn('[pd-console] Feature flag loading failed (feedback channel disabled):', flagLoadResult.reason);
|
|
211
|
+
}
|
|
204
212
|
return {
|
|
205
213
|
stateManager,
|
|
206
214
|
healthReadModel,
|
|
@@ -211,12 +219,14 @@ async function initServices(workspaceDir, authConfig) {
|
|
|
211
219
|
authConfig,
|
|
212
220
|
configStore,
|
|
213
221
|
workspaceService,
|
|
222
|
+
feedbackFlags,
|
|
214
223
|
};
|
|
215
224
|
}
|
|
216
225
|
async function closeServices(services) {
|
|
217
226
|
disposeOverviewModels();
|
|
218
227
|
disposeGateModels();
|
|
219
228
|
disposeFeedbackModels();
|
|
229
|
+
disposeFeedbackReportModels();
|
|
220
230
|
disposeSampleModels();
|
|
221
231
|
disposeApprovalsModels();
|
|
222
232
|
disposeEvolutionModels();
|
|
@@ -292,6 +302,12 @@ function handleRequest(services) {
|
|
|
292
302
|
asyncHandler(() => handleGatesRoute(req, res, services.workspaceDir, subPath))(req, res);
|
|
293
303
|
return;
|
|
294
304
|
}
|
|
305
|
+
// GET/POST /api/feedback/reports, /api/feedback/reports/:id — local MVP seed feedback drafts (PRI-285)
|
|
306
|
+
if (urlPath === '/api/feedback/reports' || urlPath.startsWith('/api/feedback/reports/')) {
|
|
307
|
+
const subPath = urlPath.slice('/api/feedback/reports'.length);
|
|
308
|
+
asyncHandler(() => handleFeedbackReportsRoute(req, res, { workspaceDir: services.workspaceDir, subPath, featureFlags: services.feedbackFlags }))(req, res);
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
295
311
|
// GET /api/feedback/gfi, /api/feedback/empathy-events, /api/feedback/gate-blocks
|
|
296
312
|
if (urlPath === '/api/feedback' || urlPath.startsWith('/api/feedback/')) {
|
|
297
313
|
const subPath = urlPath.slice('/api/feedback'.length);
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { FeedbackReport } from '@principles/core/runtime-v2/feedback';
|
|
2
|
+
export type FeedbackReportDraftSummary = {
|
|
3
|
+
id: string;
|
|
4
|
+
createdAt: string;
|
|
5
|
+
type: string;
|
|
6
|
+
title: string;
|
|
7
|
+
};
|
|
8
|
+
export type FeedbackReportListResult = {
|
|
9
|
+
ok: boolean;
|
|
10
|
+
drafts: FeedbackReportDraftSummary[];
|
|
11
|
+
error?: string;
|
|
12
|
+
nextAction?: string;
|
|
13
|
+
};
|
|
14
|
+
export type FeedbackReportGetResult = {
|
|
15
|
+
ok: boolean;
|
|
16
|
+
report?: FeedbackReport;
|
|
17
|
+
error?: string;
|
|
18
|
+
errorCode?: 'NOT_FOUND' | 'INVALID_ID' | 'READ_ERROR';
|
|
19
|
+
nextAction?: string;
|
|
20
|
+
};
|
|
21
|
+
export type FeedbackReportDeleteResult = {
|
|
22
|
+
ok: boolean;
|
|
23
|
+
error?: string;
|
|
24
|
+
nextAction?: string;
|
|
25
|
+
};
|
|
26
|
+
export declare class FeedbackReportConsoleModel {
|
|
27
|
+
private readonly workspaceDir;
|
|
28
|
+
private readonly draftsDir;
|
|
29
|
+
constructor(workspaceDir: string);
|
|
30
|
+
private ensureDraftsDir;
|
|
31
|
+
/**
|
|
32
|
+
* Persist a generated feedback report draft to disk.
|
|
33
|
+
* Returns the resulting summary plus a structured error if writing failed.
|
|
34
|
+
*/
|
|
35
|
+
create(report: FeedbackReport): Promise<FeedbackReportGetResult>;
|
|
36
|
+
/**
|
|
37
|
+
* List all draft summaries (id, createdAt, type, title), newest-first.
|
|
38
|
+
*/
|
|
39
|
+
list(): Promise<FeedbackReportListResult>;
|
|
40
|
+
/**
|
|
41
|
+
* Load a draft by id. Returns ok:false + nextAction when missing or invalid.
|
|
42
|
+
*/
|
|
43
|
+
get(id: string): Promise<FeedbackReportGetResult>;
|
|
44
|
+
/**
|
|
45
|
+
* Delete a draft by id. No-op (still ok:true) if the file does not exist.
|
|
46
|
+
*/
|
|
47
|
+
delete(id: string): Promise<FeedbackReportDeleteResult>;
|
|
48
|
+
dispose(): void;
|
|
49
|
+
}
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
// FeedbackReportConsoleModel.ts
|
|
2
|
+
// Stores MVP seed feedback report drafts locally on disk at:
|
|
3
|
+
// <workspaceDir>/.pd/feedback/drafts/<id>.json
|
|
4
|
+
//
|
|
5
|
+
// This is a writeable but non-publishing channel. Drafts are produced by
|
|
6
|
+
// `createFeedbackReport` (in @principles/core) and then surfaced back to the
|
|
7
|
+
// user so they can copy/paste them into GitHub, email, or the maintainer.
|
|
8
|
+
// This model never reaches the network — it only reads/writes local files.
|
|
9
|
+
import * as fs from 'fs';
|
|
10
|
+
import * as path from 'path';
|
|
11
|
+
import { randomUUID } from 'crypto';
|
|
12
|
+
/**
|
|
13
|
+
* Best-effort error message extraction from an `unknown` caught value.
|
|
14
|
+
* Returns `String(err)` if no `.message` is present.
|
|
15
|
+
*/
|
|
16
|
+
function errMsg(e, err) {
|
|
17
|
+
// Check the unknown caught value (err) for a string message first.
|
|
18
|
+
if (err !== null && err !== undefined && typeof err === 'object' && 'message' in err) {
|
|
19
|
+
const msg = err.message;
|
|
20
|
+
if (typeof msg === 'string')
|
|
21
|
+
return msg;
|
|
22
|
+
}
|
|
23
|
+
if (typeof err === 'string')
|
|
24
|
+
return err;
|
|
25
|
+
return String(err);
|
|
26
|
+
}
|
|
27
|
+
const FEEDBACK_DIR = '.pd/feedback';
|
|
28
|
+
const DRAFTS_DIR = 'drafts';
|
|
29
|
+
function ensureDir(dir) {
|
|
30
|
+
try {
|
|
31
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
32
|
+
return { ok: true };
|
|
33
|
+
}
|
|
34
|
+
catch (err) {
|
|
35
|
+
return {
|
|
36
|
+
ok: false,
|
|
37
|
+
error: `Failed to create directory ${dir}: ${err instanceof Error ? err.message : String(err)}`,
|
|
38
|
+
nextAction: 'verify the workspace directory is writable and that the parent exists',
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function reportIdValidator(id) {
|
|
43
|
+
if (typeof id !== 'string')
|
|
44
|
+
return false;
|
|
45
|
+
if (id.length === 0 || id.length > 256)
|
|
46
|
+
return false;
|
|
47
|
+
// Only allow safe filename characters; reject path traversal attempts.
|
|
48
|
+
if (id.includes('..') || id.includes('/') || id.includes('\\'))
|
|
49
|
+
return false;
|
|
50
|
+
return /^[A-Za-z0-9._-]+$/.test(id);
|
|
51
|
+
}
|
|
52
|
+
function validateReportShape(value) {
|
|
53
|
+
if (value === null || typeof value !== 'object')
|
|
54
|
+
return false;
|
|
55
|
+
const v = value;
|
|
56
|
+
return (typeof v.id === 'string' &&
|
|
57
|
+
typeof v.createdAt === 'string' &&
|
|
58
|
+
typeof v.type === 'string' &&
|
|
59
|
+
typeof v.title === 'string' &&
|
|
60
|
+
typeof v.userText === 'object' &&
|
|
61
|
+
v.userText !== null &&
|
|
62
|
+
typeof v.diagnosticSummary === 'object' &&
|
|
63
|
+
v.diagnosticSummary !== null);
|
|
64
|
+
}
|
|
65
|
+
export class FeedbackReportConsoleModel {
|
|
66
|
+
workspaceDir;
|
|
67
|
+
draftsDir;
|
|
68
|
+
constructor(workspaceDir) {
|
|
69
|
+
this.workspaceDir = workspaceDir;
|
|
70
|
+
this.draftsDir = path.join(workspaceDir, FEEDBACK_DIR, DRAFTS_DIR);
|
|
71
|
+
}
|
|
72
|
+
ensureDraftsDir() {
|
|
73
|
+
return ensureDir(this.draftsDir);
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Persist a generated feedback report draft to disk.
|
|
77
|
+
* Returns the resulting summary plus a structured error if writing failed.
|
|
78
|
+
*/
|
|
79
|
+
async create(report) {
|
|
80
|
+
if (!validateReportShape(report)) {
|
|
81
|
+
return {
|
|
82
|
+
ok: false,
|
|
83
|
+
error: 'FeedbackReport is missing required fields (id, createdAt, type, title, userText, diagnosticSummary)',
|
|
84
|
+
nextAction: 'call createFeedbackReport() to generate a validated report, then submit it',
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
const dir = this.ensureDraftsDir();
|
|
88
|
+
if (!dir.ok)
|
|
89
|
+
return dir;
|
|
90
|
+
const filePath = path.join(this.draftsDir, `${report.id}.json`);
|
|
91
|
+
try {
|
|
92
|
+
// Atomic write: serialize to a tmp file, then rename.
|
|
93
|
+
const tmpPath = `${filePath}.${randomUUID()}.tmp`;
|
|
94
|
+
const body = JSON.stringify(report, null, 2);
|
|
95
|
+
await fs.promises.writeFile(tmpPath, body, { encoding: 'utf8', mode: 0o600 });
|
|
96
|
+
await fs.promises.rename(tmpPath, filePath);
|
|
97
|
+
return { ok: true, report };
|
|
98
|
+
}
|
|
99
|
+
catch (err) {
|
|
100
|
+
return {
|
|
101
|
+
ok: false,
|
|
102
|
+
error: `Failed to write feedback report draft at ${filePath}: ${err instanceof Error ? err.message : String(err)}`,
|
|
103
|
+
nextAction: 'verify the workspace directory is writable and that disk space is available',
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* List all draft summaries (id, createdAt, type, title), newest-first.
|
|
109
|
+
*/
|
|
110
|
+
async list() {
|
|
111
|
+
const dir = this.ensureDraftsDir();
|
|
112
|
+
if (!dir.ok) {
|
|
113
|
+
return {
|
|
114
|
+
ok: false,
|
|
115
|
+
drafts: [],
|
|
116
|
+
error: dir.error,
|
|
117
|
+
nextAction: dir.nextAction,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
try {
|
|
121
|
+
const entries = await fs.promises.readdir(this.draftsDir, { withFileTypes: true });
|
|
122
|
+
const files = entries.filter((e) => e.isFile() && e.name.endsWith('.json'));
|
|
123
|
+
const drafts = [];
|
|
124
|
+
for (const f of files) {
|
|
125
|
+
const filePath = path.join(this.draftsDir, f.name);
|
|
126
|
+
try {
|
|
127
|
+
const raw = await fs.promises.readFile(filePath, 'utf8');
|
|
128
|
+
const parsed = JSON.parse(raw);
|
|
129
|
+
if (parsed !== null &&
|
|
130
|
+
typeof parsed === 'object' &&
|
|
131
|
+
typeof parsed.id === 'string' &&
|
|
132
|
+
typeof parsed.createdAt === 'string' &&
|
|
133
|
+
typeof parsed.type === 'string' &&
|
|
134
|
+
typeof parsed.title === 'string') {
|
|
135
|
+
const obj = parsed;
|
|
136
|
+
drafts.push({
|
|
137
|
+
id: obj.id,
|
|
138
|
+
createdAt: obj.createdAt,
|
|
139
|
+
type: obj.type,
|
|
140
|
+
title: obj.title,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
catch (readErr) {
|
|
145
|
+
// Skip unreadable/malformed draft — but record diagnostics for observability.
|
|
146
|
+
const skipReason = readErr instanceof Error ? readErr.message : String(readErr);
|
|
147
|
+
console.warn(`[FeedbackReportConsoleModel] skipping unreadable draft "${f.name}": ${skipReason}`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
drafts.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
|
|
151
|
+
return { ok: true, drafts };
|
|
152
|
+
}
|
|
153
|
+
catch (err) {
|
|
154
|
+
return {
|
|
155
|
+
ok: false,
|
|
156
|
+
drafts: [],
|
|
157
|
+
error: `Failed to read feedback drafts directory ${this.draftsDir}: ${err instanceof Error ? err.message : String(err)}`,
|
|
158
|
+
nextAction: 'verify the workspace directory exists and is readable',
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Load a draft by id. Returns ok:false + nextAction when missing or invalid.
|
|
164
|
+
*/
|
|
165
|
+
async get(id) {
|
|
166
|
+
if (!reportIdValidator(id)) {
|
|
167
|
+
return {
|
|
168
|
+
ok: false,
|
|
169
|
+
error: `Invalid feedback report id: ${JSON.stringify(id)}`,
|
|
170
|
+
errorCode: 'INVALID_ID',
|
|
171
|
+
nextAction: 'pass a safe id matching /^[A-Za-z0-9._-]+$/ with length 1-256',
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
const dir = this.ensureDraftsDir();
|
|
175
|
+
if (!dir.ok)
|
|
176
|
+
return dir;
|
|
177
|
+
const filePath = path.join(this.draftsDir, `${id}.json`);
|
|
178
|
+
try {
|
|
179
|
+
const raw = await fs.promises.readFile(filePath, 'utf8');
|
|
180
|
+
const parsed = JSON.parse(raw);
|
|
181
|
+
if (!validateReportShape(parsed)) {
|
|
182
|
+
return {
|
|
183
|
+
ok: false,
|
|
184
|
+
error: `Feedback report at ${filePath} is malformed`,
|
|
185
|
+
nextAction: 'delete the corrupted draft and create a new one',
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
return { ok: true, report: parsed };
|
|
189
|
+
}
|
|
190
|
+
catch (err) {
|
|
191
|
+
const e = err;
|
|
192
|
+
if (e?.code === 'ENOENT') {
|
|
193
|
+
return {
|
|
194
|
+
ok: false,
|
|
195
|
+
error: `Feedback report not found: ${id}`,
|
|
196
|
+
errorCode: 'NOT_FOUND',
|
|
197
|
+
nextAction: 'verify the report id is correct (use list() to enumerate drafts)',
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
return {
|
|
201
|
+
ok: false,
|
|
202
|
+
error: `Failed to read feedback report at ${filePath}: ${errMsg(e, err)}`,
|
|
203
|
+
errorCode: 'READ_ERROR',
|
|
204
|
+
nextAction: 'verify the file is readable and the workspace directory is intact',
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Delete a draft by id. No-op (still ok:true) if the file does not exist.
|
|
210
|
+
*/
|
|
211
|
+
async delete(id) {
|
|
212
|
+
if (!reportIdValidator(id)) {
|
|
213
|
+
return {
|
|
214
|
+
ok: false,
|
|
215
|
+
error: `Invalid feedback report id: ${JSON.stringify(id)}`,
|
|
216
|
+
nextAction: 'pass a safe id matching /^[A-Za-z0-9._-]+$/ with length 1-256',
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
const filePath = path.join(this.draftsDir, `${id}.json`);
|
|
220
|
+
try {
|
|
221
|
+
await fs.promises.unlink(filePath);
|
|
222
|
+
return { ok: true };
|
|
223
|
+
}
|
|
224
|
+
catch (err) {
|
|
225
|
+
const e = err;
|
|
226
|
+
if (e?.code === 'ENOENT') {
|
|
227
|
+
return { ok: true };
|
|
228
|
+
}
|
|
229
|
+
return {
|
|
230
|
+
ok: false,
|
|
231
|
+
error: `Failed to delete feedback report at ${filePath}: ${errMsg(e, err)}`,
|
|
232
|
+
nextAction: 'verify the file is deletable and the workspace directory is writable',
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
dispose() {
|
|
237
|
+
// Nothing to dispose; this model holds no connections.
|
|
238
|
+
// (This override satisfies the class-methods-use-this ESLint rule for
|
|
239
|
+
// symmetry with other model classes in this package.)
|
|
240
|
+
void this;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
2
|
+
/**
|
|
3
|
+
* Handle requests to `/api/feedback/reports*` for the given workspace.
|
|
4
|
+
* Routes:
|
|
5
|
+
* POST /api/feedback/reports — create draft from input + diagnostics
|
|
6
|
+
* GET /api/feedback/reports — list draft summaries
|
|
7
|
+
* GET /api/feedback/reports/:id — fetch a single draft
|
|
8
|
+
* DELETE /api/feedback/reports/:id — delete a draft
|
|
9
|
+
*/
|
|
10
|
+
export type FeedbackReportsContext = {
|
|
11
|
+
workspaceDir: string;
|
|
12
|
+
subPath: string;
|
|
13
|
+
featureFlags?: Record<string, {
|
|
14
|
+
enabled: boolean;
|
|
15
|
+
}>;
|
|
16
|
+
};
|
|
17
|
+
export declare function handleFeedbackReportsRoute(req: IncomingMessage, res: ServerResponse, ctx: FeedbackReportsContext): Promise<void>;
|
|
18
|
+
export declare function disposeFeedbackReportModels(): void;
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
// feedback-reports.ts
|
|
2
|
+
// Server route for /api/feedback/reports/*
|
|
3
|
+
// Implements the Console-first feedback draft storage described in
|
|
4
|
+
// docs/superpowers/specs/2026-05-31-feedback-channel-design.md.
|
|
5
|
+
//
|
|
6
|
+
// The route is a local-only CRUD over <workspaceDir>/.pd/feedback/drafts/.
|
|
7
|
+
// It MUST NOT reach the network and MUST NOT auto-submit anything.
|
|
8
|
+
import { createFeedbackReport, } from '@principles/core/runtime-v2/feedback';
|
|
9
|
+
import { FeedbackReportConsoleModel, } from '../models/FeedbackReportConsoleModel.js';
|
|
10
|
+
import { sendSuccess, sendError, sendNotFound, sendMethodNotAllowed, sendBadRequest, } from '../utils/response.js';
|
|
11
|
+
const models = new Map();
|
|
12
|
+
function getModel(workspaceDir) {
|
|
13
|
+
let model = models.get(workspaceDir);
|
|
14
|
+
if (!model) {
|
|
15
|
+
model = new FeedbackReportConsoleModel(workspaceDir);
|
|
16
|
+
models.set(workspaceDir, model);
|
|
17
|
+
}
|
|
18
|
+
return model;
|
|
19
|
+
}
|
|
20
|
+
async function readJsonBody(req) {
|
|
21
|
+
return new Promise((resolve) => {
|
|
22
|
+
const chunks = [];
|
|
23
|
+
let total = 0;
|
|
24
|
+
const MAX_BODY = 256 * 1024; // 256 KiB — drafts are bounded by core contract
|
|
25
|
+
req.on('data', (chunk) => {
|
|
26
|
+
total += chunk.length;
|
|
27
|
+
if (total > MAX_BODY) {
|
|
28
|
+
resolve({ ok: false, error: 'request body exceeds 256 KiB limit' });
|
|
29
|
+
req.destroy();
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
chunks.push(chunk);
|
|
33
|
+
});
|
|
34
|
+
req.on('end', () => {
|
|
35
|
+
if (chunks.length === 0) {
|
|
36
|
+
resolve({ ok: true, value: {} });
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
const raw = Buffer.concat(chunks).toString('utf8');
|
|
40
|
+
try {
|
|
41
|
+
resolve({ ok: true, value: JSON.parse(raw) });
|
|
42
|
+
}
|
|
43
|
+
catch (err) {
|
|
44
|
+
resolve({
|
|
45
|
+
ok: false,
|
|
46
|
+
error: `invalid JSON body: ${err instanceof Error ? err.message : String(err)}`,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
req.on('error', (err) => {
|
|
51
|
+
resolve({ ok: false, error: err instanceof Error ? err.message : String(err) });
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
function errorMessage(err) {
|
|
56
|
+
return err instanceof Error ? err.message : String(err);
|
|
57
|
+
}
|
|
58
|
+
export async function handleFeedbackReportsRoute(req, res, ctx) {
|
|
59
|
+
const { workspaceDir, subPath } = ctx;
|
|
60
|
+
const model = getModel(workspaceDir);
|
|
61
|
+
const method = req.method ?? 'GET';
|
|
62
|
+
// List / create
|
|
63
|
+
if (subPath === '' || subPath === '/') {
|
|
64
|
+
if (method === 'GET') {
|
|
65
|
+
try {
|
|
66
|
+
const result = await model.list();
|
|
67
|
+
if (!result.ok) {
|
|
68
|
+
sendError(res, 500, 'feedback_reports_list_failed', result.error);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
sendSuccess(res, { drafts: result.drafts });
|
|
72
|
+
}
|
|
73
|
+
catch (err) {
|
|
74
|
+
sendError(res, 500, 'feedback_reports_list_error', errorMessage(err));
|
|
75
|
+
}
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
if (method === 'POST') {
|
|
79
|
+
// Feature flag gate — feedback_channel must be enabled to create drafts
|
|
80
|
+
if (ctx.featureFlags
|
|
81
|
+
&& Object.hasOwn(ctx.featureFlags, 'feedback_channel')
|
|
82
|
+
&& !ctx.featureFlags.feedback_channel.enabled) {
|
|
83
|
+
sendError(res, 403, 'feedback_channel_disabled', 'feedback_channel feature flag is disabled. Enable feedback_channel in .pd/feature-flags.yaml to use feedback reports.');
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
const bodyResult = await readJsonBody(req);
|
|
87
|
+
if (!bodyResult.ok) {
|
|
88
|
+
sendBadRequest(res, bodyResult.error);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
if (bodyResult.value === null || typeof bodyResult.value !== 'object') {
|
|
92
|
+
sendBadRequest(res, 'request body must be a JSON object with {input, diagnostics}');
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
const obj = bodyResult.value;
|
|
96
|
+
const { input } = obj;
|
|
97
|
+
const diagnostics = obj.diagnostics ?? {};
|
|
98
|
+
const reportResult = createFeedbackReport(input, diagnostics);
|
|
99
|
+
if (!reportResult.ok) {
|
|
100
|
+
sendBadRequest(res, reportResult.errors.map((e) => `${e.field}: ${e.reason}`).join('; '));
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
const draftResult = await model.create(reportResult.report);
|
|
104
|
+
if (!draftResult.ok || !draftResult.report) {
|
|
105
|
+
sendError(res, 500, 'feedback_reports_create_failed', draftResult.error ?? 'unknown error');
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
const r = draftResult.report;
|
|
109
|
+
sendSuccess(res, {
|
|
110
|
+
id: r.id,
|
|
111
|
+
createdAt: r.createdAt,
|
|
112
|
+
report: r,
|
|
113
|
+
});
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
sendMethodNotAllowed(res);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
// Single draft: /:id
|
|
120
|
+
const id = subPath.replace(/^\//, '');
|
|
121
|
+
if (id.length === 0) {
|
|
122
|
+
sendNotFound(res, `Route /api/feedback/reports${subPath} not found`);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
if (method === 'GET') {
|
|
126
|
+
try {
|
|
127
|
+
const result = await model.get(id);
|
|
128
|
+
if (!result.ok) {
|
|
129
|
+
if (result.errorCode === 'NOT_FOUND') {
|
|
130
|
+
sendNotFound(res, result.error);
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
sendError(res, 400, 'feedback_reports_get_failed', result.error);
|
|
134
|
+
}
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
sendSuccess(res, { report: result.report });
|
|
138
|
+
}
|
|
139
|
+
catch (err) {
|
|
140
|
+
sendError(res, 500, 'feedback_reports_get_error', errorMessage(err));
|
|
141
|
+
}
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
if (method === 'DELETE') {
|
|
145
|
+
try {
|
|
146
|
+
const result = await model.delete(id);
|
|
147
|
+
if (!result.ok) {
|
|
148
|
+
sendError(res, 500, 'feedback_reports_delete_failed', result.error);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
sendSuccess(res, { deleted: true });
|
|
152
|
+
}
|
|
153
|
+
catch (err) {
|
|
154
|
+
sendError(res, 500, 'feedback_reports_delete_error', errorMessage(err));
|
|
155
|
+
}
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
sendMethodNotAllowed(res);
|
|
159
|
+
}
|
|
160
|
+
export function disposeFeedbackReportModels() {
|
|
161
|
+
for (const model of models.values()) {
|
|
162
|
+
model.dispose();
|
|
163
|
+
}
|
|
164
|
+
models.clear();
|
|
165
|
+
}
|
package/console/dist/ui/App.js
CHANGED
|
@@ -26,6 +26,7 @@ import { PrincipleDetailPage } from "./pages/PrincipleDetailPage.js";
|
|
|
26
26
|
import { AgentsPage } from "./pages/AgentsPage.js";
|
|
27
27
|
import { LoginPage } from "./pages/LoginPage.js";
|
|
28
28
|
import { UpdatePage } from "./pages/UpdatePage.js";
|
|
29
|
+
import { ReportProblemPage } from "./pages/ReportProblemPage.js";
|
|
29
30
|
import { ErrorBoundary } from "./components/error-boundary.js";
|
|
30
31
|
import { getToken, clearToken, checkAuth } from "./api.js";
|
|
31
32
|
export function App() {
|
|
@@ -49,5 +50,5 @@ export function App() {
|
|
|
49
50
|
clearToken();
|
|
50
51
|
setAuthed(false);
|
|
51
52
|
};
|
|
52
|
-
return (_jsx(ThemeProvider, { children: _jsx(I18nextProvider, { i18n: i18n, children: _jsx(HashRouter, { children: _jsxs("div", { className: "min-h-screen bg-background text-foreground", children: [_jsx("a", { href: "#main-content", className: "sr-only focus:not-sr-only focus:absolute focus:z-50 focus:p-4 focus:bg-background focus:text-foreground", children: "Skip to main content" }), _jsx(AppSidebar, { collapsed: sidebarCollapsed, onCollapsedChange: setSidebarCollapsed }), _jsxs("div", { className: `transition-[margin] duration-300 motion-reduce:transition-none flex flex-col min-h-screen ${sidebarCollapsed ? "ml-16" : "ml-56"}`, children: [_jsx("header", { className: "sticky top-0 z-40 border-b border-border bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60", children: _jsxs("div", { className: "flex items-center justify-between px-6 py-3", children: [_jsx("div", { className: "flex items-center gap-4", children: _jsx("span", { className: "text-sm font-medium text-muted-foreground", children: "PD Console v0.2" }) }), _jsxs("div", { className: "flex items-center gap-3", children: [_jsx(LanguageSwitcher, {}), _jsx(ThemeToggle, {}), getToken() && (_jsx(Button, { variant: "ghost", size: "sm", onClick: handleSignOut, children: t("common:logout") }))] })] }) }), _jsx("main", { id: "main-content", className: "flex-1 p-6 overflow-y-auto", children: _jsx(ErrorBoundary, { children: _jsxs(Routes, { children: [_jsx(Route, { path: "/", element: _jsx(PainPage, {}) }), _jsx(Route, { path: "/pain", element: _jsx(PainPage, {}) }), _jsx(Route, { path: "/approvals", element: _jsx(ApprovalsPage, {}) }), _jsx(Route, { path: "/overview", element: _jsx(OverviewPage, {}) }), _jsx(Route, { path: "/central", element: _jsx(CentralPage, {}) }), _jsx(Route, { path: "/tasks", element: _jsx(TasksPage, {}) }), _jsx(Route, { path: "/feedback", element: _jsx(FeedbackPage, {}) }), _jsx(Route, { path: "/gates", element: _jsx(GatesPage, {}) }), _jsx(Route, { path: "/samples", element: _jsx(SamplesPage, {}) }), _jsx(Route, { path: "/evolution", element: _jsx(EvolutionPage, {}) }), _jsx(Route, { path: "/principles", element: _jsx(PrinciplesPage, {}) }), _jsx(Route, { path: "/principles/:id", element: _jsx(PrincipleDetailPage, {}) }), _jsx(Route, { path: "/agents", element: _jsx(AgentsPage, {}) }), _jsx(Route, { path: "/data-flow", element: _jsx(DataFlowPage, {}) }), _jsx(Route, { path: "/event-log", element: _jsx(EventLogPage, {}) }), _jsx(Route, { path: "/thinking-models", element: _jsx(ThinkingModelsPage, {}) }), _jsx(Route, { path: "/settings", element: _jsx(SettingsPage, {}) }), _jsx(Route, { path: "/settings/update", element: _jsx(UpdatePage, {}) })] }) }) })] })] }) }) }) }));
|
|
53
|
+
return (_jsx(ThemeProvider, { children: _jsx(I18nextProvider, { i18n: i18n, children: _jsx(HashRouter, { children: _jsxs("div", { className: "min-h-screen bg-background text-foreground", children: [_jsx("a", { href: "#main-content", className: "sr-only focus:not-sr-only focus:absolute focus:z-50 focus:p-4 focus:bg-background focus:text-foreground", children: "Skip to main content" }), _jsx(AppSidebar, { collapsed: sidebarCollapsed, onCollapsedChange: setSidebarCollapsed }), _jsxs("div", { className: `transition-[margin] duration-300 motion-reduce:transition-none flex flex-col min-h-screen ${sidebarCollapsed ? "ml-16" : "ml-56"}`, children: [_jsx("header", { className: "sticky top-0 z-40 border-b border-border bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60", children: _jsxs("div", { className: "flex items-center justify-between px-6 py-3", children: [_jsx("div", { className: "flex items-center gap-4", children: _jsx("span", { className: "text-sm font-medium text-muted-foreground", children: "PD Console v0.2" }) }), _jsxs("div", { className: "flex items-center gap-3", children: [_jsx(LanguageSwitcher, {}), _jsx(ThemeToggle, {}), getToken() && (_jsx(Button, { variant: "ghost", size: "sm", onClick: handleSignOut, children: t("common:logout") }))] })] }) }), _jsx("main", { id: "main-content", className: "flex-1 p-6 overflow-y-auto", children: _jsx(ErrorBoundary, { children: _jsxs(Routes, { children: [_jsx(Route, { path: "/", element: _jsx(PainPage, {}) }), _jsx(Route, { path: "/pain", element: _jsx(PainPage, {}) }), _jsx(Route, { path: "/approvals", element: _jsx(ApprovalsPage, {}) }), _jsx(Route, { path: "/overview", element: _jsx(OverviewPage, {}) }), _jsx(Route, { path: "/central", element: _jsx(CentralPage, {}) }), _jsx(Route, { path: "/tasks", element: _jsx(TasksPage, {}) }), _jsx(Route, { path: "/feedback", element: _jsx(FeedbackPage, {}) }), _jsx(Route, { path: "/gates", element: _jsx(GatesPage, {}) }), _jsx(Route, { path: "/samples", element: _jsx(SamplesPage, {}) }), _jsx(Route, { path: "/evolution", element: _jsx(EvolutionPage, {}) }), _jsx(Route, { path: "/principles", element: _jsx(PrinciplesPage, {}) }), _jsx(Route, { path: "/principles/:id", element: _jsx(PrincipleDetailPage, {}) }), _jsx(Route, { path: "/agents", element: _jsx(AgentsPage, {}) }), _jsx(Route, { path: "/data-flow", element: _jsx(DataFlowPage, {}) }), _jsx(Route, { path: "/event-log", element: _jsx(EventLogPage, {}) }), _jsx(Route, { path: "/thinking-models", element: _jsx(ThinkingModelsPage, {}) }), _jsx(Route, { path: "/settings", element: _jsx(SettingsPage, {}) }), _jsx(Route, { path: "/settings/update", element: _jsx(UpdatePage, {}) }), _jsx(Route, { path: "/report-problem", element: _jsx(ReportProblemPage, {}) })] }) }) })] })] }) }) }) }));
|
|
53
54
|
}
|