@urielsh/prodify 0.1.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/.prodify/AGENTS.md +56 -0
- package/.prodify/README.md +10 -0
- package/.prodify/artifacts/01-understand.md +28 -0
- package/.prodify/artifacts/02-diagnose.md +26 -0
- package/.prodify/artifacts/03-architecture.md +30 -0
- package/.prodify/artifacts/04-plan.md +35 -0
- package/.prodify/artifacts/05-refactor.md +24 -0
- package/.prodify/artifacts/06-validate.md +17 -0
- package/.prodify/artifacts/README.md +6 -0
- package/.prodify/artifacts/architecture_spec.md +29 -0
- package/.prodify/artifacts/artifact-validation-design.md +276 -0
- package/.prodify/artifacts/cli-command-design.md +299 -0
- package/.prodify/artifacts/completed-steps-tracking.md +201 -0
- package/.prodify/artifacts/diagnostic_report.md +45 -0
- package/.prodify/artifacts/hardening-patch-summary.md +57 -0
- package/.prodify/artifacts/hardening-verification-report.md +48 -0
- package/.prodify/artifacts/implementation_summary.md +19 -0
- package/.prodify/artifacts/improvement-evaluation-spec.md +148 -0
- package/.prodify/artifacts/next-step-resolver-design.md +570 -0
- package/.prodify/artifacts/orientation_map.md +32 -0
- package/.prodify/artifacts/persona-removal-audit.md +106 -0
- package/.prodify/artifacts/planning-alignment-report.md +189 -0
- package/.prodify/artifacts/refactor-plan-template-hardening.md +83 -0
- package/.prodify/artifacts/refactor-validate-loop-design.md +231 -0
- package/.prodify/artifacts/refactor_plan.md +21 -0
- package/.prodify/artifacts/refactor_plan.strict-example.md +31 -0
- package/.prodify/artifacts/run-state-design.md +292 -0
- package/.prodify/artifacts/run-summary-spec.md +149 -0
- package/.prodify/artifacts/run_state.json +14 -0
- package/.prodify/artifacts/sample-repo-test-plan.md +129 -0
- package/.prodify/artifacts/status-output-spec.md +349 -0
- package/.prodify/artifacts/step-selection-design.md +251 -0
- package/.prodify/artifacts/task-dispatcher-design.md +266 -0
- package/.prodify/artifacts/task-header-audit.md +42 -0
- package/.prodify/artifacts/task-log-enhancement-spec.md +264 -0
- package/.prodify/artifacts/task-protocol-hardening-plan.md +68 -0
- package/.prodify/artifacts/task-self-validation-spec.md +171 -0
- package/.prodify/artifacts/task_log.json +160 -0
- package/.prodify/artifacts/template-usage-audit.md +20 -0
- package/.prodify/artifacts/validation_report.md +22 -0
- package/.prodify/contracts/README.md +3 -0
- package/.prodify/contracts/architecture.contract.json +42 -0
- package/.prodify/contracts/diagnose.contract.json +42 -0
- package/.prodify/contracts/plan.contract.json +42 -0
- package/.prodify/contracts/refactor.contract.json +45 -0
- package/.prodify/contracts/understand.contract.json +42 -0
- package/.prodify/contracts/validate.contract.json +52 -0
- package/.prodify/contracts-src/README.md +4 -0
- package/.prodify/contracts-src/architecture.contract.md +29 -0
- package/.prodify/contracts-src/diagnose.contract.md +29 -0
- package/.prodify/contracts-src/plan.contract.md +29 -0
- package/.prodify/contracts-src/refactor.contract.md +32 -0
- package/.prodify/contracts-src/understand.contract.md +29 -0
- package/.prodify/contracts-src/validate.contract.md +35 -0
- package/.prodify/metrics/README.md +4 -0
- package/.prodify/planning.md +13 -0
- package/.prodify/project.md +15 -0
- package/.prodify/rules/01-global-operating-rules.md +21 -0
- package/.prodify/rules/02-analysis-discipline.md +17 -0
- package/.prodify/rules/03-diagnosis-severity-rules.md +42 -0
- package/.prodify/rules/04-architecture-rules.md +27 -0
- package/.prodify/rules/05-planning-rules.md +23 -0
- package/.prodify/rules/06-refactor-rules.md +20 -0
- package/.prodify/rules/07-validation-rules.md +25 -0
- package/.prodify/rules/08-output-format-rules.md +20 -0
- package/.prodify/rules/09-template-usage-rules.md +11 -0
- package/.prodify/rules/10-artifact-flow-and-orchestration-rules.md +40 -0
- package/.prodify/rules/README.md +3 -0
- package/.prodify/rules/example-rule.md +4 -0
- package/.prodify/runtime-commands.md +57 -0
- package/.prodify/skills/README.md +8 -0
- package/.prodify/skills/domain/react-frontend.skill.json +34 -0
- package/.prodify/skills/domain/typescript-backend.skill.json +34 -0
- package/.prodify/skills/quality-policy/maintainability-review.skill.json +25 -0
- package/.prodify/skills/quality-policy/security-hardening.skill.json +32 -0
- package/.prodify/skills/quality-policy/test-hardening.skill.json +23 -0
- package/.prodify/skills/registry.json +16 -0
- package/.prodify/skills/stage-method/architecture-method.skill.json +22 -0
- package/.prodify/skills/stage-method/codebase-scanning.skill.json +22 -0
- package/.prodify/skills/stage-method/diagnosis-method.skill.json +22 -0
- package/.prodify/skills/stage-method/planning-method.skill.json +22 -0
- package/.prodify/skills/stage-method/refactoring-method.skill.json +22 -0
- package/.prodify/skills/stage-method/validation-method.skill.json +22 -0
- package/.prodify/state.json +30 -0
- package/.prodify/tasks/01-understand.md +83 -0
- package/.prodify/tasks/02-diagnose.md +67 -0
- package/.prodify/tasks/03-architecture.md +70 -0
- package/.prodify/tasks/04-plan.md +71 -0
- package/.prodify/tasks/05-refactor.md +61 -0
- package/.prodify/tasks/06-validate.md +69 -0
- package/.prodify/tasks/README.md +3 -0
- package/.prodify/tasks/example-task.md +13 -0
- package/.prodify/templates/01-understand.template.md +18 -0
- package/.prodify/templates/02-diagnose.template.md +18 -0
- package/.prodify/templates/03-architecture.template.md +18 -0
- package/.prodify/templates/04-plan.template.md +22 -0
- package/.prodify/templates/05-refactor.template.md +19 -0
- package/.prodify/templates/06-validate.template.md +16 -0
- package/.prodify/templates/README.md +3 -0
- package/.prodify/templates/architecture_spec.template.md +29 -0
- package/.prodify/templates/diagnostic_report.template.md +45 -0
- package/.prodify/templates/example.template.md +5 -0
- package/.prodify/templates/implementation_summary.template.md +19 -0
- package/.prodify/templates/orientation_map.template.md +32 -0
- package/.prodify/templates/refactor_plan.template.md +24 -0
- package/.prodify/templates/validation_report.template.md +22 -0
- package/.prodify/version.json +5 -0
- package/AGENTS.md +305 -0
- package/LICENSE +201 -0
- package/README.md +118 -0
- package/assets/presets/default/canonical/AGENTS.md +54 -0
- package/assets/presets/default/canonical/artifacts/README.md +6 -0
- package/assets/presets/default/canonical/contracts-src/README.md +4 -0
- package/assets/presets/default/canonical/contracts-src/architecture.contract.md +56 -0
- package/assets/presets/default/canonical/contracts-src/diagnose.contract.md +49 -0
- package/assets/presets/default/canonical/contracts-src/plan.contract.md +42 -0
- package/assets/presets/default/canonical/contracts-src/refactor.contract.md +54 -0
- package/assets/presets/default/canonical/contracts-src/understand.contract.md +56 -0
- package/assets/presets/default/canonical/contracts-src/validate.contract.md +64 -0
- package/assets/presets/default/canonical/metrics/README.md +4 -0
- package/assets/presets/default/canonical/planning.md +13 -0
- package/assets/presets/default/canonical/project.md +15 -0
- package/assets/presets/default/canonical/rules/README.md +3 -0
- package/assets/presets/default/canonical/rules/example-rule.md +4 -0
- package/assets/presets/default/canonical/runtime-commands.md +57 -0
- package/assets/presets/default/canonical/skills/README.md +8 -0
- package/assets/presets/default/canonical/skills/domain/react-frontend.skill.json +34 -0
- package/assets/presets/default/canonical/skills/domain/typescript-backend.skill.json +34 -0
- package/assets/presets/default/canonical/skills/quality-policy/maintainability-review.skill.json +25 -0
- package/assets/presets/default/canonical/skills/quality-policy/security-hardening.skill.json +32 -0
- package/assets/presets/default/canonical/skills/quality-policy/test-hardening.skill.json +23 -0
- package/assets/presets/default/canonical/skills/registry.json +16 -0
- package/assets/presets/default/canonical/skills/stage-method/architecture-method.skill.json +22 -0
- package/assets/presets/default/canonical/skills/stage-method/codebase-scanning.skill.json +22 -0
- package/assets/presets/default/canonical/skills/stage-method/diagnosis-method.skill.json +22 -0
- package/assets/presets/default/canonical/skills/stage-method/planning-method.skill.json +22 -0
- package/assets/presets/default/canonical/skills/stage-method/refactoring-method.skill.json +22 -0
- package/assets/presets/default/canonical/skills/stage-method/validation-method.skill.json +22 -0
- package/assets/presets/default/canonical/state.json +30 -0
- package/assets/presets/default/canonical/tasks/README.md +3 -0
- package/assets/presets/default/canonical/tasks/example-task.md +13 -0
- package/assets/presets/default/canonical/templates/README.md +3 -0
- package/assets/presets/default/canonical/templates/example.template.md +5 -0
- package/assets/presets/default/preset.json +5 -0
- package/dist/cli.js +53 -0
- package/dist/commands/doctor.js +16 -0
- package/dist/commands/init.js +30 -0
- package/dist/commands/install.js +27 -0
- package/dist/commands/setup-agent.js +23 -0
- package/dist/commands/status.js +28 -0
- package/dist/commands/sync.js +29 -0
- package/dist/commands/update.js +18 -0
- package/dist/contracts/compiled-schema.js +37 -0
- package/dist/contracts/compiler.js +83 -0
- package/dist/contracts/freshness.js +52 -0
- package/dist/contracts/index.js +5 -0
- package/dist/contracts/parser.js +201 -0
- package/dist/contracts/schema.js +138 -0
- package/dist/contracts/source-schema.js +111 -0
- package/dist/core/agent-runtime.js +36 -0
- package/dist/core/agent-setup.js +111 -0
- package/dist/core/doctor.js +155 -0
- package/dist/core/errors.js +19 -0
- package/dist/core/flow-state.js +262 -0
- package/dist/core/fs.js +50 -0
- package/dist/core/install.js +44 -0
- package/dist/core/managed-files.js +106 -0
- package/dist/core/paths.js +56 -0
- package/dist/core/preset-validation.js +43 -0
- package/dist/core/prompt-builder.js +63 -0
- package/dist/core/repo-context.js +77 -0
- package/dist/core/repo-root.js +55 -0
- package/dist/core/skill-resolution.js +123 -0
- package/dist/core/state.js +220 -0
- package/dist/core/status.js +293 -0
- package/dist/core/sync.js +63 -0
- package/dist/core/targets.js +48 -0
- package/dist/core/upgrade.js +57 -0
- package/dist/core/validation.js +138 -0
- package/dist/core/version-checks.js +35 -0
- package/dist/generators/claude.js +14 -0
- package/dist/generators/codex.js +14 -0
- package/dist/generators/copilot.js +29 -0
- package/dist/generators/header.js +13 -0
- package/dist/generators/opencode.js +14 -0
- package/dist/generators/shared.js +37 -0
- package/dist/index.js +9 -0
- package/dist/legacy/targets.js +55 -0
- package/dist/presets/default.js +3 -0
- package/dist/presets/loader.js +34 -0
- package/dist/presets/version.js +18 -0
- package/dist/scoring/model.js +262 -0
- package/dist/skills/loader.js +40 -0
- package/dist/skills/schema.js +159 -0
- package/dist/types.js +1 -0
- package/docs/canonical-prodify-layout.md +87 -0
- package/docs/claude-support.md +22 -0
- package/docs/cli-doctor-spec.md +52 -0
- package/docs/cli-init-spec.md +70 -0
- package/docs/cli-install-agent-spec.md +48 -0
- package/docs/cli-sync-spec.md +40 -0
- package/docs/codex-support.md +24 -0
- package/docs/compatibility-targets.md +59 -0
- package/docs/copilot-support.md +30 -0
- package/docs/default-preset.md +47 -0
- package/docs/generated-file-headers.md +45 -0
- package/docs/generation-rules.md +94 -0
- package/docs/idempotency-guarantees.md +31 -0
- package/docs/managed-file-detection.md +45 -0
- package/docs/manual-edit-conflicts.md +37 -0
- package/docs/opencode-support.md +27 -0
- package/docs/ownership-rules.md +39 -0
- package/docs/path-resolution-rules.md +40 -0
- package/docs/preset-structure.md +49 -0
- package/docs/skill-system.md +67 -0
- package/docs/versioning-and-upgrade-strategy.md +40 -0
- package/package.json +22 -0
- package/src/README.md +11 -0
- package/src/cli.ts +61 -0
- package/src/commands/doctor.ts +20 -0
- package/src/commands/init.ts +37 -0
- package/src/commands/setup-agent.ts +28 -0
- package/src/commands/status.ts +34 -0
- package/src/commands/update.ts +23 -0
- package/src/contracts/README.md +10 -0
- package/src/contracts/compiled-schema.ts +42 -0
- package/src/contracts/compiler.ts +111 -0
- package/src/contracts/freshness.ts +58 -0
- package/src/contracts/index.ts +11 -0
- package/src/contracts/parser.ts +253 -0
- package/src/contracts/source-schema.ts +141 -0
- package/src/core/agent-runtime.ts +53 -0
- package/src/core/agent-setup.ts +147 -0
- package/src/core/doctor.ts +171 -0
- package/src/core/errors.ts +28 -0
- package/src/core/flow-state.ts +333 -0
- package/src/core/fs.ts +59 -0
- package/src/core/paths.ts +63 -0
- package/src/core/preset-validation.ts +47 -0
- package/src/core/prompt-builder.ts +73 -0
- package/src/core/repo-context.ts +93 -0
- package/src/core/repo-root.ts +74 -0
- package/src/core/skill-resolution.ts +151 -0
- package/src/core/state.ts +264 -0
- package/src/core/status.ts +372 -0
- package/src/core/targets.ts +53 -0
- package/src/core/upgrade.ts +66 -0
- package/src/core/validation.ts +233 -0
- package/src/core/version-checks.ts +40 -0
- package/src/index.ts +13 -0
- package/src/presets/default.ts +7 -0
- package/src/presets/loader.ts +46 -0
- package/src/presets/version.ts +31 -0
- package/src/scoring/model.ts +332 -0
- package/src/skills/loader.ts +58 -0
- package/src/skills/schema.ts +197 -0
- package/src/types.ts +329 -0
- package/tests/integration/cli-flows.test.js +347 -0
- package/tests/unit/agent-setup.test.js +81 -0
- package/tests/unit/cli.test.js +28 -0
- package/tests/unit/contracts.test.js +61 -0
- package/tests/unit/helpers.js +28 -0
- package/tests/unit/paths.test.js +52 -0
- package/tests/unit/preset-loader.test.js +37 -0
- package/tests/unit/scoring.test.js +65 -0
- package/tests/unit/self-hosted-workspace.test.js +22 -0
- package/tests/unit/skills.test.js +115 -0
- package/tests/unit/state-and-flow.test.js +120 -0
- package/tests/unit/targets.test.js +13 -0
- package/tests/unit/validation.test.js +73 -0
- package/tests/unit/version.test.js +24 -0
- package/tsconfig.json +23 -0
- package/urielsh-prodify-0.1.0.tgz +0 -0
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
import { inspectCompiledContracts } from '../contracts/freshness.js';
|
|
5
|
+
import { ProdifyError } from '../core/errors.js';
|
|
6
|
+
import { listFilesRecursive, pathExists, writeFileEnsuringDir } from '../core/fs.js';
|
|
7
|
+
import { resolveRepoPath } from '../core/paths.js';
|
|
8
|
+
import type { ProdifyState, ScoreDelta, ScoreMetric, ScoreSnapshot, ScoreSnapshotKind } from '../types.js';
|
|
9
|
+
|
|
10
|
+
const SCORE_SCHEMA_VERSION = '1';
|
|
11
|
+
|
|
12
|
+
interface ToolOutput {
|
|
13
|
+
adapter: string;
|
|
14
|
+
details: Record<string, unknown>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function roundScore(value: number): number {
|
|
18
|
+
return Math.round(value * 100) / 100;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function createMetric(options: {
|
|
22
|
+
id: string;
|
|
23
|
+
label: string;
|
|
24
|
+
tool: string;
|
|
25
|
+
weight: number;
|
|
26
|
+
ratio: number;
|
|
27
|
+
details: string;
|
|
28
|
+
}): ScoreMetric {
|
|
29
|
+
const ratio = Math.max(0, Math.min(1, options.ratio));
|
|
30
|
+
const points = roundScore(options.weight * ratio);
|
|
31
|
+
return {
|
|
32
|
+
id: options.id,
|
|
33
|
+
label: options.label,
|
|
34
|
+
tool: options.tool,
|
|
35
|
+
weight: options.weight,
|
|
36
|
+
max_points: options.weight,
|
|
37
|
+
points,
|
|
38
|
+
status: ratio === 1 ? 'pass' : ratio === 0 ? 'fail' : 'partial',
|
|
39
|
+
details: options.details
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function detectEcosystems(repoRoot: string): Promise<string[]> {
|
|
44
|
+
const ecosystems = [];
|
|
45
|
+
if (await pathExists(resolveRepoPath(repoRoot, 'package.json'))) {
|
|
46
|
+
ecosystems.push('typescript-javascript');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const repoFiles = await listFilesRecursive(repoRoot);
|
|
50
|
+
if (repoFiles.some((file) => file.relativePath.endsWith('.py'))) {
|
|
51
|
+
ecosystems.push('python');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (repoFiles.some((file) => file.relativePath.endsWith('.cs') || file.relativePath.endsWith('.csproj') || file.relativePath.endsWith('.sln'))) {
|
|
55
|
+
ecosystems.push('csharp');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return ecosystems.sort((left, right) => left.localeCompare(right));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function buildEcosystemMetrics(repoRoot: string, ecosystems: string[]): Promise<{ metrics: ScoreMetric[]; toolOutputs: ToolOutput[] }> {
|
|
62
|
+
const metrics: ScoreMetric[] = [];
|
|
63
|
+
const toolOutputs: ToolOutput[] = [];
|
|
64
|
+
|
|
65
|
+
if (ecosystems.includes('typescript-javascript')) {
|
|
66
|
+
const packageJsonPath = resolveRepoPath(repoRoot, 'package.json');
|
|
67
|
+
const tsconfigPath = resolveRepoPath(repoRoot, 'tsconfig.json');
|
|
68
|
+
const packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf8')) as {
|
|
69
|
+
scripts?: Record<string, string>;
|
|
70
|
+
};
|
|
71
|
+
const scripts = packageJson.scripts ?? {};
|
|
72
|
+
const hasBuild = typeof scripts.build === 'string' && scripts.build.length > 0;
|
|
73
|
+
const hasTest = typeof scripts.test === 'string' && scripts.test.length > 0;
|
|
74
|
+
const hasTsconfig = await pathExists(tsconfigPath);
|
|
75
|
+
const score = [hasBuild, hasTest, hasTsconfig].filter(Boolean).length / 3;
|
|
76
|
+
|
|
77
|
+
metrics.push(createMetric({
|
|
78
|
+
id: 'typescript-javascript',
|
|
79
|
+
label: 'TypeScript/JavaScript local tooling',
|
|
80
|
+
tool: 'package-json',
|
|
81
|
+
weight: 15,
|
|
82
|
+
ratio: score,
|
|
83
|
+
details: `build=${hasBuild}, test=${hasTest}, tsconfig=${hasTsconfig}`
|
|
84
|
+
}));
|
|
85
|
+
toolOutputs.push({
|
|
86
|
+
adapter: 'typescript-javascript',
|
|
87
|
+
details: {
|
|
88
|
+
build_script: hasBuild,
|
|
89
|
+
test_script: hasTest,
|
|
90
|
+
tsconfig: hasTsconfig
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (ecosystems.includes('python')) {
|
|
96
|
+
const hasPyproject = await pathExists(resolveRepoPath(repoRoot, 'pyproject.toml'));
|
|
97
|
+
const hasRequirements = await pathExists(resolveRepoPath(repoRoot, 'requirements.txt'));
|
|
98
|
+
const ratio = [hasPyproject, hasRequirements].filter(Boolean).length / 2;
|
|
99
|
+
|
|
100
|
+
metrics.push(createMetric({
|
|
101
|
+
id: 'python',
|
|
102
|
+
label: 'Python local tooling',
|
|
103
|
+
tool: 'filesystem',
|
|
104
|
+
weight: 7.5,
|
|
105
|
+
ratio,
|
|
106
|
+
details: `pyproject=${hasPyproject}, requirements=${hasRequirements}`
|
|
107
|
+
}));
|
|
108
|
+
toolOutputs.push({
|
|
109
|
+
adapter: 'python',
|
|
110
|
+
details: {
|
|
111
|
+
pyproject: hasPyproject,
|
|
112
|
+
requirements: hasRequirements
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (ecosystems.includes('csharp')) {
|
|
118
|
+
const repoFiles = await listFilesRecursive(repoRoot);
|
|
119
|
+
const hasSolution = repoFiles.some((file) => file.relativePath.endsWith('.sln'));
|
|
120
|
+
const hasProject = repoFiles.some((file) => file.relativePath.endsWith('.csproj'));
|
|
121
|
+
const ratio = [hasSolution, hasProject].filter(Boolean).length / 2;
|
|
122
|
+
|
|
123
|
+
metrics.push(createMetric({
|
|
124
|
+
id: 'csharp',
|
|
125
|
+
label: 'C# local tooling',
|
|
126
|
+
tool: 'filesystem',
|
|
127
|
+
weight: 7.5,
|
|
128
|
+
ratio,
|
|
129
|
+
details: `solution=${hasSolution}, project=${hasProject}`
|
|
130
|
+
}));
|
|
131
|
+
toolOutputs.push({
|
|
132
|
+
adapter: 'csharp',
|
|
133
|
+
details: {
|
|
134
|
+
solution: hasSolution,
|
|
135
|
+
project: hasProject
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
metrics,
|
|
142
|
+
toolOutputs
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async function buildRepoHygieneMetric(repoRoot: string): Promise<{ metric: ScoreMetric; toolOutput: ToolOutput }> {
|
|
147
|
+
const requiredSignals = ['README.md', 'LICENSE', 'tests', '.prodify/contracts-src'];
|
|
148
|
+
const available = await Promise.all(requiredSignals.map((relativePath) => pathExists(resolveRepoPath(repoRoot, relativePath))));
|
|
149
|
+
const ratio = available.filter(Boolean).length / requiredSignals.length;
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
metric: createMetric({
|
|
153
|
+
id: 'repo-hygiene',
|
|
154
|
+
label: 'Repository hygiene signals',
|
|
155
|
+
tool: 'filesystem',
|
|
156
|
+
weight: 20,
|
|
157
|
+
ratio,
|
|
158
|
+
details: requiredSignals.map((entry, index) => `${entry}=${available[index]}`).join(', ')
|
|
159
|
+
}),
|
|
160
|
+
toolOutput: {
|
|
161
|
+
adapter: 'filesystem',
|
|
162
|
+
details: Object.fromEntries(requiredSignals.map((entry, index) => [entry, available[index]]))
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function buildRuntimeMetric(runtimeState: ProdifyState): { metric: ScoreMetric; toolOutput: ToolOutput } {
|
|
168
|
+
const healthyState = runtimeState.runtime.current_state !== 'failed' && runtimeState.runtime.current_state !== 'blocked';
|
|
169
|
+
const ratio = healthyState ? 1 : 0;
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
metric: createMetric({
|
|
173
|
+
id: 'runtime-state',
|
|
174
|
+
label: 'Contract-driven runtime state',
|
|
175
|
+
tool: 'state-json',
|
|
176
|
+
weight: 25,
|
|
177
|
+
ratio,
|
|
178
|
+
details: `current_state=${runtimeState.runtime.current_state}, status=${runtimeState.runtime.status}`
|
|
179
|
+
}),
|
|
180
|
+
toolOutput: {
|
|
181
|
+
adapter: 'state-json',
|
|
182
|
+
details: {
|
|
183
|
+
current_state: runtimeState.runtime.current_state,
|
|
184
|
+
status: runtimeState.runtime.status,
|
|
185
|
+
last_validation_result: runtimeState.runtime.last_validation_result
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function buildValidationMetric(runtimeState: ProdifyState): { metric: ScoreMetric; toolOutput: ToolOutput } {
|
|
192
|
+
const passed = runtimeState.runtime.last_validation?.passed === true;
|
|
193
|
+
const finalReady = runtimeState.runtime.current_state === 'validate_complete' || runtimeState.runtime.current_state === 'completed';
|
|
194
|
+
const ratio = passed ? (finalReady ? 1 : 0.5) : 0;
|
|
195
|
+
|
|
196
|
+
return {
|
|
197
|
+
metric: createMetric({
|
|
198
|
+
id: 'validation-gate',
|
|
199
|
+
label: 'Validated contract completion',
|
|
200
|
+
tool: 'state-json',
|
|
201
|
+
weight: 10,
|
|
202
|
+
ratio,
|
|
203
|
+
details: `passed=${passed}, final_ready=${finalReady}`
|
|
204
|
+
}),
|
|
205
|
+
toolOutput: {
|
|
206
|
+
adapter: 'validation-gate',
|
|
207
|
+
details: {
|
|
208
|
+
passed,
|
|
209
|
+
final_ready: finalReady
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export async function calculateLocalScore(
|
|
216
|
+
repoRoot: string,
|
|
217
|
+
{
|
|
218
|
+
kind,
|
|
219
|
+
runtimeState
|
|
220
|
+
}: {
|
|
221
|
+
kind: ScoreSnapshotKind;
|
|
222
|
+
runtimeState: ProdifyState;
|
|
223
|
+
}
|
|
224
|
+
): Promise<{ snapshot: ScoreSnapshot; toolOutputs: ToolOutput[] }> {
|
|
225
|
+
const contractInventory = await inspectCompiledContracts(repoRoot);
|
|
226
|
+
const ecosystems = await detectEcosystems(repoRoot);
|
|
227
|
+
const metrics: ScoreMetric[] = [];
|
|
228
|
+
const toolOutputs: ToolOutput[] = [];
|
|
229
|
+
|
|
230
|
+
metrics.push(createMetric({
|
|
231
|
+
id: 'contracts',
|
|
232
|
+
label: 'Compiled contract health',
|
|
233
|
+
tool: 'contract-compiler',
|
|
234
|
+
weight: 30,
|
|
235
|
+
ratio: contractInventory.ok ? 1 : Math.max(0, 1 - ((contractInventory.missingCompiledStages.length + contractInventory.staleStages.length) / 6)),
|
|
236
|
+
details: `source=${contractInventory.sourceCount}, compiled=${contractInventory.compiledCount}, stale=${contractInventory.staleStages.length}, missing=${contractInventory.missingCompiledStages.length}`
|
|
237
|
+
}));
|
|
238
|
+
toolOutputs.push({
|
|
239
|
+
adapter: 'contract-compiler',
|
|
240
|
+
details: {
|
|
241
|
+
ok: contractInventory.ok,
|
|
242
|
+
sourceCount: contractInventory.sourceCount,
|
|
243
|
+
compiledCount: contractInventory.compiledCount,
|
|
244
|
+
staleStages: contractInventory.staleStages,
|
|
245
|
+
missingCompiledStages: contractInventory.missingCompiledStages,
|
|
246
|
+
missingSourceStages: contractInventory.missingSourceStages,
|
|
247
|
+
invalidStages: contractInventory.invalidStages
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
const runtimeMetric = buildRuntimeMetric(runtimeState);
|
|
252
|
+
metrics.push(runtimeMetric.metric);
|
|
253
|
+
toolOutputs.push(runtimeMetric.toolOutput);
|
|
254
|
+
|
|
255
|
+
const validationMetric = buildValidationMetric(runtimeState);
|
|
256
|
+
metrics.push(validationMetric.metric);
|
|
257
|
+
toolOutputs.push(validationMetric.toolOutput);
|
|
258
|
+
|
|
259
|
+
const hygieneMetric = await buildRepoHygieneMetric(repoRoot);
|
|
260
|
+
metrics.push(hygieneMetric.metric);
|
|
261
|
+
toolOutputs.push(hygieneMetric.toolOutput);
|
|
262
|
+
|
|
263
|
+
const ecosystemMetrics = await buildEcosystemMetrics(repoRoot, ecosystems);
|
|
264
|
+
metrics.push(...ecosystemMetrics.metrics);
|
|
265
|
+
toolOutputs.push(...ecosystemMetrics.toolOutputs);
|
|
266
|
+
|
|
267
|
+
const totalScore = roundScore(metrics.reduce((sum, metric) => sum + metric.points, 0));
|
|
268
|
+
const maxScore = roundScore(metrics.reduce((sum, metric) => sum + metric.max_points, 0));
|
|
269
|
+
|
|
270
|
+
if (kind === 'final' && !(runtimeState.runtime.last_validation?.passed && (runtimeState.runtime.current_state === 'validate_complete' || runtimeState.runtime.current_state === 'completed'))) {
|
|
271
|
+
throw new ProdifyError('Final scoring requires a validated runtime state at validate_complete or completed.', {
|
|
272
|
+
code: 'SCORING_STATE_INVALID'
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return {
|
|
277
|
+
snapshot: {
|
|
278
|
+
schema_version: SCORE_SCHEMA_VERSION,
|
|
279
|
+
kind,
|
|
280
|
+
ecosystems,
|
|
281
|
+
total_score: totalScore,
|
|
282
|
+
max_score: maxScore,
|
|
283
|
+
metrics
|
|
284
|
+
},
|
|
285
|
+
toolOutputs
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function serializeJson(value: unknown): string {
|
|
290
|
+
return `${JSON.stringify(value, null, 2)}\n`;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
export async function writeScoreSnapshot(
|
|
294
|
+
repoRoot: string,
|
|
295
|
+
{
|
|
296
|
+
kind,
|
|
297
|
+
runtimeState
|
|
298
|
+
}: {
|
|
299
|
+
kind: ScoreSnapshotKind;
|
|
300
|
+
runtimeState: ProdifyState;
|
|
301
|
+
}
|
|
302
|
+
): Promise<ScoreSnapshot> {
|
|
303
|
+
const { snapshot, toolOutputs } = await calculateLocalScore(repoRoot, {
|
|
304
|
+
kind,
|
|
305
|
+
runtimeState
|
|
306
|
+
});
|
|
307
|
+
const metricsDir = resolveRepoPath(repoRoot, '.prodify/metrics');
|
|
308
|
+
|
|
309
|
+
await writeFileEnsuringDir(path.join(metricsDir, `${kind}.score.json`), serializeJson(snapshot));
|
|
310
|
+
await writeFileEnsuringDir(path.join(metricsDir, `${kind}.tools.json`), serializeJson({
|
|
311
|
+
schema_version: SCORE_SCHEMA_VERSION,
|
|
312
|
+
kind,
|
|
313
|
+
outputs: toolOutputs
|
|
314
|
+
}));
|
|
315
|
+
|
|
316
|
+
return snapshot;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
export async function writeScoreDelta(repoRoot: string): Promise<ScoreDelta> {
|
|
320
|
+
const metricsDir = resolveRepoPath(repoRoot, '.prodify/metrics');
|
|
321
|
+
const baseline = JSON.parse(await fs.readFile(path.join(metricsDir, 'baseline.score.json'), 'utf8')) as ScoreSnapshot;
|
|
322
|
+
const final = JSON.parse(await fs.readFile(path.join(metricsDir, 'final.score.json'), 'utf8')) as ScoreSnapshot;
|
|
323
|
+
const delta: ScoreDelta = {
|
|
324
|
+
schema_version: SCORE_SCHEMA_VERSION,
|
|
325
|
+
baseline_score: baseline.total_score,
|
|
326
|
+
final_score: final.total_score,
|
|
327
|
+
delta: roundScore(final.total_score - baseline.total_score)
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
await writeFileEnsuringDir(path.join(metricsDir, 'delta.json'), serializeJson(delta));
|
|
331
|
+
return delta;
|
|
332
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
|
|
3
|
+
import { pathExists } from '../core/fs.js';
|
|
4
|
+
import { resolveCanonicalPath } from '../core/paths.js';
|
|
5
|
+
import { ProdifyError } from '../core/errors.js';
|
|
6
|
+
import { validateSkillDefinitionShape, validateSkillRegistryManifest } from './schema.js';
|
|
7
|
+
import type { SkillDefinition, SkillRegistryManifest } from '../types.js';
|
|
8
|
+
|
|
9
|
+
const SKILL_REGISTRY_PATH = '.prodify/skills/registry.json';
|
|
10
|
+
|
|
11
|
+
async function readJsonFile(filePath: string, missingCode: string, invalidCode: string): Promise<unknown> {
|
|
12
|
+
if (!(await pathExists(filePath))) {
|
|
13
|
+
throw new ProdifyError(`Required skill file is missing: ${filePath}.`, {
|
|
14
|
+
code: missingCode
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
return JSON.parse(await fs.readFile(filePath, 'utf8'));
|
|
20
|
+
} catch {
|
|
21
|
+
throw new ProdifyError(`Skill file is malformed JSON: ${filePath}.`, {
|
|
22
|
+
code: invalidCode
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function readSkillRegistryManifest(repoRoot: string): Promise<SkillRegistryManifest> {
|
|
28
|
+
const registryPath = resolveCanonicalPath(repoRoot, SKILL_REGISTRY_PATH);
|
|
29
|
+
return validateSkillRegistryManifest(await readJsonFile(
|
|
30
|
+
registryPath,
|
|
31
|
+
'SKILL_REGISTRY_MISSING',
|
|
32
|
+
'SKILL_REGISTRY_INVALID'
|
|
33
|
+
));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function loadSkillRegistry(repoRoot: string): Promise<Map<string, SkillDefinition>> {
|
|
37
|
+
const manifest = await readSkillRegistryManifest(repoRoot);
|
|
38
|
+
const registry = new Map<string, SkillDefinition>();
|
|
39
|
+
|
|
40
|
+
for (const relativePath of manifest.skills) {
|
|
41
|
+
const fullPath = resolveCanonicalPath(repoRoot, `.prodify/skills/${relativePath}`);
|
|
42
|
+
const definition = validateSkillDefinitionShape(await readJsonFile(
|
|
43
|
+
fullPath,
|
|
44
|
+
'SKILL_DEFINITION_MISSING',
|
|
45
|
+
'SKILL_DEFINITION_INVALID'
|
|
46
|
+
));
|
|
47
|
+
|
|
48
|
+
if (registry.has(definition.id)) {
|
|
49
|
+
throw new ProdifyError(`Duplicate skill id detected: ${definition.id}.`, {
|
|
50
|
+
code: 'SKILL_REGISTRY_INVALID'
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
registry.set(definition.id, definition);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return registry;
|
|
58
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { normalizeRepoRelativePath } from '../core/paths.js';
|
|
2
|
+
import { ProdifyError } from '../core/errors.js';
|
|
3
|
+
import { CONTRACT_STAGE_NAMES } from '../contracts/source-schema.js';
|
|
4
|
+
import type {
|
|
5
|
+
FlowStage,
|
|
6
|
+
RepoContextFact,
|
|
7
|
+
SkillCategory,
|
|
8
|
+
SkillCondition,
|
|
9
|
+
SkillConditionPredicate,
|
|
10
|
+
SkillDefinition,
|
|
11
|
+
SkillRegistryManifest,
|
|
12
|
+
StageSkillRouting,
|
|
13
|
+
StageSkillRoutingRule
|
|
14
|
+
} from '../types.js';
|
|
15
|
+
|
|
16
|
+
const SKILL_CATEGORIES: readonly SkillCategory[] = ['stage-method', 'domain', 'quality-policy'];
|
|
17
|
+
const REPO_CONTEXT_FACTS: readonly RepoContextFact[] = ['language', 'framework', 'project_type', 'architecture_pattern', 'risk_signal'];
|
|
18
|
+
|
|
19
|
+
function asString(value: unknown, fieldName: string): string {
|
|
20
|
+
if (typeof value !== 'string' || value.trim() === '') {
|
|
21
|
+
throw new ProdifyError(`Skill field "${fieldName}" must be a non-empty string.`, {
|
|
22
|
+
code: 'SKILL_SCHEMA_INVALID'
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return value.trim();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function asOptionalStringArray(value: unknown, fieldName: string): string[] {
|
|
30
|
+
if (!Array.isArray(value) || value.length === 0) {
|
|
31
|
+
return [];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return asStringArray(value, fieldName);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function asStringArray(value: unknown, fieldName: string): string[] {
|
|
38
|
+
if (!Array.isArray(value) || value.length === 0) {
|
|
39
|
+
throw new ProdifyError(`Skill field "${fieldName}" must be a non-empty list.`, {
|
|
40
|
+
code: 'SKILL_SCHEMA_INVALID'
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const normalized = value.map((entry) => asString(entry, fieldName));
|
|
45
|
+
return [...new Set(normalized)].sort((left, right) => left.localeCompare(right));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function asRecord(value: unknown, fieldName: string): Record<string, unknown> {
|
|
49
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
50
|
+
throw new ProdifyError(`Skill field "${fieldName}" must be a mapping.`, {
|
|
51
|
+
code: 'SKILL_SCHEMA_INVALID'
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return value as Record<string, unknown>;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function asSkillCategory(value: unknown): SkillCategory {
|
|
59
|
+
const category = asString(value, 'category') as SkillCategory;
|
|
60
|
+
if (!SKILL_CATEGORIES.includes(category)) {
|
|
61
|
+
throw new ProdifyError(`Skill category is invalid: ${category}.`, {
|
|
62
|
+
code: 'SKILL_SCHEMA_INVALID'
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return category;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function asStageList(value: unknown, fieldName: string): FlowStage[] {
|
|
70
|
+
const stages = asStringArray(value, fieldName) as FlowStage[];
|
|
71
|
+
for (const stage of stages) {
|
|
72
|
+
if (!CONTRACT_STAGE_NAMES.includes(stage)) {
|
|
73
|
+
throw new ProdifyError(`Skill field "${fieldName}" contains unsupported stage "${stage}".`, {
|
|
74
|
+
code: 'SKILL_SCHEMA_INVALID'
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return stages;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function normalizePredicate(value: unknown, fieldName: string): SkillConditionPredicate {
|
|
83
|
+
const record = asRecord(value, fieldName);
|
|
84
|
+
const fact = asString(record.fact, `${fieldName}.fact`) as RepoContextFact;
|
|
85
|
+
if (!REPO_CONTEXT_FACTS.includes(fact)) {
|
|
86
|
+
throw new ProdifyError(`Skill condition fact is invalid: ${fact}.`, {
|
|
87
|
+
code: 'SKILL_SCHEMA_INVALID'
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
fact,
|
|
93
|
+
includes: asString(record.includes, `${fieldName}.includes`)
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function normalizeSkillCondition(value: unknown, fieldName: string): SkillCondition {
|
|
98
|
+
const record = asRecord(value, fieldName);
|
|
99
|
+
const all = Array.isArray(record.all)
|
|
100
|
+
? record.all.map((entry, index) => normalizePredicate(entry, `${fieldName}.all[${index}]`))
|
|
101
|
+
: [];
|
|
102
|
+
|
|
103
|
+
if (all.length === 0) {
|
|
104
|
+
throw new ProdifyError(`Skill condition "${fieldName}" must declare at least one "all" predicate.`, {
|
|
105
|
+
code: 'SKILL_SCHEMA_INVALID'
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
all: all.sort((left, right) => `${left.fact}:${left.includes}`.localeCompare(`${right.fact}:${right.includes}`))
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function validateSkillDefinitionShape(value: unknown): SkillDefinition {
|
|
115
|
+
const record = asRecord(value, 'skill');
|
|
116
|
+
return {
|
|
117
|
+
schema_version: asString(record.schema_version, 'schema_version'),
|
|
118
|
+
id: asString(record.id, 'id'),
|
|
119
|
+
name: asString(record.name, 'name'),
|
|
120
|
+
version: asString(record.version, 'version'),
|
|
121
|
+
category: asSkillCategory(record.category),
|
|
122
|
+
description: asString(record.description, 'description'),
|
|
123
|
+
intended_use: asStringArray(record.intended_use, 'intended_use'),
|
|
124
|
+
stage_compatibility: asStageList(record.stage_compatibility, 'stage_compatibility'),
|
|
125
|
+
activation_conditions: Array.isArray(record.activation_conditions)
|
|
126
|
+
? record.activation_conditions.map((entry, index) => normalizeSkillCondition(entry, `activation_conditions[${index}]`))
|
|
127
|
+
: [],
|
|
128
|
+
execution_guidance: asStringArray(record.execution_guidance, 'execution_guidance'),
|
|
129
|
+
caution_guidance: asOptionalStringArray(record.caution_guidance, 'caution_guidance')
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function validateSkillRegistryManifest(value: unknown): SkillRegistryManifest {
|
|
134
|
+
const record = asRecord(value, 'skill registry');
|
|
135
|
+
return {
|
|
136
|
+
schema_version: asString(record.schema_version, 'schema_version'),
|
|
137
|
+
skills: asStringArray(record.skills, 'skills')
|
|
138
|
+
.map((entry) => normalizeRepoRelativePath(entry))
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function validateRoutingSkillIds(skillIds: string[], fieldName: string): string[] {
|
|
143
|
+
return [...new Set(skillIds.map((entry) => asString(entry, fieldName)))].sort((left, right) => left.localeCompare(right));
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function normalizeRoutingRule(value: unknown, fieldName: string): StageSkillRoutingRule {
|
|
147
|
+
const record = asRecord(value, fieldName);
|
|
148
|
+
return {
|
|
149
|
+
skill: asString(record.skill, `${fieldName}.skill`),
|
|
150
|
+
when: normalizeSkillCondition(record.when, `${fieldName}.when`),
|
|
151
|
+
reason: asString(record.reason, `${fieldName}.reason`)
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function normalizeStageSkillRouting(value: unknown): StageSkillRouting {
|
|
156
|
+
if (value === undefined || value === null) {
|
|
157
|
+
return {
|
|
158
|
+
default_skills: [],
|
|
159
|
+
allowed_skills: [],
|
|
160
|
+
conditional_skills: []
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const record = asRecord(value, 'skill_routing');
|
|
165
|
+
const defaultSkills = Array.isArray(record.default_skills)
|
|
166
|
+
? validateRoutingSkillIds(record.default_skills, 'skill_routing.default_skills')
|
|
167
|
+
: [];
|
|
168
|
+
const allowedSkills = Array.isArray(record.allowed_skills)
|
|
169
|
+
? validateRoutingSkillIds(record.allowed_skills, 'skill_routing.allowed_skills')
|
|
170
|
+
: [];
|
|
171
|
+
const conditionalSkills = Array.isArray(record.conditional_skills)
|
|
172
|
+
? record.conditional_skills.map((entry, index) => normalizeRoutingRule(entry, `skill_routing.conditional_skills[${index}]`))
|
|
173
|
+
.sort((left, right) => left.skill.localeCompare(right.skill) || left.reason.localeCompare(right.reason))
|
|
174
|
+
: [];
|
|
175
|
+
|
|
176
|
+
for (const skillId of defaultSkills) {
|
|
177
|
+
if (!allowedSkills.includes(skillId)) {
|
|
178
|
+
throw new ProdifyError(`Default skill "${skillId}" must also be listed in allowed_skills.`, {
|
|
179
|
+
code: 'SKILL_ROUTING_INVALID'
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
for (const rule of conditionalSkills) {
|
|
185
|
+
if (!allowedSkills.includes(rule.skill)) {
|
|
186
|
+
throw new ProdifyError(`Conditional skill "${rule.skill}" must also be listed in allowed_skills.`, {
|
|
187
|
+
code: 'SKILL_ROUTING_INVALID'
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return {
|
|
193
|
+
default_skills: defaultSkills,
|
|
194
|
+
allowed_skills: allowedSkills,
|
|
195
|
+
conditional_skills: conditionalSkills
|
|
196
|
+
};
|
|
197
|
+
}
|