chati-dev 4.3.0 → 4.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (156) hide show
  1. package/README.md +18 -13
  2. package/bin/chati.js +32 -4
  3. package/framework/agents/build/dev.md +9 -5
  4. package/framework/agents/discover/brief.md +2 -0
  5. package/framework/agents/discover/brownfield-wu.md +2 -0
  6. package/framework/agents/discover/greenfield-wu.md +18 -1
  7. package/framework/agents/plan/detail.md +2 -0
  8. package/framework/agents/plan/tasks.md +31 -0
  9. package/framework/agents/plan/ux-brand-architect.md +21 -2
  10. package/framework/agents/plan/ux-component-engineer.md +10 -0
  11. package/framework/agents/quality/qa-implementation.md +1 -1
  12. package/framework/agents/quality/qa-planning.md +1 -1
  13. package/framework/agents/quality/qa-visual.md +33 -21
  14. package/framework/config.yaml +5 -4
  15. package/framework/constitution.md +58 -11
  16. package/framework/context/governance.md +12 -10
  17. package/framework/context/root.md +2 -2
  18. package/framework/data/entity-registry.yaml +12 -4
  19. package/framework/data/qa-rubrics.yaml +335 -0
  20. package/framework/domains/constitution.yaml +3 -3
  21. package/framework/executors/json-validate.js +80 -0
  22. package/framework/executors/npm-script.js +72 -0
  23. package/framework/executors/path-check.js +67 -0
  24. package/framework/executors/registry.yaml +36 -0
  25. package/framework/hooks/advance-trigger.js +47 -3
  26. package/framework/hooks/constitution-guard.js +9 -4
  27. package/framework/hooks/git-push-authority.js +113 -0
  28. package/framework/hooks/license-guard.js +59 -9
  29. package/framework/hooks/model-governance.js +40 -17
  30. package/framework/hooks/prism-engine.js +78 -11
  31. package/framework/hooks/session-digest.js +12 -3
  32. package/framework/hooks/settings.json +7 -3
  33. package/framework/hooks/style-guard.js +52 -3
  34. package/framework/hooks/team-quality-gate.js +43 -5
  35. package/framework/i18n/en.yaml +3 -3
  36. package/framework/i18n/es.yaml +3 -3
  37. package/framework/i18n/fr.yaml +3 -3
  38. package/framework/i18n/pt.yaml +3 -3
  39. package/framework/intelligence/confidence.yaml +85 -6
  40. package/framework/intelligence/context-engine.md +9 -5
  41. package/framework/intelligence/decision-engine.md +1 -1
  42. package/framework/orchestrator/chati-router.js +59 -5
  43. package/framework/orchestrator/chati-update.md +19 -3
  44. package/framework/orchestrator/chati.md +20 -1
  45. package/framework/quality-gates/planning-gate.md +2 -0
  46. package/framework/rules/human-writing-style.md +47 -0
  47. package/framework/scaffold/motion-premium/README.md +12 -2
  48. package/framework/scaffold/motion-premium/lib/animations/tokens.ts.template +1 -1
  49. package/framework/scaffold/motion-premium/scaffold.yaml +6 -1
  50. package/framework/scaffold/motion-premium-3d/README.md +10 -0
  51. package/framework/scaffold/motion-premium-3d/components/webgl/WebGLContext.tsx.template +1 -1
  52. package/framework/scaffold/motion-premium-3d/scaffold.yaml +3 -0
  53. package/framework/scaffold/saas-dashboard/README.md +58 -0
  54. package/framework/scaffold/saas-dashboard/app/dashboard/page.tsx.template +546 -0
  55. package/framework/scaffold/saas-dashboard/app/globals.css.template +746 -0
  56. package/framework/scaffold/saas-dashboard/app/layout.tsx.template +192 -0
  57. package/framework/scaffold/saas-dashboard/app/shell.css.template +1070 -0
  58. package/framework/scaffold/saas-dashboard/components/data/Chart.tsx.template +725 -0
  59. package/framework/scaffold/saas-dashboard/components/data/DataTable.tsx.template +623 -0
  60. package/framework/scaffold/saas-dashboard/components/data/DataTable.types.ts.template +104 -0
  61. package/framework/scaffold/saas-dashboard/components/data/DataTablePagination.tsx.template +234 -0
  62. package/framework/scaffold/saas-dashboard/components/data/SkeletonBlock.tsx.template +111 -0
  63. package/framework/scaffold/saas-dashboard/components/data/StatCard.tsx.template +393 -0
  64. package/framework/scaffold/saas-dashboard/components/data/StatCardGrid.tsx.template +121 -0
  65. package/framework/scaffold/saas-dashboard/components/feedback/EmptyState.tsx.template +281 -0
  66. package/framework/scaffold/saas-dashboard/components/interaction/CommandPalette.tsx.template +767 -0
  67. package/framework/scaffold/saas-dashboard/components/interaction/Field.tsx.template +351 -0
  68. package/framework/scaffold/saas-dashboard/components/interaction/Form.tsx.template +424 -0
  69. package/framework/scaffold/saas-dashboard/components/primitives/Badge.tsx.template +193 -0
  70. package/framework/scaffold/saas-dashboard/components/primitives/Button.tsx.template +261 -0
  71. package/framework/scaffold/saas-dashboard/components/primitives/Card.tsx.template +287 -0
  72. package/framework/scaffold/saas-dashboard/components/primitives/Input.tsx.template +310 -0
  73. package/framework/scaffold/saas-dashboard/components/primitives/Spinner.tsx.template +135 -0
  74. package/framework/scaffold/saas-dashboard/components/primitives/index.ts.template +60 -0
  75. package/framework/scaffold/saas-dashboard/components/shell/AppShell.tsx.template +174 -0
  76. package/framework/scaffold/saas-dashboard/components/shell/Breadcrumb.tsx.template +186 -0
  77. package/framework/scaffold/saas-dashboard/components/shell/Container.tsx.template +86 -0
  78. package/framework/scaffold/saas-dashboard/components/shell/NavItem.tsx.template +147 -0
  79. package/framework/scaffold/saas-dashboard/components/shell/Sidebar.tsx.template +378 -0
  80. package/framework/scaffold/saas-dashboard/components/shell/SidebarContext.tsx.template +169 -0
  81. package/framework/scaffold/saas-dashboard/components/shell/ThemeToggle.tsx.template +128 -0
  82. package/framework/scaffold/saas-dashboard/components/shell/Topbar.tsx.template +301 -0
  83. package/framework/scaffold/saas-dashboard/lib/brand.ts.template +324 -0
  84. package/framework/scaffold/saas-dashboard/scaffold.yaml +225 -0
  85. package/framework/schemas/session.schema.json +5 -0
  86. package/framework/schemas/task.schema.json +9 -0
  87. package/framework/scripts/visual-qa.js +101 -3
  88. package/framework/tasks/orchestrator-health.md +4 -4
  89. package/framework/tasks/qa-impl-verdict.md +11 -5
  90. package/framework/templates/brandbook-html-tmpl.md +1 -1
  91. package/framework/templates/qa-gate-tmpl.yaml +24 -11
  92. package/package.json +4 -3
  93. package/src/config/context-file-generator.js +0 -6
  94. package/src/dashboard/renderer.js +0 -36
  95. package/src/executors/runner.js +204 -0
  96. package/src/installer/core.js +13 -3
  97. package/src/installer/templates.js +5 -6
  98. package/src/intelligence/registry-manager.js +1 -1
  99. package/src/license/client.js +9 -3
  100. package/src/license/commands.js +12 -2
  101. package/src/license/machine-id.js +42 -1
  102. package/src/memory/gotchas.js +58 -40
  103. package/src/memory/magic-docs.js +1 -1
  104. package/src/memory/session-digest.js +9 -4
  105. package/src/orchestrator/cli.js +186 -15
  106. package/src/orchestrator/doctor.js +98 -16
  107. package/src/orchestrator/pipeline-manager.js +44 -22
  108. package/src/orchestrator/session-manager.js +64 -8
  109. package/src/telemetry/sender.js +7 -19
  110. package/src/terminal/run-team.js +3 -3
  111. package/src/upgrade/tracked-files-detector.js +34 -16
  112. package/src/utils/feature-flags.js +1 -1
  113. package/src/utils/flatten-entities.js +4 -40
  114. package/src/utils/schema-validator.js +0 -14
  115. package/src/wizard/i18n.js +3 -3
  116. package/src/api/index.js +0 -120
  117. package/src/autonomy/autonomous-gate.js +0 -294
  118. package/src/autonomy/build-loop.js +0 -281
  119. package/src/autonomy/build-state.js +0 -286
  120. package/src/autonomy/cause-analyzer.js +0 -177
  121. package/src/autonomy/escalation.js +0 -214
  122. package/src/autonomy/index.js +0 -51
  123. package/src/autonomy/mode-manager.js +0 -225
  124. package/src/autonomy/mode-suggester.js +0 -283
  125. package/src/autonomy/progress-reporter.js +0 -275
  126. package/src/autonomy/safety-net.js +0 -370
  127. package/src/config/agent-customizer.js +0 -231
  128. package/src/decision/analyzer.js +0 -291
  129. package/src/decision/engine.js +0 -250
  130. package/src/decision/index.js +0 -38
  131. package/src/decision/registry-healer.js +0 -468
  132. package/src/decision/registry-updater.js +0 -339
  133. package/src/extensions/loader.js +0 -145
  134. package/src/extensions/registry.js +0 -134
  135. package/src/gates/circuit-breaker.js +0 -151
  136. package/src/gates/g1-planning-complete.js +0 -154
  137. package/src/gates/g2-qa-planning.js +0 -156
  138. package/src/gates/g3-implementation.js +0 -215
  139. package/src/gates/g4-qa-implementation.js +0 -240
  140. package/src/gates/g5-deploy-ready.js +0 -181
  141. package/src/gates/gate-base.js +0 -185
  142. package/src/gates/index.js +0 -46
  143. package/src/health/auto-fix.js +0 -216
  144. package/src/health/engine.js +0 -246
  145. package/src/merger/semantic-merger.js +0 -292
  146. package/src/preview/detector.js +0 -238
  147. package/src/preview/index.js +0 -20
  148. package/src/preview/launcher.js +0 -235
  149. package/src/preview/log-buffer.js +0 -103
  150. package/src/quality/metrics-collector.js +0 -281
  151. package/src/quality/test-runner.js +0 -366
  152. package/src/tasks/executor.js +0 -195
  153. package/src/tasks/index.js +0 -4
  154. package/src/tasks/loader.js +0 -210
  155. package/src/tasks/router.js +0 -182
  156. package/src/utils/event-bus.js +0 -126
@@ -1,231 +0,0 @@
1
- /**
2
- * @fileoverview Agent customization via per-project override files.
3
- *
4
- * Loads `.customize.yaml` files from `chati.dev/customize/<agent>.yaml`
5
- * and applies overrides to agent prompts.
6
- *
7
- * Constitution Article XVI — Model Governance.
8
- */
9
-
10
- import { existsSync, readFileSync } from 'fs';
11
- import { join } from 'path';
12
- import yaml from 'js-yaml';
13
- import { resolveFrameworkDir } from '../utils/framework-dir.js';
14
-
15
- // ---------------------------------------------------------------------------
16
- // Constants
17
- // ---------------------------------------------------------------------------
18
-
19
- /**
20
- * Get the customize directory for a given project.
21
- * Supports both .chati.dev/ and chati.dev/ structures.
22
- * @param {string} projectDir
23
- * @returns {string} Relative path from project root
24
- */
25
- function getCustomizeDir(projectDir) {
26
- return `${resolveFrameworkDir(projectDir)}/customize`;
27
- }
28
-
29
- /** Valid override fields that customizations can set. */
30
- export const VALID_OVERRIDE_FIELDS = [
31
- 'model',
32
- 'extra_context',
33
- 'skip_sections',
34
- 'add_sections',
35
- 'provider',
36
- 'timeout',
37
- 'max_iterations',
38
- ];
39
-
40
- // ---------------------------------------------------------------------------
41
- // Public API
42
- // ---------------------------------------------------------------------------
43
-
44
- /**
45
- * @typedef {object} Customization
46
- * @property {boolean} found - Whether a customization file was found
47
- * @property {string} [model] - Override model name
48
- * @property {string} [extra_context] - Additional context to inject
49
- * @property {string[]} [skip_sections] - Sections to omit from prompt
50
- * @property {{ name: string, content: string }[]} [add_sections] - Sections to append
51
- * @property {string} [provider] - Override provider name
52
- * @property {number} [timeout] - Override timeout in ms
53
- * @property {number} [max_iterations] - Override max iterations
54
- */
55
-
56
- /**
57
- * Load customization for a specific agent.
58
- *
59
- * @param {string} projectDir - Project root directory
60
- * @param {string} agentName - Agent name (e.g., 'brief', 'dev')
61
- * @returns {{ found: boolean, overrides: Customization }}
62
- */
63
- export function loadCustomization(projectDir, agentName) {
64
- if (!projectDir || !agentName) {
65
- return { found: false, overrides: {} };
66
- }
67
-
68
- const customizePath = join(projectDir, getCustomizeDir(projectDir), `${agentName}.yaml`);
69
-
70
- if (!existsSync(customizePath)) {
71
- return { found: false, overrides: {} };
72
- }
73
-
74
- try {
75
- const raw = readFileSync(customizePath, 'utf-8');
76
- const parsed = yaml.load(raw);
77
-
78
- if (!parsed || typeof parsed !== 'object') {
79
- return { found: true, overrides: {} };
80
- }
81
-
82
- // Filter to valid override fields only
83
- const overrides = {};
84
- for (const field of VALID_OVERRIDE_FIELDS) {
85
- if (parsed[field] !== undefined) {
86
- overrides[field] = parsed[field];
87
- }
88
- }
89
-
90
- return { found: true, overrides };
91
- } catch { /* expected: content may be malformed */
92
- return { found: false, overrides: {} };
93
- }
94
- }
95
-
96
- /**
97
- * Apply customization overrides to an agent prompt.
98
- *
99
- * @param {string} agentPrompt - Original agent prompt
100
- * @param {Customization} customization - Customization overrides
101
- * @returns {string} Modified prompt
102
- */
103
- export function applyCustomization(agentPrompt, customization) {
104
- if (!agentPrompt || !customization) {
105
- return agentPrompt || '';
106
- }
107
-
108
- let prompt = agentPrompt;
109
-
110
- // Skip sections
111
- if (customization.skip_sections && Array.isArray(customization.skip_sections)) {
112
- for (const sectionName of customization.skip_sections) {
113
- // Match section heading + all content until the next heading of same or higher level, or end of text
114
- // Two-pass approach: try lookahead for next heading, then fallback to end
115
- const headingPattern = new RegExp(
116
- `^(#{1,3})\\s+${escapeRegex(sectionName)}[^\\n]*\\n([\\s\\S]*?)(?=^#{1,3}\\s)`,
117
- 'gm'
118
- );
119
- let replaced = prompt.replace(headingPattern, '');
120
- if (replaced === prompt) {
121
- // Section is at the end — no next heading follows
122
- const endPattern = new RegExp(
123
- `^(#{1,3})\\s+${escapeRegex(sectionName)}[^\\n]*\\n[\\s\\S]*$`,
124
- 'gm'
125
- );
126
- replaced = prompt.replace(endPattern, '');
127
- }
128
- prompt = replaced;
129
- }
130
- }
131
-
132
- // Add extra context at the end
133
- if (customization.extra_context && typeof customization.extra_context === 'string') {
134
- prompt += `\n\n---\n\n## Additional Context\n\n${customization.extra_context}\n`;
135
- }
136
-
137
- // Add new sections
138
- if (customization.add_sections && Array.isArray(customization.add_sections)) {
139
- for (const section of customization.add_sections) {
140
- if (section.name && section.content) {
141
- prompt += `\n\n## ${section.name}\n\n${section.content}\n`;
142
- }
143
- }
144
- }
145
-
146
- return prompt;
147
- }
148
-
149
- /**
150
- * Validate a customization object.
151
- *
152
- * @param {object} customization
153
- * @returns {{ valid: boolean, errors: string[] }}
154
- */
155
- export function validateCustomization(customization) {
156
- const errors = [];
157
-
158
- if (!customization || typeof customization !== 'object') {
159
- errors.push('Customization must be a non-null object');
160
- return { valid: false, errors };
161
- }
162
-
163
- // Check for unknown fields
164
- for (const key of Object.keys(customization)) {
165
- if (!VALID_OVERRIDE_FIELDS.includes(key)) {
166
- errors.push(`Unknown override field: "${key}"`);
167
- }
168
- }
169
-
170
- // Validate model
171
- if (customization.model !== undefined && typeof customization.model !== 'string') {
172
- errors.push('model must be a string');
173
- }
174
-
175
- // Validate extra_context
176
- if (customization.extra_context !== undefined && typeof customization.extra_context !== 'string') {
177
- errors.push('extra_context must be a string');
178
- }
179
-
180
- // Validate skip_sections
181
- if (customization.skip_sections !== undefined) {
182
- if (!Array.isArray(customization.skip_sections)) {
183
- errors.push('skip_sections must be an array');
184
- } else if (customization.skip_sections.some(s => typeof s !== 'string')) {
185
- errors.push('skip_sections must contain only strings');
186
- }
187
- }
188
-
189
- // Validate add_sections
190
- if (customization.add_sections !== undefined) {
191
- if (!Array.isArray(customization.add_sections)) {
192
- errors.push('add_sections must be an array');
193
- } else {
194
- for (const section of customization.add_sections) {
195
- if (!section.name || !section.content) {
196
- errors.push('Each add_section must have name and content');
197
- }
198
- }
199
- }
200
- }
201
-
202
- // Validate timeout
203
- if (customization.timeout !== undefined) {
204
- if (typeof customization.timeout !== 'number' || customization.timeout <= 0) {
205
- errors.push('timeout must be a positive number');
206
- }
207
- }
208
-
209
- // Validate max_iterations
210
- if (customization.max_iterations !== undefined) {
211
- if (typeof customization.max_iterations !== 'number' || customization.max_iterations <= 0) {
212
- errors.push('max_iterations must be a positive number');
213
- }
214
- }
215
-
216
- return { valid: errors.length === 0, errors };
217
- }
218
-
219
- // ---------------------------------------------------------------------------
220
- // Helpers
221
- // ---------------------------------------------------------------------------
222
-
223
- /**
224
- * Escape special regex characters in a string.
225
- *
226
- * @param {string} str
227
- * @returns {string}
228
- */
229
- function escapeRegex(str) {
230
- return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
231
- }
@@ -1,291 +0,0 @@
1
- /**
2
- * Impact Analyzer - Dependency analysis for chati.dev
3
- * Analyzes impact of changes via dependency graph traversal
4
- *
5
- * @module decision/analyzer
6
- */
7
-
8
- import { readFileSync, existsSync } from 'fs';
9
- import { join } from 'path';
10
- import yaml from 'js-yaml';
11
- import { resolveFrameworkDir } from '../utils/framework-dir.js';
12
- import { flattenEntitiesFromRegistry } from '../utils/flatten-entities.js';
13
-
14
- /**
15
- * Analyze impact of changing an entity.
16
- * Builds dependency graph from entity-registry and calculates affected entities.
17
- *
18
- * @param {string} projectDir
19
- * @param {string} entityPath - Path of entity being changed
20
- * @returns {{ impactLevel: 'low'|'medium'|'high'|'critical', affectedEntities: object[], dependencyChain: string[] }}
21
- */
22
- export function analyzeImpact(projectDir, entityPath) {
23
- const graph = buildDependencyGraph(projectDir);
24
- const affectedPaths = getTransitiveDependents(graph, entityPath);
25
-
26
- // Determine impact level based on affected entity count
27
- let impactLevel;
28
- const affectedCount = affectedPaths.length;
29
-
30
- if (affectedCount === 0) {
31
- impactLevel = 'low';
32
- } else if (affectedCount <= 2) {
33
- impactLevel = 'low';
34
- } else if (affectedCount <= 5) {
35
- impactLevel = 'medium';
36
- } else if (affectedCount <= 10) {
37
- impactLevel = 'high';
38
- } else {
39
- impactLevel = 'critical';
40
- }
41
-
42
- // Load entity details
43
- const registryPath = join(projectDir, resolveFrameworkDir(projectDir), 'entity-registry.yaml');
44
- const registry = existsSync(registryPath)
45
- ? yaml.load(readFileSync(registryPath, 'utf8'))
46
- : { entities: {} };
47
-
48
- const allEntities = flattenEntitiesFromRegistry(registry);
49
- const entityMap = new Map(allEntities.map(e => [e.path, e]));
50
-
51
- const affectedEntities = affectedPaths.map(path => {
52
- const entity = entityMap.get(path);
53
- return {
54
- path,
55
- type: entity?.type || 'unknown',
56
- name: entity?.name || path.split('/').pop()
57
- };
58
- });
59
-
60
- return {
61
- impactLevel,
62
- affectedEntities,
63
- dependencyChain: affectedPaths
64
- };
65
- }
66
-
67
- /**
68
- * Build dependency graph from entity registry.
69
- * @param {string} projectDir
70
- * @returns {Map<string, string[]>} Map of entityPath -> [dependentPaths]
71
- */
72
- export function buildDependencyGraph(projectDir) {
73
- const registryPath = join(projectDir, resolveFrameworkDir(projectDir), 'entity-registry.yaml');
74
- const graph = new Map();
75
-
76
- if (!existsSync(registryPath)) {
77
- return graph;
78
- }
79
-
80
- const registry = yaml.load(readFileSync(registryPath, 'utf8'));
81
- const entities = flattenEntitiesFromRegistry(registry);
82
-
83
- // Initialize graph with all entities
84
- entities.forEach(entity => {
85
- if (entity.path) {
86
- graph.set(entity.path, []);
87
- }
88
- });
89
-
90
- // Build reverse dependency map (who depends on whom)
91
- entities.forEach(entity => {
92
- if (entity.dependencies && Array.isArray(entity.dependencies)) {
93
- entity.dependencies.forEach(depPath => {
94
- if (!graph.has(depPath)) {
95
- graph.set(depPath, []);
96
- }
97
- // Add entity.path as dependent of depPath
98
- const dependents = graph.get(depPath);
99
- if (!dependents.includes(entity.path)) {
100
- dependents.push(entity.path);
101
- }
102
- });
103
- }
104
- });
105
-
106
- return graph;
107
- }
108
-
109
- /**
110
- * Get all entities that depend on a given entity (transitive).
111
- * Uses BFS traversal.
112
- * @param {Map<string, string[]>} graph
113
- * @param {string} entityPath
114
- * @returns {string[]} All affected paths
115
- */
116
- export function getTransitiveDependents(graph, entityPath) {
117
- const visited = new Set();
118
- const queue = [entityPath];
119
- const result = [];
120
-
121
- while (queue.length > 0) {
122
- const current = queue.shift();
123
-
124
- if (visited.has(current)) {
125
- continue;
126
- }
127
-
128
- visited.add(current);
129
-
130
- // Get direct dependents
131
- const dependents = graph.get(current) || [];
132
-
133
- dependents.forEach(dependent => {
134
- if (!visited.has(dependent)) {
135
- queue.push(dependent);
136
- result.push(dependent);
137
- }
138
- });
139
- }
140
-
141
- return result;
142
- }
143
-
144
- /**
145
- * Score a decision option based on multiple criteria using TF-IDF-inspired
146
- * keyword relevance scoring.
147
- *
148
- * @param {{ name: string, description?: string, keywords?: string[] }} option
149
- * @param {{ requirements?: string[], constraints?: string[], existingEntities?: string[] }} context
150
- * @returns {{ score: number, alignment: number, complexity: number, reuse: number, breakdown: object }}
151
- */
152
- export function scoreDecision(option, context = {}) {
153
- if (!option || !option.name) {
154
- return { score: 0, alignment: 0, complexity: 0, reuse: 0, breakdown: {} };
155
- }
156
-
157
- const requirements = context.requirements || [];
158
- const constraints = context.constraints || [];
159
- const existingEntities = context.existingEntities || [];
160
-
161
- // 1. Alignment score — how well does option match requirements?
162
- const optionText = `${option.name} ${option.description || ''} ${(option.keywords || []).join(' ')}`.toLowerCase();
163
- const alignment = computeRelevance(optionText, requirements);
164
-
165
- // 2. Complexity score (inverse — lower complexity = higher score)
166
- const complexitySignals = ['microservice', 'distributed', 'multi-tenant', 'real-time', 'concurrent', 'migration'];
167
- let complexityCount = 0;
168
- for (const signal of complexitySignals) {
169
- if (optionText.includes(signal)) {
170
- complexityCount++;
171
- }
172
- }
173
- const complexity = Math.max(0, 1 - complexityCount * 0.15);
174
-
175
- // 3. Reuse score — overlap with existing entities (decision priority: REUSE > ADAPT > CREATE)
176
- let reuse = 0;
177
- if (existingEntities.length > 0) {
178
- const optionWords = new Set(optionText.split(/\s+/));
179
- let matchCount = 0;
180
- for (const entity of existingEntities) {
181
- const entityWords = entity.toLowerCase().split(/[\s/.-]+/);
182
- for (const word of entityWords) {
183
- if (word.length > 3 && optionWords.has(word)) {
184
- matchCount++;
185
- break;
186
- }
187
- }
188
- }
189
- reuse = existingEntities.length > 0 ? matchCount / existingEntities.length : 0;
190
- }
191
-
192
- // 4. Constraint satisfaction
193
- let constraintScore = 1;
194
- if (constraints.length > 0) {
195
- let satisfied = 0;
196
- for (const constraint of constraints) {
197
- const constraintWords = constraint.toLowerCase().split(/\s+/);
198
- const hasMatch = constraintWords.some(w => w.length > 3 && optionText.includes(w));
199
- if (hasMatch) satisfied++;
200
- }
201
- constraintScore = constraints.length > 0 ? satisfied / constraints.length : 1;
202
- }
203
-
204
- // Weighted final score
205
- const score = Math.round(
206
- (alignment * 0.4 + complexity * 0.2 + reuse * 0.25 + constraintScore * 0.15) * 100
207
- ) / 100;
208
-
209
- return {
210
- score,
211
- alignment: Math.round(alignment * 100) / 100,
212
- complexity: Math.round(complexity * 100) / 100,
213
- reuse: Math.round(reuse * 100) / 100,
214
- breakdown: {
215
- alignment: { weight: 0.4, raw: alignment },
216
- complexity: { weight: 0.2, raw: complexity },
217
- reuse: { weight: 0.25, raw: reuse },
218
- constraints: { weight: 0.15, raw: constraintScore },
219
- },
220
- };
221
- }
222
-
223
- // ---------------------------------------------------------------------------
224
- // TF-IDF Helpers
225
- // ---------------------------------------------------------------------------
226
-
227
- /**
228
- * Compute TF-IDF-inspired relevance between text and a set of documents.
229
- *
230
- * @param {string} text - Source text
231
- * @param {string[]} documents - Reference documents (requirements, etc.)
232
- * @returns {number} Relevance score (0-1)
233
- */
234
- function computeRelevance(text, documents) {
235
- if (!text || documents.length === 0) return 0;
236
-
237
- const textWords = tokenize(text);
238
- if (textWords.length === 0) return 0;
239
-
240
- // Build document frequency map
241
- const docCount = documents.length;
242
- const df = new Map();
243
-
244
- for (const doc of documents) {
245
- const words = new Set(tokenize(doc.toLowerCase()));
246
- for (const word of words) {
247
- df.set(word, (df.get(word) || 0) + 1);
248
- }
249
- }
250
-
251
- // Calculate TF-IDF score for each text word
252
- const textWordSet = new Set(textWords);
253
- let totalScore = 0;
254
-
255
- for (const word of textWordSet) {
256
- const termFreq = textWords.filter(w => w === word).length / textWords.length;
257
- const docFreq = df.get(word) || 0;
258
-
259
- if (docFreq > 0) {
260
- // IDF = log(N / df) — boost rare terms
261
- const idf = Math.log((docCount + 1) / (docFreq + 1)) + 1;
262
- totalScore += termFreq * idf;
263
- }
264
- }
265
-
266
- // Normalize to 0-1 range
267
- const maxPossible = Math.log(docCount + 1) + 1;
268
- return maxPossible > 0 ? Math.min(totalScore / maxPossible, 1) : 0;
269
- }
270
-
271
- /**
272
- * Tokenize text into meaningful words (skip stopwords and short tokens).
273
- *
274
- * @param {string} text
275
- * @returns {string[]}
276
- */
277
- function tokenize(text) {
278
- const stopwords = new Set([
279
- 'the', 'a', 'an', 'and', 'or', 'but', 'in', 'on', 'at', 'to', 'for',
280
- 'of', 'with', 'by', 'is', 'are', 'was', 'were', 'be', 'been', 'being',
281
- 'have', 'has', 'had', 'do', 'does', 'did', 'will', 'would', 'could',
282
- 'should', 'may', 'might', 'shall', 'can', 'this', 'that', 'these',
283
- 'those', 'it', 'its', 'not', 'no', 'from', 'as', 'into', 'all',
284
- ]);
285
-
286
- return text
287
- .toLowerCase()
288
- .split(/[\s,.;:!?()[\]{}"'`/\\-]+/)
289
- .filter(w => w.length > 2 && !stopwords.has(w));
290
- }
291
-