scene-capability-engine 3.0.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 (336) hide show
  1. package/CHANGELOG.md +2513 -0
  2. package/LICENSE +21 -0
  3. package/README.md +765 -0
  4. package/README.zh.md +630 -0
  5. package/bin/kiro-spec-engine.js +796 -0
  6. package/bin/kse.js +3 -0
  7. package/bin/sce.js +3 -0
  8. package/bin/sco.js +3 -0
  9. package/docs/331-poc-adaptation-roadmap.md +156 -0
  10. package/docs/331-poc-dual-track-integration-guide.md +120 -0
  11. package/docs/331-poc-weekly-delivery-checklist.md +52 -0
  12. package/docs/OFFLINE_INSTALL.md +96 -0
  13. package/docs/README.md +279 -0
  14. package/docs/adopt-migration-guide.md +599 -0
  15. package/docs/adoption-guide.md +616 -0
  16. package/docs/agent-hooks-analysis.md +815 -0
  17. package/docs/architecture.md +733 -0
  18. package/docs/articles/ai-driven-development-philosophy-and-practice-review.md +208 -0
  19. package/docs/articles/ai-driven-development-philosophy-and-practice.en.md +459 -0
  20. package/docs/articles/ai-driven-development-philosophy-and-practice.md +492 -0
  21. package/docs/autonomous-control-guide.md +851 -0
  22. package/docs/command-reference.md +1368 -0
  23. package/docs/community.md +115 -0
  24. package/docs/cross-tool-guide.md +555 -0
  25. package/docs/developer-guide.md +619 -0
  26. package/docs/document-governance.md +865 -0
  27. package/docs/environment-management-guide.md +526 -0
  28. package/docs/examples/add-export-command/design.md +194 -0
  29. package/docs/examples/add-export-command/requirements.md +110 -0
  30. package/docs/examples/add-export-command/tasks.md +88 -0
  31. package/docs/examples/add-rest-api/design.md +855 -0
  32. package/docs/examples/add-rest-api/requirements.md +323 -0
  33. package/docs/examples/add-rest-api/tasks.md +355 -0
  34. package/docs/examples/add-user-dashboard/design.md +192 -0
  35. package/docs/examples/add-user-dashboard/requirements.md +143 -0
  36. package/docs/examples/add-user-dashboard/tasks.md +91 -0
  37. package/docs/faq.md +697 -0
  38. package/docs/handoffs/evidence/ontology/moqui-template-baseline-2026-02-17-232922.json +156 -0
  39. package/docs/handoffs/evidence/ontology/moqui-template-baseline-2026-02-17-232922.md +24 -0
  40. package/docs/images/wechat-qr.png +0 -0
  41. package/docs/integration-modes.md +529 -0
  42. package/docs/integration-philosophy.md +313 -0
  43. package/docs/knowledge-management-guide.md +263 -0
  44. package/docs/manual-workflows-guide.md +418 -0
  45. package/docs/moqui-capability-matrix.md +73 -0
  46. package/docs/moqui-template-core-library-playbook.md +109 -0
  47. package/docs/multi-agent-coordination-guide.md +553 -0
  48. package/docs/multi-repo-management-guide.md +1344 -0
  49. package/docs/quick-start-with-ai-tools.md +375 -0
  50. package/docs/quick-start.md +146 -0
  51. package/docs/release-checklist.md +121 -0
  52. package/docs/releases/README.md +13 -0
  53. package/docs/releases/v1.46.2-validation.md +45 -0
  54. package/docs/releases/v1.46.2.md +50 -0
  55. package/docs/scene-runtime-guide.md +347 -0
  56. package/docs/spec-collaboration-guide.md +369 -0
  57. package/docs/spec-locking-guide.md +225 -0
  58. package/docs/spec-numbering-guide.md +348 -0
  59. package/docs/spec-workflow.md +519 -0
  60. package/docs/steering-strategy-guide.md +196 -0
  61. package/docs/team-collaboration-guide.md +465 -0
  62. package/docs/testing-strategy.md +272 -0
  63. package/docs/tools/claude-guide.md +654 -0
  64. package/docs/tools/cursor-guide.md +706 -0
  65. package/docs/tools/generic-guide.md +446 -0
  66. package/docs/tools/kiro-guide.md +308 -0
  67. package/docs/tools/vscode-guide.md +445 -0
  68. package/docs/tools/windsurf-guide.md +391 -0
  69. package/docs/troubleshooting.md +1135 -0
  70. package/docs/upgrade-guide.md +639 -0
  71. package/docs/value-observability-guide.md +127 -0
  72. package/docs/zh/README.md +341 -0
  73. package/docs/zh/quick-start.md +764 -0
  74. package/docs/zh/release-checklist.md +121 -0
  75. package/docs/zh/releases/README.md +13 -0
  76. package/docs/zh/releases/v1.46.2-validation.md +45 -0
  77. package/docs/zh/releases/v1.46.2.md +50 -0
  78. package/docs/zh/spec-numbering-guide.md +348 -0
  79. package/docs/zh/tools/claude-guide.md +349 -0
  80. package/docs/zh/tools/cursor-guide.md +281 -0
  81. package/docs/zh/tools/generic-guide.md +499 -0
  82. package/docs/zh/tools/kiro-guide.md +342 -0
  83. package/docs/zh/tools/vscode-guide.md +449 -0
  84. package/docs/zh/tools/windsurf-guide.md +378 -0
  85. package/docs/zh/value-observability-guide.md +127 -0
  86. package/docs//344/272/244/344/273/230/346/270/205/345/215/225.md +75 -0
  87. package/lib/adoption/adoption-logger.js +487 -0
  88. package/lib/adoption/adoption-strategy.js +538 -0
  89. package/lib/adoption/backup-manager.js +420 -0
  90. package/lib/adoption/conflict-resolver.js +410 -0
  91. package/lib/adoption/detection-engine.js +275 -0
  92. package/lib/adoption/diff-viewer.js +226 -0
  93. package/lib/adoption/error-formatter.js +509 -0
  94. package/lib/adoption/file-classifier.js +385 -0
  95. package/lib/adoption/progress-reporter.js +534 -0
  96. package/lib/adoption/smart-orchestrator.js +470 -0
  97. package/lib/adoption/strategy-selector.js +218 -0
  98. package/lib/adoption/summary-generator.js +493 -0
  99. package/lib/adoption/template-sync.js +605 -0
  100. package/lib/auto/autonomous-engine.js +485 -0
  101. package/lib/auto/checkpoint-manager.js +300 -0
  102. package/lib/auto/close-loop-runner.js +2476 -0
  103. package/lib/auto/config-schema.js +176 -0
  104. package/lib/auto/decision-engine.js +344 -0
  105. package/lib/auto/error-recovery-manager.js +580 -0
  106. package/lib/auto/goal-decomposer.js +278 -0
  107. package/lib/auto/progress-tracker.js +502 -0
  108. package/lib/auto/safety-manager.js +186 -0
  109. package/lib/auto/semantic-decomposer.js +137 -0
  110. package/lib/auto/state-manager.js +126 -0
  111. package/lib/auto/task-queue-manager.js +340 -0
  112. package/lib/backup/backup-system.js +372 -0
  113. package/lib/backup/selective-backup.js +207 -0
  114. package/lib/collab/agent-registry.js +240 -0
  115. package/lib/collab/collab-manager.js +285 -0
  116. package/lib/collab/contract-manager.js +320 -0
  117. package/lib/collab/coordinator.js +370 -0
  118. package/lib/collab/dependency-manager.js +280 -0
  119. package/lib/collab/index.js +20 -0
  120. package/lib/collab/integration-manager.js +202 -0
  121. package/lib/collab/merge-coordinator.js +252 -0
  122. package/lib/collab/metadata-manager.js +233 -0
  123. package/lib/collab/multi-agent-config.js +120 -0
  124. package/lib/collab/spec-lifecycle-manager.js +304 -0
  125. package/lib/collab/sync-barrier.js +88 -0
  126. package/lib/collab/visualizer.js +208 -0
  127. package/lib/commands/adopt.js +749 -0
  128. package/lib/commands/auto.js +19559 -0
  129. package/lib/commands/collab.js +275 -0
  130. package/lib/commands/context.js +99 -0
  131. package/lib/commands/docs.js +808 -0
  132. package/lib/commands/doctor.js +273 -0
  133. package/lib/commands/env.js +420 -0
  134. package/lib/commands/knowledge.js +309 -0
  135. package/lib/commands/lock.js +235 -0
  136. package/lib/commands/ops.js +409 -0
  137. package/lib/commands/orchestrate.js +446 -0
  138. package/lib/commands/prompt.js +105 -0
  139. package/lib/commands/repo.js +118 -0
  140. package/lib/commands/rollback.js +219 -0
  141. package/lib/commands/scene.js +15549 -0
  142. package/lib/commands/spec-bootstrap.js +147 -0
  143. package/lib/commands/spec-gate.js +157 -0
  144. package/lib/commands/spec-pipeline.js +205 -0
  145. package/lib/commands/status.js +321 -0
  146. package/lib/commands/task.js +199 -0
  147. package/lib/commands/templates.js +654 -0
  148. package/lib/commands/upgrade.js +231 -0
  149. package/lib/commands/value.js +569 -0
  150. package/lib/commands/watch.js +684 -0
  151. package/lib/commands/workflows.js +240 -0
  152. package/lib/commands/workspace-multi.js +325 -0
  153. package/lib/commands/workspace.js +189 -0
  154. package/lib/context/context-exporter.js +378 -0
  155. package/lib/context/prompt-generator.js +482 -0
  156. package/lib/data/moqui-capability-lexicon.json +45 -0
  157. package/lib/environment/backup-system.js +189 -0
  158. package/lib/environment/environment-manager.js +379 -0
  159. package/lib/environment/environment-registry.js +168 -0
  160. package/lib/gitignore/gitignore-backup.js +229 -0
  161. package/lib/gitignore/gitignore-detector.js +239 -0
  162. package/lib/gitignore/gitignore-integration.js +267 -0
  163. package/lib/gitignore/gitignore-transformer.js +193 -0
  164. package/lib/gitignore/layered-rules-template.js +42 -0
  165. package/lib/governance/archive-tool.js +284 -0
  166. package/lib/governance/cleanup-tool.js +237 -0
  167. package/lib/governance/config-manager.js +186 -0
  168. package/lib/governance/diagnostic-engine.js +271 -0
  169. package/lib/governance/doc-reference-checker.js +200 -0
  170. package/lib/governance/execution-logger.js +243 -0
  171. package/lib/governance/file-scanner.js +285 -0
  172. package/lib/governance/hooks-manager.js +333 -0
  173. package/lib/governance/reporter.js +337 -0
  174. package/lib/governance/validation-engine.js +181 -0
  175. package/lib/i18n.js +79 -0
  176. package/lib/knowledge/entry-manager.js +208 -0
  177. package/lib/knowledge/index-manager.js +261 -0
  178. package/lib/knowledge/knowledge-manager.js +273 -0
  179. package/lib/knowledge/template-manager.js +191 -0
  180. package/lib/lock/index.js +21 -0
  181. package/lib/lock/lock-file.js +192 -0
  182. package/lib/lock/lock-manager.js +321 -0
  183. package/lib/lock/machine-identifier.js +135 -0
  184. package/lib/lock/steering-file-lock.js +207 -0
  185. package/lib/lock/task-lock-manager.js +345 -0
  186. package/lib/operations/audit-logger.js +293 -0
  187. package/lib/operations/feedback-manager.js +1147 -0
  188. package/lib/operations/index.js +23 -0
  189. package/lib/operations/models/index.js +170 -0
  190. package/lib/operations/operations-manager.js +151 -0
  191. package/lib/operations/operations-validator.js +280 -0
  192. package/lib/operations/permission-manager.js +354 -0
  193. package/lib/operations/template-loader.js +143 -0
  194. package/lib/orchestrator/agent-spawner.js +629 -0
  195. package/lib/orchestrator/bootstrap-prompt-builder.js +236 -0
  196. package/lib/orchestrator/index.js +19 -0
  197. package/lib/orchestrator/orchestration-engine.js +1270 -0
  198. package/lib/orchestrator/orchestrator-config.js +173 -0
  199. package/lib/orchestrator/status-monitor.js +591 -0
  200. package/lib/python-checker.js +209 -0
  201. package/lib/repo/config-manager.js +580 -0
  202. package/lib/repo/errors/config-error.js +13 -0
  203. package/lib/repo/errors/git-error.js +15 -0
  204. package/lib/repo/errors/repo-error.js +14 -0
  205. package/lib/repo/git-operations.js +181 -0
  206. package/lib/repo/handlers/.gitkeep +1 -0
  207. package/lib/repo/handlers/exec-handler.js +155 -0
  208. package/lib/repo/handlers/health-handler.js +169 -0
  209. package/lib/repo/handlers/init-handler.js +197 -0
  210. package/lib/repo/handlers/status-handler.js +176 -0
  211. package/lib/repo/output-formatter.js +184 -0
  212. package/lib/repo/path-resolver.js +178 -0
  213. package/lib/repo/repo-manager.js +514 -0
  214. package/lib/scene-runtime/audit-emitter.js +59 -0
  215. package/lib/scene-runtime/binding-plugin-loader.js +351 -0
  216. package/lib/scene-runtime/binding-registry.js +349 -0
  217. package/lib/scene-runtime/eval-bridge.js +44 -0
  218. package/lib/scene-runtime/index.js +19 -0
  219. package/lib/scene-runtime/moqui-adapter.js +620 -0
  220. package/lib/scene-runtime/moqui-client.js +606 -0
  221. package/lib/scene-runtime/moqui-extractor.js +2029 -0
  222. package/lib/scene-runtime/plan-compiler.js +208 -0
  223. package/lib/scene-runtime/policy-gate.js +58 -0
  224. package/lib/scene-runtime/runtime-executor.js +358 -0
  225. package/lib/scene-runtime/scene-loader.js +96 -0
  226. package/lib/scene-runtime/scene-ontology.js +959 -0
  227. package/lib/scene-runtime/scene-template-linter.js +852 -0
  228. package/lib/scene-runtime/templates/scene-template-erp-query-v0.1.yaml +28 -0
  229. package/lib/scene-runtime/templates/scene-template-hybrid-shadow-v0.1.yaml +34 -0
  230. package/lib/spec/bootstrap/context-collector.js +48 -0
  231. package/lib/spec/bootstrap/draft-generator.js +158 -0
  232. package/lib/spec/bootstrap/questionnaire-engine.js +70 -0
  233. package/lib/spec/bootstrap/trace-emitter.js +59 -0
  234. package/lib/spec/multi-spec-orchestrate.js +93 -0
  235. package/lib/spec/pipeline/constants.js +6 -0
  236. package/lib/spec/pipeline/stage-adapters.js +118 -0
  237. package/lib/spec/pipeline/stage-runner.js +146 -0
  238. package/lib/spec/pipeline/state-store.js +119 -0
  239. package/lib/spec-gate/engine/gate-engine.js +165 -0
  240. package/lib/spec-gate/policy/default-policy.js +22 -0
  241. package/lib/spec-gate/policy/policy-loader.js +103 -0
  242. package/lib/spec-gate/result-emitter.js +81 -0
  243. package/lib/spec-gate/rules/default-rules.js +156 -0
  244. package/lib/spec-gate/rules/rule-registry.js +51 -0
  245. package/lib/steering/adoption-config.js +164 -0
  246. package/lib/steering/compliance-auto-fixer.js +204 -0
  247. package/lib/steering/compliance-cache.js +99 -0
  248. package/lib/steering/compliance-error-reporter.js +70 -0
  249. package/lib/steering/context-sync-manager.js +273 -0
  250. package/lib/steering/index.js +92 -0
  251. package/lib/steering/spec-steering.js +230 -0
  252. package/lib/steering/steering-compliance-checker.js +73 -0
  253. package/lib/steering/steering-loader.js +144 -0
  254. package/lib/steering/steering-manager.js +289 -0
  255. package/lib/task/index.js +12 -0
  256. package/lib/task/task-claimer.js +489 -0
  257. package/lib/task/task-status-store.js +418 -0
  258. package/lib/templates/cache-manager.js +440 -0
  259. package/lib/templates/content-generalizer.js +247 -0
  260. package/lib/templates/frontmatter-generator.js +128 -0
  261. package/lib/templates/git-handler.js +471 -0
  262. package/lib/templates/metadata-collector.js +328 -0
  263. package/lib/templates/path-utils.js +144 -0
  264. package/lib/templates/registry-parser.js +505 -0
  265. package/lib/templates/spec-reader.js +216 -0
  266. package/lib/templates/template-applicator.js +249 -0
  267. package/lib/templates/template-creator.js +256 -0
  268. package/lib/templates/template-error.js +143 -0
  269. package/lib/templates/template-exporter.js +502 -0
  270. package/lib/templates/template-manager.js +782 -0
  271. package/lib/templates/template-validator.js +361 -0
  272. package/lib/upgrade/migration-engine.js +382 -0
  273. package/lib/upgrade/migrations/.gitkeep +52 -0
  274. package/lib/upgrade/migrations/1.0.0-to-1.1.0.js +78 -0
  275. package/lib/utils/file-diff.js +177 -0
  276. package/lib/utils/fs-utils.js +274 -0
  277. package/lib/utils/tool-detector.js +383 -0
  278. package/lib/utils/validation.js +324 -0
  279. package/lib/value/gate-summary-emitter.js +99 -0
  280. package/lib/value/metric-contract-loader.js +210 -0
  281. package/lib/value/risk-evaluator.js +117 -0
  282. package/lib/value/weekly-snapshot-builder.js +61 -0
  283. package/lib/version/version-checker.js +156 -0
  284. package/lib/version/version-manager.js +327 -0
  285. package/lib/watch/action-executor.js +458 -0
  286. package/lib/watch/event-debouncer.js +323 -0
  287. package/lib/watch/execution-logger.js +550 -0
  288. package/lib/watch/file-watcher.js +499 -0
  289. package/lib/watch/presets.js +266 -0
  290. package/lib/watch/watch-manager.js +533 -0
  291. package/lib/workspace/multi/global-config.js +150 -0
  292. package/lib/workspace/multi/index.js +22 -0
  293. package/lib/workspace/multi/path-utils.js +173 -0
  294. package/lib/workspace/multi/workspace-context-resolver.js +244 -0
  295. package/lib/workspace/multi/workspace-registry.js +196 -0
  296. package/lib/workspace/multi/workspace-state-manager.js +537 -0
  297. package/lib/workspace/multi/workspace.js +90 -0
  298. package/lib/workspace/workspace-manager.js +370 -0
  299. package/lib/workspace/workspace-sync.js +356 -0
  300. package/locales/en.json +114 -0
  301. package/locales/zh.json +114 -0
  302. package/package.json +102 -0
  303. package/template/.kiro/README.md +247 -0
  304. package/template/.kiro/hooks/check-spec-on-create.kiro.hook +17 -0
  305. package/template/.kiro/hooks/run-tests-on-save.kiro.hook +13 -0
  306. package/template/.kiro/hooks/sync-tasks-on-edit.kiro.hook +16 -0
  307. package/template/.kiro/specs/SPEC_WORKFLOW_GUIDE.md +134 -0
  308. package/template/.kiro/steering/CORE_PRINCIPLES.md +133 -0
  309. package/template/.kiro/steering/CURRENT_CONTEXT.md +30 -0
  310. package/template/.kiro/steering/ENVIRONMENT.md +35 -0
  311. package/template/.kiro/steering/RULES_GUIDE.md +46 -0
  312. package/template/.kiro/templates/operations/default/change-impact.md +112 -0
  313. package/template/.kiro/templates/operations/default/deployment.md +91 -0
  314. package/template/.kiro/templates/operations/default/feedback-response.md +269 -0
  315. package/template/.kiro/templates/operations/default/migration-plan.md +172 -0
  316. package/template/.kiro/templates/operations/default/monitoring.md +135 -0
  317. package/template/.kiro/templates/operations/default/operations.md +135 -0
  318. package/template/.kiro/templates/operations/default/rollback.md +143 -0
  319. package/template/.kiro/templates/operations/default/tools.yaml +364 -0
  320. package/template/.kiro/templates/operations/default/troubleshooting.md +123 -0
  321. package/template/.kiro/tools/backup_manager.py +295 -0
  322. package/template/.kiro/tools/configuration_manager.py +218 -0
  323. package/template/.kiro/tools/document_evaluator.py +550 -0
  324. package/template/.kiro/tools/enhancement_logger.py +168 -0
  325. package/template/.kiro/tools/error_handler.py +335 -0
  326. package/template/.kiro/tools/improvement_identifier.py +444 -0
  327. package/template/.kiro/tools/modification_applicator.py +737 -0
  328. package/template/.kiro/tools/quality_gate_enforcer.py +207 -0
  329. package/template/.kiro/tools/quality_scorer.py +305 -0
  330. package/template/.kiro/tools/report_generator.py +154 -0
  331. package/template/.kiro/tools/ultrawork_enhancer.py +676 -0
  332. package/template/.kiro/tools/ultrawork_enhancer_refactored.py +0 -0
  333. package/template/.kiro/tools/ultrawork_enhancer_v2.py +463 -0
  334. package/template/.kiro/tools/ultrawork_enhancer_v3.py +606 -0
  335. package/template/.kiro/tools/workflow_quality_gate.py +100 -0
  336. package/template/README.md +111 -0
@@ -0,0 +1,2029 @@
1
+ 'use strict';
2
+
3
+ const path = require('path');
4
+ const MoquiClient = require('./moqui-client');
5
+ const { loadAdapterConfig, validateAdapterConfig } = require('./moqui-adapter');
6
+
7
+ // ─── Constants ─────────────────────────────────────────────────────
8
+
9
+ const SUPPORTED_PATTERNS = ['crud', 'query', 'workflow'];
10
+
11
+ const HEADER_ITEM_SUFFIXES = [
12
+ { header: 'Header', item: 'Item' },
13
+ { header: 'Header', item: 'Detail' },
14
+ { header: 'Master', item: 'Detail' }
15
+ ];
16
+
17
+ const SCENE_API_VERSION = 'kse.scene/v0.2';
18
+ const PACKAGE_API_VERSION = 'kse.scene.package/v0.1';
19
+
20
+ // ─── YAML Serializer ──────────────────────────────────────────────
21
+
22
+ /**
23
+ * Check if a string value needs quoting in YAML.
24
+ * Quotes are needed for: empty strings, strings containing special chars,
25
+ * strings that look like booleans or numbers, strings with leading/trailing spaces.
26
+ * @param {string} value - String value to check
27
+ * @returns {boolean} true if quoting is needed
28
+ */
29
+ function needsYamlQuoting(value) {
30
+ if (value === '') {
31
+ return true;
32
+ }
33
+
34
+ // Leading or trailing whitespace
35
+ if (value !== value.trim()) {
36
+ return true;
37
+ }
38
+
39
+ // Looks like a boolean
40
+ if (value === 'true' || value === 'false' || value === 'null' ||
41
+ value === 'True' || value === 'False' || value === 'Null' ||
42
+ value === 'TRUE' || value === 'FALSE' || value === 'NULL' ||
43
+ value === 'yes' || value === 'no' || value === 'on' || value === 'off' ||
44
+ value === 'Yes' || value === 'No' || value === 'On' || value === 'Off' ||
45
+ value === 'YES' || value === 'NO' || value === 'ON' || value === 'OFF') {
46
+ return true;
47
+ }
48
+
49
+ // Looks like a number
50
+ if (/^[-+]?(\d+\.?\d*|\.\d+)([eE][-+]?\d+)?$/.test(value)) {
51
+ return true;
52
+ }
53
+
54
+ // Contains special YAML characters that could cause parsing issues
55
+ if (/[:#\[\]{}&*!|>'"%@`]/.test(value)) {
56
+ return true;
57
+ }
58
+
59
+ // Starts with special characters
60
+ if (/^[-?](\s|$)/.test(value)) {
61
+ return true;
62
+ }
63
+
64
+ return false;
65
+ }
66
+
67
+ /**
68
+ * Format a scalar value for YAML output.
69
+ * @param {*} value - Value to format
70
+ * @returns {string} Formatted YAML value
71
+ */
72
+ function formatYamlValue(value) {
73
+ if (value === null || value === undefined) {
74
+ return 'null';
75
+ }
76
+
77
+ if (typeof value === 'boolean') {
78
+ return value ? 'true' : 'false';
79
+ }
80
+
81
+ if (typeof value === 'number') {
82
+ return String(value);
83
+ }
84
+
85
+ const str = String(value);
86
+
87
+ if (needsYamlQuoting(str)) {
88
+ // Use double quotes and escape special characters
89
+ const escaped = str
90
+ .replace(/\\/g, '\\\\')
91
+ .replace(/"/g, '\\"')
92
+ .replace(/\n/g, '\\n')
93
+ .replace(/\r/g, '\\r')
94
+ .replace(/\t/g, '\\t');
95
+ return `"${escaped}"`;
96
+ }
97
+
98
+ return str;
99
+ }
100
+
101
+ /**
102
+ * Serialize a scene manifest object to YAML string.
103
+ * Uses a minimal built-in serializer (no external deps).
104
+ * Handles nested objects, arrays, string/number/boolean values with 2-space indentation.
105
+ * @param {Object} manifest - Scene manifest object
106
+ * @returns {string} YAML string
107
+ */
108
+ function serializeManifestToYaml(manifest) {
109
+ if (manifest === null || manifest === undefined) {
110
+ return 'null\n';
111
+ }
112
+
113
+ if (typeof manifest !== 'object') {
114
+ return formatYamlValue(manifest) + '\n';
115
+ }
116
+
117
+ const lines = [];
118
+ serializeObject(manifest, 0, lines);
119
+ return lines.join('\n') + '\n';
120
+ }
121
+
122
+ /**
123
+ * Serialize an object into YAML lines at the given indentation level.
124
+ * @param {Object} obj - Object to serialize
125
+ * @param {number} indent - Current indentation level (number of spaces)
126
+ * @param {string[]} lines - Output lines array
127
+ */
128
+ function serializeObject(obj, indent, lines) {
129
+ const prefix = ' '.repeat(indent);
130
+ const keys = Object.keys(obj);
131
+
132
+ for (const key of keys) {
133
+ const value = obj[key];
134
+ serializeKeyValue(key, value, indent, prefix, lines);
135
+ }
136
+ }
137
+
138
+ /**
139
+ * Serialize a key-value pair into YAML lines.
140
+ * @param {string} key - Object key
141
+ * @param {*} value - Value to serialize
142
+ * @param {number} indent - Current indentation level
143
+ * @param {string} prefix - Indentation string
144
+ * @param {string[]} lines - Output lines array
145
+ */
146
+ function serializeKeyValue(key, value, indent, prefix, lines) {
147
+ if (value === null || value === undefined) {
148
+ lines.push(`${prefix}${key}: null`);
149
+ return;
150
+ }
151
+
152
+ if (typeof value !== 'object') {
153
+ // Scalar value
154
+ lines.push(`${prefix}${key}: ${formatYamlValue(value)}`);
155
+ return;
156
+ }
157
+
158
+ if (Array.isArray(value)) {
159
+ if (value.length === 0) {
160
+ lines.push(`${prefix}${key}: []`);
161
+ return;
162
+ }
163
+
164
+ lines.push(`${prefix}${key}:`);
165
+ serializeArray(value, indent + 2, lines);
166
+ return;
167
+ }
168
+
169
+ // Nested object
170
+ if (Object.keys(value).length === 0) {
171
+ lines.push(`${prefix}${key}: {}`);
172
+ return;
173
+ }
174
+
175
+ lines.push(`${prefix}${key}:`);
176
+ serializeObject(value, indent + 2, lines);
177
+ }
178
+
179
+ /**
180
+ * Serialize an array into YAML lines.
181
+ * @param {Array} arr - Array to serialize
182
+ * @param {number} indent - Current indentation level
183
+ * @param {string[]} lines - Output lines array
184
+ */
185
+ function serializeArray(arr, indent, lines) {
186
+ const prefix = ' '.repeat(indent);
187
+
188
+ for (const item of arr) {
189
+ if (item === null || item === undefined) {
190
+ lines.push(`${prefix}- null`);
191
+ continue;
192
+ }
193
+
194
+ if (typeof item !== 'object') {
195
+ // Scalar array item
196
+ lines.push(`${prefix}- ${formatYamlValue(item)}`);
197
+ continue;
198
+ }
199
+
200
+ if (Array.isArray(item)) {
201
+ // Nested array — serialize as block under "- "
202
+ lines.push(`${prefix}-`);
203
+ serializeArray(item, indent + 2, lines);
204
+ continue;
205
+ }
206
+
207
+ // Object array item — first key on same line as "- ", rest indented
208
+ const keys = Object.keys(item);
209
+
210
+ if (keys.length === 0) {
211
+ lines.push(`${prefix}- {}`);
212
+ continue;
213
+ }
214
+
215
+ const firstKey = keys[0];
216
+ const firstValue = item[firstKey];
217
+
218
+ if (firstValue !== null && firstValue !== undefined && typeof firstValue === 'object') {
219
+ // First value is complex — put key on "- " line, value below
220
+ if (Array.isArray(firstValue)) {
221
+ if (firstValue.length === 0) {
222
+ lines.push(`${prefix}- ${firstKey}: []`);
223
+ } else {
224
+ lines.push(`${prefix}- ${firstKey}:`);
225
+ serializeArray(firstValue, indent + 4, lines);
226
+ }
227
+ } else {
228
+ if (Object.keys(firstValue).length === 0) {
229
+ lines.push(`${prefix}- ${firstKey}: {}`);
230
+ } else {
231
+ lines.push(`${prefix}- ${firstKey}:`);
232
+ serializeObject(firstValue, indent + 4, lines);
233
+ }
234
+ }
235
+ } else {
236
+ // First value is scalar — put on same line as "- "
237
+ lines.push(`${prefix}- ${firstKey}: ${formatYamlValue(firstValue)}`);
238
+ }
239
+
240
+ // Remaining keys indented to align with first key
241
+ for (let i = 1; i < keys.length; i++) {
242
+ const k = keys[i];
243
+ const v = item[k];
244
+ serializeKeyValue(k, v, indent + 2, ' '.repeat(indent + 2), lines);
245
+ }
246
+ }
247
+ }
248
+
249
+ // ─── YAML Parser ──────────────────────────────────────────────────
250
+
251
+ /**
252
+ * Parse a YAML string back into an object.
253
+ * Handles the subset of YAML used by scene manifests:
254
+ * indentation-based nesting, "- " array items, key: value pairs,
255
+ * boolean (true/false), number, and string values.
256
+ * @param {string} yamlString - YAML content
257
+ * @returns {Object} Parsed object
258
+ */
259
+ function parseYaml(yamlString) {
260
+ if (!yamlString || typeof yamlString !== 'string') {
261
+ return {};
262
+ }
263
+
264
+ const trimmed = yamlString.trim();
265
+
266
+ if (!trimmed) {
267
+ return {};
268
+ }
269
+
270
+ if (trimmed === 'null') {
271
+ return null;
272
+ }
273
+
274
+ // Split into lines, preserving empty lines for structure
275
+ const rawLines = trimmed.split('\n');
276
+
277
+ // Filter out empty lines and comment lines, but keep track of indentation
278
+ const lines = [];
279
+
280
+ for (const raw of rawLines) {
281
+ // Skip empty lines and comment-only lines
282
+ if (raw.trim() === '' || raw.trim().startsWith('#')) {
283
+ continue;
284
+ }
285
+
286
+ lines.push(raw);
287
+ }
288
+
289
+ if (lines.length === 0) {
290
+ return {};
291
+ }
292
+
293
+ // Check if the first line is a scalar value (no colon, no dash)
294
+ const firstLine = lines[0].trim();
295
+
296
+ if (lines.length === 1 && !firstLine.includes(':') && !firstLine.startsWith('-')) {
297
+ return parseScalarValue(firstLine);
298
+ }
299
+
300
+ const result = parseBlock(lines, 0, lines.length, 0);
301
+
302
+ return result.value;
303
+ }
304
+
305
+ /**
306
+ * Parse a block of YAML lines into a value (object or array).
307
+ * @param {string[]} lines - All lines
308
+ * @param {number} start - Start index (inclusive)
309
+ * @param {number} end - End index (exclusive)
310
+ * @param {number} expectedIndent - Expected indentation level
311
+ * @returns {{ value: Object|Array }}
312
+ */
313
+ function parseBlock(lines, start, end, expectedIndent) {
314
+ if (start >= end) {
315
+ return { value: {} };
316
+ }
317
+
318
+ const firstLine = lines[start];
319
+ const firstContent = firstLine.trimStart();
320
+
321
+ // Determine if this block is an array or object
322
+ if (firstContent.startsWith('- ') || firstContent === '-') {
323
+ return { value: parseArrayBlock(lines, start, end, expectedIndent) };
324
+ }
325
+
326
+ return { value: parseObjectBlock(lines, start, end, expectedIndent) };
327
+ }
328
+
329
+ /**
330
+ * Get the indentation level of a line (number of leading spaces).
331
+ * @param {string} line - Input line
332
+ * @returns {number} Number of leading spaces
333
+ */
334
+ function getIndent(line) {
335
+ let count = 0;
336
+
337
+ for (let i = 0; i < line.length; i++) {
338
+ if (line[i] === ' ') {
339
+ count++;
340
+ } else {
341
+ break;
342
+ }
343
+ }
344
+
345
+ return count;
346
+ }
347
+
348
+ /**
349
+ * Parse a block of lines as an object.
350
+ * @param {string[]} lines - All lines
351
+ * @param {number} start - Start index
352
+ * @param {number} end - End index
353
+ * @param {number} baseIndent - Base indentation level
354
+ * @returns {Object}
355
+ */
356
+ function parseObjectBlock(lines, start, end, baseIndent) {
357
+ const result = {};
358
+ let i = start;
359
+
360
+ while (i < end) {
361
+ const line = lines[i];
362
+ const indent = getIndent(line);
363
+
364
+ // Skip lines with deeper indentation (they belong to a previous key)
365
+ if (indent < baseIndent) {
366
+ break;
367
+ }
368
+
369
+ const content = line.trimStart();
370
+
371
+ // Skip if this is an array item at this level
372
+ if (content.startsWith('- ') || content === '-') {
373
+ i++;
374
+ continue;
375
+ }
376
+
377
+ const colonIdx = content.indexOf(':');
378
+
379
+ if (colonIdx === -1) {
380
+ i++;
381
+ continue;
382
+ }
383
+
384
+ const key = content.substring(0, colonIdx).trim();
385
+ const afterColon = content.substring(colonIdx + 1).trim();
386
+
387
+ if (afterColon === '' || afterColon === '') {
388
+ // Value is on subsequent lines (nested block)
389
+ const childIndent = findChildIndent(lines, i + 1, end);
390
+
391
+ if (childIndent > indent) {
392
+ const childEnd = findBlockEnd(lines, i + 1, end, childIndent);
393
+ const child = parseBlock(lines, i + 1, childEnd, childIndent);
394
+ result[key] = child.value;
395
+ i = childEnd;
396
+ } else {
397
+ // Empty value — treat as null
398
+ result[key] = null;
399
+ i++;
400
+ }
401
+ } else if (afterColon === '[]') {
402
+ result[key] = [];
403
+ i++;
404
+ } else if (afterColon === '{}') {
405
+ result[key] = {};
406
+ i++;
407
+ } else {
408
+ // Inline scalar value
409
+ result[key] = parseScalarValue(afterColon);
410
+ i++;
411
+ }
412
+ }
413
+
414
+ return result;
415
+ }
416
+
417
+ /**
418
+ * Parse a block of lines as an array.
419
+ * @param {string[]} lines - All lines
420
+ * @param {number} start - Start index
421
+ * @param {number} end - End index
422
+ * @param {number} baseIndent - Base indentation level
423
+ * @returns {Array}
424
+ */
425
+ function parseArrayBlock(lines, start, end, baseIndent) {
426
+ const result = [];
427
+ let i = start;
428
+
429
+ while (i < end) {
430
+ const line = lines[i];
431
+ const indent = getIndent(line);
432
+
433
+ if (indent < baseIndent) {
434
+ break;
435
+ }
436
+
437
+ const content = line.trimStart();
438
+
439
+ if (!content.startsWith('- ') && content !== '-') {
440
+ i++;
441
+ continue;
442
+ }
443
+
444
+ if (content === '-') {
445
+ // Bare dash — value is on subsequent lines
446
+ const childIndent = findChildIndent(lines, i + 1, end);
447
+
448
+ if (childIndent > indent) {
449
+ const childEnd = findBlockEnd(lines, i + 1, end, childIndent);
450
+ const child = parseBlock(lines, i + 1, childEnd, childIndent);
451
+ result.push(child.value);
452
+ i = childEnd;
453
+ } else {
454
+ result.push(null);
455
+ i++;
456
+ }
457
+
458
+ continue;
459
+ }
460
+
461
+ // "- " prefix — extract the content after "- "
462
+ const itemContent = content.substring(2);
463
+
464
+ if (itemContent === '[]') {
465
+ result.push([]);
466
+ i++;
467
+ continue;
468
+ }
469
+
470
+ if (itemContent === '{}') {
471
+ result.push({});
472
+ i++;
473
+ continue;
474
+ }
475
+
476
+ // Check if item content has a colon (it's an object entry)
477
+ const colonIdx = itemContent.indexOf(':');
478
+
479
+ if (colonIdx !== -1) {
480
+ // This is an object item in the array
481
+ // Parse the first key-value, then look for more keys at indent+2
482
+ const firstKey = itemContent.substring(0, colonIdx).trim();
483
+ const afterColon = itemContent.substring(colonIdx + 1).trim();
484
+
485
+ const obj = {};
486
+
487
+ if (afterColon === '') {
488
+ // Value is on subsequent lines
489
+ const valueIndent = findChildIndent(lines, i + 1, end);
490
+
491
+ if (valueIndent > indent + 2) {
492
+ const valueEnd = findBlockEnd(lines, i + 1, end, valueIndent);
493
+ const child = parseBlock(lines, i + 1, valueEnd, valueIndent);
494
+ obj[firstKey] = child.value;
495
+ i = valueEnd;
496
+ } else if (valueIndent === indent + 2) {
497
+ // Could be the value block or sibling keys
498
+ // Check if next line is a key at indent+2 or deeper content
499
+ const nextContent = lines[i + 1] ? lines[i + 1].trimStart() : '';
500
+ const nextIndent = lines[i + 1] ? getIndent(lines[i + 1]) : 0;
501
+
502
+ if (nextIndent > indent + 2) {
503
+ // Deeper content — it's the value
504
+ const valueEnd = findBlockEnd(lines, i + 1, end, nextIndent);
505
+ const child = parseBlock(lines, i + 1, valueEnd, nextIndent);
506
+ obj[firstKey] = child.value;
507
+ i = valueEnd;
508
+ } else {
509
+ // Same level as sibling keys — value is null, parse siblings
510
+ obj[firstKey] = null;
511
+ i++;
512
+ }
513
+ } else {
514
+ obj[firstKey] = null;
515
+ i++;
516
+ }
517
+ } else if (afterColon === '[]') {
518
+ obj[firstKey] = [];
519
+ i++;
520
+ } else if (afterColon === '{}') {
521
+ obj[firstKey] = {};
522
+ i++;
523
+ } else {
524
+ obj[firstKey] = parseScalarValue(afterColon);
525
+ i++;
526
+ }
527
+
528
+ // Parse remaining keys at indent+2
529
+ const siblingIndent = indent + 2;
530
+
531
+ while (i < end) {
532
+ const sibLine = lines[i];
533
+ const sibIndent = getIndent(sibLine);
534
+
535
+ if (sibIndent < siblingIndent) {
536
+ break;
537
+ }
538
+
539
+ if (sibIndent > siblingIndent) {
540
+ // This belongs to a previous key's value — skip
541
+ i++;
542
+ continue;
543
+ }
544
+
545
+ const sibContent = sibLine.trimStart();
546
+
547
+ // If it's a new array item at the base indent, stop
548
+ if (sibContent.startsWith('- ') && sibIndent === baseIndent) {
549
+ break;
550
+ }
551
+
552
+ const sibColonIdx = sibContent.indexOf(':');
553
+
554
+ if (sibColonIdx === -1) {
555
+ i++;
556
+ continue;
557
+ }
558
+
559
+ const sibKey = sibContent.substring(0, sibColonIdx).trim();
560
+ const sibAfterColon = sibContent.substring(sibColonIdx + 1).trim();
561
+
562
+ if (sibAfterColon === '') {
563
+ const childIndent = findChildIndent(lines, i + 1, end);
564
+
565
+ if (childIndent > siblingIndent) {
566
+ const childEnd = findBlockEnd(lines, i + 1, end, childIndent);
567
+ const child = parseBlock(lines, i + 1, childEnd, childIndent);
568
+ obj[sibKey] = child.value;
569
+ i = childEnd;
570
+ } else {
571
+ obj[sibKey] = null;
572
+ i++;
573
+ }
574
+ } else if (sibAfterColon === '[]') {
575
+ obj[sibKey] = [];
576
+ i++;
577
+ } else if (sibAfterColon === '{}') {
578
+ obj[sibKey] = {};
579
+ i++;
580
+ } else {
581
+ obj[sibKey] = parseScalarValue(sibAfterColon);
582
+ i++;
583
+ }
584
+ }
585
+
586
+ result.push(obj);
587
+ } else {
588
+ // Scalar array item
589
+ result.push(parseScalarValue(itemContent));
590
+ i++;
591
+ }
592
+ }
593
+
594
+ return result;
595
+ }
596
+
597
+ /**
598
+ * Find the indentation level of the first non-empty child line.
599
+ * @param {string[]} lines - All lines
600
+ * @param {number} start - Start index
601
+ * @param {number} end - End index
602
+ * @returns {number} Child indentation level, or -1 if no child found
603
+ */
604
+ function findChildIndent(lines, start, end) {
605
+ for (let i = start; i < end; i++) {
606
+ const line = lines[i];
607
+ const trimmed = line.trim();
608
+
609
+ if (trimmed === '' || trimmed.startsWith('#')) {
610
+ continue;
611
+ }
612
+
613
+ return getIndent(line);
614
+ }
615
+
616
+ return -1;
617
+ }
618
+
619
+ /**
620
+ * Find the end index of a block at the given indentation level.
621
+ * @param {string[]} lines - All lines
622
+ * @param {number} start - Start index
623
+ * @param {number} end - End index
624
+ * @param {number} blockIndent - Block indentation level
625
+ * @returns {number} End index (exclusive)
626
+ */
627
+ function findBlockEnd(lines, start, end, blockIndent) {
628
+ for (let i = start; i < end; i++) {
629
+ const line = lines[i];
630
+ const trimmed = line.trim();
631
+
632
+ if (trimmed === '' || trimmed.startsWith('#')) {
633
+ continue;
634
+ }
635
+
636
+ const indent = getIndent(line);
637
+
638
+ if (indent < blockIndent) {
639
+ return i;
640
+ }
641
+ }
642
+
643
+ return end;
644
+ }
645
+
646
+ /**
647
+ * Parse a scalar YAML value string into a JavaScript value.
648
+ * Handles: booleans, numbers, null, quoted strings, unquoted strings.
649
+ * @param {string} value - Raw value string
650
+ * @returns {*} Parsed value
651
+ */
652
+ function parseScalarValue(value) {
653
+ if (value === 'null' || value === 'Null' || value === 'NULL' || value === '~') {
654
+ return null;
655
+ }
656
+
657
+ if (value === 'true' || value === 'True' || value === 'TRUE') {
658
+ return true;
659
+ }
660
+
661
+ if (value === 'false' || value === 'False' || value === 'FALSE') {
662
+ return false;
663
+ }
664
+
665
+ // Quoted string (double quotes)
666
+ if (value.startsWith('"') && value.endsWith('"') && value.length >= 2) {
667
+ const inner = value.slice(1, -1);
668
+ return inner
669
+ .replace(/\\n/g, '\n')
670
+ .replace(/\\r/g, '\r')
671
+ .replace(/\\t/g, '\t')
672
+ .replace(/\\"/g, '"')
673
+ .replace(/\\\\/g, '\\');
674
+ }
675
+
676
+ // Quoted string (single quotes)
677
+ if (value.startsWith("'") && value.endsWith("'") && value.length >= 2) {
678
+ return value.slice(1, -1).replace(/''/g, "'");
679
+ }
680
+
681
+ // Number
682
+ if (/^[-+]?(\d+\.?\d*|\.\d+)([eE][-+]?\d+)?$/.test(value)) {
683
+ const num = Number(value);
684
+
685
+ if (!isNaN(num) && isFinite(num)) {
686
+ return num;
687
+ }
688
+ }
689
+
690
+ return value;
691
+ }
692
+
693
+ // ─── PascalCase to kebab-case ──────────────────────────────────────
694
+
695
+ /**
696
+ * Convert a PascalCase or camelCase string to kebab-case.
697
+ * E.g., "OrderHeader" → "order-header", "Order" → "order",
698
+ * "HTMLParser" → "html-parser", "myValue" → "my-value"
699
+ * @param {string} str - PascalCase/camelCase string
700
+ * @returns {string} kebab-case string
701
+ */
702
+ function toKebabCase(str) {
703
+ if (!str || typeof str !== 'string') {
704
+ return '';
705
+ }
706
+
707
+ // Insert hyphen before uppercase letters that follow lowercase letters or
708
+ // before an uppercase letter followed by a lowercase letter in a run of uppercase
709
+ return str
710
+ .replace(/([a-z0-9])([A-Z])/g, '$1-$2')
711
+ .replace(/([A-Z]+)([A-Z][a-z])/g, '$1-$2')
712
+ .toLowerCase();
713
+ }
714
+
715
+ // ─── Entity Grouping ──────────────────────────────────────────────
716
+
717
+ /**
718
+ * Group related entities by header/item suffix patterns.
719
+ * E.g., OrderHeader + OrderItem → { base: 'Order', entities: ['OrderHeader', 'OrderItem'], isComposite: true }
720
+ * Entities without any suffix match are placed in their own group with isComposite: false.
721
+ * Every input entity appears in exactly one group.
722
+ * @param {string[]} entityNames - List of entity names
723
+ * @returns {EntityGroup[]}
724
+ */
725
+ function groupRelatedEntities(entityNames) {
726
+ if (!Array.isArray(entityNames) || entityNames.length === 0) {
727
+ return [];
728
+ }
729
+
730
+ // Track which entities have been assigned to a group
731
+ const assigned = new Set();
732
+ const groups = [];
733
+
734
+ // First pass: find header/item pairs using HEADER_ITEM_SUFFIXES
735
+ for (const suffixPair of HEADER_ITEM_SUFFIXES) {
736
+ // Find all entities ending with the header suffix
737
+ for (const entity of entityNames) {
738
+ if (assigned.has(entity)) {
739
+ continue;
740
+ }
741
+
742
+ if (!entity.endsWith(suffixPair.header)) {
743
+ continue;
744
+ }
745
+
746
+ const base = entity.substring(0, entity.length - suffixPair.header.length);
747
+
748
+ if (!base) {
749
+ continue; // Skip if the entity name IS the suffix (e.g., "Header")
750
+ }
751
+
752
+ // Look for matching item entity
753
+ const itemEntity = base + suffixPair.item;
754
+
755
+ if (entityNames.includes(itemEntity) && !assigned.has(itemEntity)) {
756
+ // Found a header/item pair
757
+ const groupEntities = [entity, itemEntity];
758
+ assigned.add(entity);
759
+ assigned.add(itemEntity);
760
+
761
+ groups.push({
762
+ base,
763
+ entities: groupEntities,
764
+ isComposite: true
765
+ });
766
+ }
767
+ }
768
+ }
769
+
770
+ // Second pass: assign remaining entities to their own groups
771
+ for (const entity of entityNames) {
772
+ if (assigned.has(entity)) {
773
+ continue;
774
+ }
775
+
776
+ assigned.add(entity);
777
+ groups.push({
778
+ base: entity,
779
+ entities: [entity],
780
+ isComposite: false
781
+ });
782
+ }
783
+
784
+ return groups;
785
+ }
786
+
787
+ // ─── Bundle and Package Name Derivation ───────────────────────────
788
+
789
+ /**
790
+ * Derive a bundle directory name from pattern and resource.
791
+ * Returns kebab-case string like "crud-order".
792
+ * @param {PatternMatch} match - Pattern match
793
+ * @returns {string} Directory name in kebab-case
794
+ */
795
+ function deriveBundleDirName(match) {
796
+ if (!match || !match.pattern || !match.primaryResource) {
797
+ return '';
798
+ }
799
+
800
+ const pattern = match.pattern.toLowerCase();
801
+ const resource = toKebabCase(match.primaryResource);
802
+
803
+ return `${pattern}-${resource}`;
804
+ }
805
+
806
+ /**
807
+ * Derive a package name from pattern and resource.
808
+ * Returns kebab-case string like "crud-order".
809
+ * @param {PatternMatch} match - Pattern match
810
+ * @returns {string} Package name in kebab-case
811
+ */
812
+ function derivePackageName(match) {
813
+ if (!match || !match.pattern || !match.primaryResource) {
814
+ return '';
815
+ }
816
+
817
+ const pattern = match.pattern.toLowerCase();
818
+ const resource = toKebabCase(match.primaryResource);
819
+
820
+ return `${pattern}-${resource}`;
821
+ }
822
+
823
+ // ─── Pattern Matching ─────────────────────────────────────────────
824
+
825
+ /**
826
+ * Derive the idempotency key from an entity name.
827
+ * Converts the entity name to a camelCase ID field.
828
+ * E.g., "OrderHeader" → "orderId", "Product" → "productId"
829
+ * @param {string} entityName - Entity name (PascalCase)
830
+ * @returns {string} Idempotency key
831
+ */
832
+ function deriveIdempotencyKey(entityName) {
833
+ if (!entityName || typeof entityName !== 'string') {
834
+ return '';
835
+ }
836
+
837
+ // Use the base name (strip common suffixes like Header, Item, Detail, Master)
838
+ let base = entityName;
839
+
840
+ for (const suffixPair of HEADER_ITEM_SUFFIXES) {
841
+ if (base.endsWith(suffixPair.header)) {
842
+ base = base.substring(0, base.length - suffixPair.header.length);
843
+ break;
844
+ }
845
+
846
+ if (base.endsWith(suffixPair.item)) {
847
+ base = base.substring(0, base.length - suffixPair.item.length);
848
+ break;
849
+ }
850
+ }
851
+
852
+ // If stripping left nothing, use original
853
+ if (!base) {
854
+ base = entityName;
855
+ }
856
+
857
+ // Convert to camelCase + "Id"
858
+ const camel = base.charAt(0).toLowerCase() + base.slice(1);
859
+
860
+ return camel + 'Id';
861
+ }
862
+
863
+ /**
864
+ * Generate model scope entries for an entity.
865
+ * Read scope includes entityId and statusId fields.
866
+ * Write scope includes statusId field (for crud patterns).
867
+ * @param {string} primaryEntity - Primary entity name
868
+ * @param {string} pattern - Pattern type ('crud' | 'query')
869
+ * @returns {{ read: string[], write: string[] }}
870
+ */
871
+ function generateEntityModelScope(primaryEntity, pattern) {
872
+ const idKey = deriveIdempotencyKey(primaryEntity);
873
+ const read = [
874
+ `moqui.${primaryEntity}.${idKey}`,
875
+ `moqui.${primaryEntity}.statusId`
876
+ ];
877
+
878
+ const write = pattern === 'crud'
879
+ ? [`moqui.${primaryEntity}.statusId`]
880
+ : [];
881
+
882
+ return { read, write };
883
+ }
884
+
885
+ /**
886
+ * Match an entity group against pattern rules.
887
+ * If the entity group has related services (services containing the entity base name),
888
+ * classify as "crud". Otherwise, classify as "query" (read-only).
889
+ *
890
+ * @param {EntityGroup} group - Grouped entity info { base, entities, isComposite }
891
+ * @param {string[]} services - Available service names
892
+ * @returns {PatternMatch|null}
893
+ */
894
+ function matchEntityPattern(group, services) {
895
+ if (!group || !group.base || !Array.isArray(group.entities) || group.entities.length === 0) {
896
+ return null;
897
+ }
898
+
899
+ services = Array.isArray(services) ? services : [];
900
+
901
+ // Determine the primary entity (first entity in the group, typically the header entity)
902
+ const primaryEntity = group.entities[0];
903
+
904
+ // Check if any service name contains the base entity name (case-insensitive)
905
+ const baseLower = group.base.toLowerCase();
906
+ const hasRelatedServices = services.some(svc => {
907
+ if (!svc || typeof svc !== 'string') {
908
+ return false;
909
+ }
910
+
911
+ return svc.toLowerCase().includes(baseLower);
912
+ });
913
+
914
+ if (hasRelatedServices) {
915
+ // CRUD pattern: entity with related services → all 5 operations
916
+ const bindingRefs = [
917
+ `moqui.${primaryEntity}.list`,
918
+ `moqui.${primaryEntity}.get`,
919
+ `moqui.${primaryEntity}.create`,
920
+ `moqui.${primaryEntity}.update`,
921
+ `moqui.${primaryEntity}.delete`
922
+ ];
923
+
924
+ const modelScope = generateEntityModelScope(primaryEntity, 'crud');
925
+ const idempotencyKey = deriveIdempotencyKey(primaryEntity);
926
+
927
+ return {
928
+ pattern: 'crud',
929
+ primaryResource: group.base,
930
+ entities: [...group.entities],
931
+ services: [],
932
+ bindingRefs,
933
+ modelScope,
934
+ governance: {
935
+ riskLevel: 'medium',
936
+ approvalRequired: true,
937
+ idempotencyRequired: true,
938
+ idempotencyKey
939
+ }
940
+ };
941
+ }
942
+
943
+ // Query pattern: entity without related services → read-only (list + get)
944
+ const bindingRefs = [
945
+ `moqui.${primaryEntity}.list`,
946
+ `moqui.${primaryEntity}.get`
947
+ ];
948
+
949
+ const modelScope = generateEntityModelScope(primaryEntity, 'query');
950
+
951
+ return {
952
+ pattern: 'query',
953
+ primaryResource: group.base,
954
+ entities: [...group.entities],
955
+ services: [],
956
+ bindingRefs,
957
+ modelScope,
958
+ governance: {
959
+ riskLevel: 'low',
960
+ approvalRequired: false,
961
+ idempotencyRequired: false
962
+ }
963
+ };
964
+ }
965
+
966
+ /**
967
+ * Match services against workflow pattern rules.
968
+ * Services that don't directly map to entity CRUD operations are workflow candidates.
969
+ * Groups related services into workflow patterns.
970
+ *
971
+ * @param {string[]} services - Service names
972
+ * @param {string[]} entities - Entity names
973
+ * @returns {PatternMatch[]}
974
+ */
975
+ function matchWorkflowPatterns(services, entities) {
976
+ if (!Array.isArray(services) || services.length === 0) {
977
+ return [];
978
+ }
979
+
980
+ entities = Array.isArray(entities) ? entities : [];
981
+
982
+ // Build a set of entity base names (lowercase) for matching
983
+ const entityBaseNames = new Set();
984
+
985
+ for (const entity of entities) {
986
+ if (!entity || typeof entity !== 'string') {
987
+ continue;
988
+ }
989
+
990
+ entityBaseNames.add(entity.toLowerCase());
991
+
992
+ // Also add stripped base names (without Header/Item/Detail/Master suffixes)
993
+ for (const suffixPair of HEADER_ITEM_SUFFIXES) {
994
+ if (entity.endsWith(suffixPair.header)) {
995
+ const base = entity.substring(0, entity.length - suffixPair.header.length);
996
+
997
+ if (base) {
998
+ entityBaseNames.add(base.toLowerCase());
999
+ }
1000
+ }
1001
+
1002
+ if (entity.endsWith(suffixPair.item)) {
1003
+ const base = entity.substring(0, entity.length - suffixPair.item.length);
1004
+
1005
+ if (base) {
1006
+ entityBaseNames.add(base.toLowerCase());
1007
+ }
1008
+ }
1009
+ }
1010
+ }
1011
+
1012
+ // Filter services that are NOT direct entity CRUD operations
1013
+ // A service is a CRUD operation if its name matches an entity base name
1014
+ const workflowServices = services.filter(svc => {
1015
+ if (!svc || typeof svc !== 'string') {
1016
+ return false;
1017
+ }
1018
+
1019
+ const svcLower = svc.toLowerCase();
1020
+
1021
+ // Check if the service name directly matches any entity base name
1022
+ for (const baseName of entityBaseNames) {
1023
+ if (svcLower === baseName || svcLower.includes(baseName)) {
1024
+ return false;
1025
+ }
1026
+ }
1027
+
1028
+ return true;
1029
+ });
1030
+
1031
+ if (workflowServices.length === 0) {
1032
+ return [];
1033
+ }
1034
+
1035
+ // Find entities referenced by workflow services
1036
+ // (entities whose base name appears in any workflow service name)
1037
+ const referencedEntities = entities.filter(entity => {
1038
+ if (!entity || typeof entity !== 'string') {
1039
+ return false;
1040
+ }
1041
+
1042
+ const entityLower = entity.toLowerCase();
1043
+
1044
+ return workflowServices.some(svc => svc.toLowerCase().includes(entityLower));
1045
+ });
1046
+
1047
+ // Generate binding refs for each workflow service
1048
+ const bindingRefs = workflowServices.map(svc => `moqui.service.${svc}.invoke`);
1049
+
1050
+ // Generate model scope from referenced entities
1051
+ const read = [];
1052
+ const write = [];
1053
+
1054
+ for (const entity of referencedEntities) {
1055
+ const idKey = deriveIdempotencyKey(entity);
1056
+ read.push(`moqui.${entity}.${idKey}`);
1057
+ read.push(`moqui.${entity}.statusId`);
1058
+ write.push(`moqui.${entity}.statusId`);
1059
+ }
1060
+
1061
+ // Create a single workflow pattern match for all workflow services
1062
+ const primaryResource = workflowServices[0];
1063
+
1064
+ return [{
1065
+ pattern: 'workflow',
1066
+ primaryResource,
1067
+ entities: referencedEntities.length > 0 ? [...referencedEntities] : [],
1068
+ services: [...workflowServices],
1069
+ bindingRefs,
1070
+ modelScope: { read, write },
1071
+ governance: {
1072
+ riskLevel: 'medium',
1073
+ approvalRequired: true,
1074
+ idempotencyRequired: true
1075
+ }
1076
+ }];
1077
+ }
1078
+
1079
+ // ─── Resource Analysis (Orchestrator) ─────────────────────────
1080
+
1081
+ /**
1082
+ * Analyze discovered resources and identify business patterns.
1083
+ * Orchestrates pattern matching across all discovered resources,
1084
+ * applies optional --pattern filter, and handles the empty-match case.
1085
+ *
1086
+ * @param {DiscoveryPayload} discovery - Discovered resources { entities, services, screens }
1087
+ * @param {Object} options - { pattern?: string } — optional pattern filter ('crud' | 'query' | 'workflow')
1088
+ * @returns {PatternMatch[]}
1089
+ */
1090
+ function analyzeResources(discovery, options = {}) {
1091
+ // Edge case: null/undefined/empty discovery
1092
+ if (!discovery) {
1093
+ return [];
1094
+ }
1095
+
1096
+ const entities = Array.isArray(discovery.entities) ? discovery.entities : [];
1097
+ const services = Array.isArray(discovery.services) ? discovery.services : [];
1098
+
1099
+ // If no entities and no services, nothing to analyze
1100
+ if (entities.length === 0 && services.length === 0) {
1101
+ return [];
1102
+ }
1103
+
1104
+ const results = [];
1105
+
1106
+ // Step 1: Group related entities
1107
+ const groups = groupRelatedEntities(entities);
1108
+
1109
+ // Step 2: For each entity group, match against entity patterns (crud/query)
1110
+ for (const group of groups) {
1111
+ const match = matchEntityPattern(group, services);
1112
+
1113
+ if (match) {
1114
+ results.push(match);
1115
+ }
1116
+ }
1117
+
1118
+ // Step 3: Detect workflow patterns from services
1119
+ const workflowMatches = matchWorkflowPatterns(services, entities);
1120
+
1121
+ for (const wm of workflowMatches) {
1122
+ results.push(wm);
1123
+ }
1124
+
1125
+ // Step 4: Apply --pattern filter if provided
1126
+ if (options.pattern) {
1127
+ const filtered = results.filter(m => m.pattern === options.pattern);
1128
+ return filtered;
1129
+ }
1130
+
1131
+ return results;
1132
+ }
1133
+
1134
+ function buildBaseBindings(match) {
1135
+ const pattern = match.pattern;
1136
+ const bindings = [];
1137
+
1138
+ if (pattern === 'crud' || pattern === 'query') {
1139
+ const primaryEntity = Array.isArray(match.entities) && match.entities.length > 0
1140
+ ? match.entities[0]
1141
+ : match.primaryResource;
1142
+
1143
+ bindings.push({
1144
+ type: 'query',
1145
+ ref: `moqui.${primaryEntity}.list`,
1146
+ timeout_ms: 2000,
1147
+ retry: 0
1148
+ });
1149
+ bindings.push({
1150
+ type: 'query',
1151
+ ref: `moqui.${primaryEntity}.get`,
1152
+ timeout_ms: 2000,
1153
+ retry: 0
1154
+ });
1155
+
1156
+ if (pattern === 'crud') {
1157
+ bindings.push({
1158
+ type: 'mutation',
1159
+ ref: `moqui.${primaryEntity}.create`,
1160
+ side_effect: true,
1161
+ timeout_ms: 3000,
1162
+ retry: 0
1163
+ });
1164
+ bindings.push({
1165
+ type: 'mutation',
1166
+ ref: `moqui.${primaryEntity}.update`,
1167
+ side_effect: true,
1168
+ timeout_ms: 3000,
1169
+ retry: 0
1170
+ });
1171
+ bindings.push({
1172
+ type: 'mutation',
1173
+ ref: `moqui.${primaryEntity}.delete`,
1174
+ side_effect: true,
1175
+ timeout_ms: 3000,
1176
+ retry: 0
1177
+ });
1178
+ }
1179
+ } else if (pattern === 'workflow') {
1180
+ const refs = Array.isArray(match.bindingRefs) ? match.bindingRefs : [];
1181
+ for (const ref of refs) {
1182
+ bindings.push({
1183
+ type: 'invoke',
1184
+ ref,
1185
+ timeout_ms: 3000,
1186
+ retry: 0
1187
+ });
1188
+ }
1189
+ }
1190
+
1191
+ return bindings;
1192
+ }
1193
+
1194
+ function deriveBindingIntent(binding, primaryResource) {
1195
+ const ref = String(binding.ref || '');
1196
+
1197
+ if (ref.endsWith('.list')) {
1198
+ return `List ${primaryResource} records from Moqui`;
1199
+ }
1200
+
1201
+ if (ref.endsWith('.get')) {
1202
+ return `Retrieve a single ${primaryResource} record`;
1203
+ }
1204
+
1205
+ if (ref.endsWith('.create')) {
1206
+ return `Create a new ${primaryResource} record`;
1207
+ }
1208
+
1209
+ if (ref.endsWith('.update')) {
1210
+ return `Update an existing ${primaryResource} record`;
1211
+ }
1212
+
1213
+ if (ref.endsWith('.delete')) {
1214
+ return `Delete an existing ${primaryResource} record`;
1215
+ }
1216
+
1217
+ if (ref.endsWith('.invoke')) {
1218
+ return `Invoke workflow service for ${primaryResource}`;
1219
+ }
1220
+
1221
+ return `Execute ${ref}`;
1222
+ }
1223
+
1224
+ function deriveBindingPreconditions(binding, previousRef) {
1225
+ const checks = ['Moqui adapter authentication is valid'];
1226
+
1227
+ if (binding.type === 'query') {
1228
+ checks.push('Read scope permits this query');
1229
+ } else if (binding.type === 'mutation') {
1230
+ checks.push('Input payload validation passed');
1231
+ } else if (binding.type === 'invoke') {
1232
+ checks.push('Workflow input contract is satisfied');
1233
+ }
1234
+
1235
+ if (previousRef) {
1236
+ checks.push(`Dependency ${previousRef} completed successfully`);
1237
+ }
1238
+
1239
+ return checks;
1240
+ }
1241
+
1242
+ function deriveBindingPostconditions(binding) {
1243
+ if (binding.type === 'query') {
1244
+ return ['Query result is available for downstream composition'];
1245
+ }
1246
+
1247
+ if (binding.type === 'mutation') {
1248
+ return ['Mutation result is captured and write scope is consistent'];
1249
+ }
1250
+
1251
+ if (binding.type === 'invoke') {
1252
+ return ['Workflow step output is captured for downstream execution'];
1253
+ }
1254
+
1255
+ return ['Binding execution result is available'];
1256
+ }
1257
+
1258
+ function addBindingSemantics(baseBindings, primaryResource) {
1259
+ const bindings = [];
1260
+
1261
+ for (let i = 0; i < baseBindings.length; i++) {
1262
+ const base = baseBindings[i];
1263
+ const previous = i > 0 ? baseBindings[i - 1] : null;
1264
+ const binding = {
1265
+ ...base,
1266
+ intent: deriveBindingIntent(base, primaryResource),
1267
+ preconditions: deriveBindingPreconditions(base, previous ? previous.ref : null),
1268
+ postconditions: deriveBindingPostconditions(base)
1269
+ };
1270
+
1271
+ if (previous && previous.ref) {
1272
+ binding.depends_on = previous.ref;
1273
+ }
1274
+
1275
+ bindings.push(binding);
1276
+ }
1277
+
1278
+ return bindings;
1279
+ }
1280
+
1281
+ function buildDataLineage(bindings, pattern, primaryResource) {
1282
+ if (!Array.isArray(bindings) || bindings.length === 0) {
1283
+ return {
1284
+ sources: [],
1285
+ transforms: [],
1286
+ sinks: []
1287
+ };
1288
+ }
1289
+
1290
+ const firstRef = bindings[0].ref;
1291
+ const lastRef = bindings[bindings.length - 1].ref;
1292
+ const sourceField = `${toKebabCase(primaryResource)}Id`;
1293
+
1294
+ const transforms = [
1295
+ {
1296
+ operation: 'normalizeInput',
1297
+ description: `Normalize ${primaryResource} request payload for template execution`
1298
+ }
1299
+ ];
1300
+
1301
+ if (pattern === 'workflow') {
1302
+ transforms.push({
1303
+ operation: 'orchestrateWorkflow',
1304
+ description: `Coordinate service chain for ${primaryResource}`
1305
+ });
1306
+ } else if (pattern === 'crud') {
1307
+ transforms.push({
1308
+ operation: 'applyMutationGuard',
1309
+ description: `Apply mutation and idempotency guard for ${primaryResource}`
1310
+ });
1311
+ } else {
1312
+ transforms.push({
1313
+ operation: 'projectQueryResult',
1314
+ description: `Project query result set for ${primaryResource}`
1315
+ });
1316
+ }
1317
+
1318
+ return {
1319
+ sources: [
1320
+ {
1321
+ ref: firstRef,
1322
+ fields: [sourceField, 'statusId']
1323
+ }
1324
+ ],
1325
+ transforms,
1326
+ sinks: [
1327
+ {
1328
+ ref: lastRef,
1329
+ fields: [sourceField, 'statusId']
1330
+ }
1331
+ ]
1332
+ };
1333
+ }
1334
+
1335
+ function buildEntityRefs(match, primaryResource) {
1336
+ const refs = Array.isArray(match.entities) && match.entities.length > 0
1337
+ ? match.entities.filter(Boolean)
1338
+ : [primaryResource];
1339
+
1340
+ return refs.map((entity, index) => ({
1341
+ id: String(entity),
1342
+ type: index === 0 ? 'primary' : 'related'
1343
+ }));
1344
+ }
1345
+
1346
+ function buildEntityRelations(entityRefs) {
1347
+ if (!Array.isArray(entityRefs) || entityRefs.length === 0) {
1348
+ return [];
1349
+ }
1350
+
1351
+ const relations = [];
1352
+ const primaryId = entityRefs[0].id;
1353
+
1354
+ for (let i = 1; i < entityRefs.length; i++) {
1355
+ relations.push({
1356
+ source: primaryId,
1357
+ target: entityRefs[i].id,
1358
+ type: 'composes'
1359
+ });
1360
+ }
1361
+
1362
+ if (relations.length === 0) {
1363
+ relations.push({
1364
+ source: primaryId,
1365
+ target: 'metadata_view',
1366
+ type: 'produces'
1367
+ });
1368
+ }
1369
+
1370
+ return relations;
1371
+ }
1372
+
1373
+ function buildBusinessRules(pattern, bindings, primaryResource) {
1374
+ const firstRef = bindings[0] ? bindings[0].ref : null;
1375
+ const lastRef = bindings[bindings.length - 1] ? bindings[bindings.length - 1].ref : null;
1376
+
1377
+ const rules = [
1378
+ {
1379
+ id: `rule.${toKebabCase(primaryResource)}.binding-order`,
1380
+ description: `Bindings for ${primaryResource} must execute in declared dependency order`,
1381
+ bind_to: firstRef,
1382
+ status: 'enforced'
1383
+ }
1384
+ ];
1385
+
1386
+ if (pattern === 'query') {
1387
+ rules.push({
1388
+ id: `rule.${toKebabCase(primaryResource)}.read-only`,
1389
+ description: `${primaryResource} query template must remain side-effect free`,
1390
+ bind_to: lastRef,
1391
+ status: 'active'
1392
+ });
1393
+ } else {
1394
+ rules.push({
1395
+ id: `rule.${toKebabCase(primaryResource)}.approval-or-idempotency`,
1396
+ description: `${primaryResource} template must enforce approval or idempotency guard`,
1397
+ bind_to: lastRef,
1398
+ status: 'active'
1399
+ });
1400
+ }
1401
+
1402
+ return rules;
1403
+ }
1404
+
1405
+ function buildDecisionLogic(pattern, bindings, primaryResource) {
1406
+ const lastRef = bindings[bindings.length - 1] ? bindings[bindings.length - 1].ref : null;
1407
+ const riskDecision = pattern === 'query'
1408
+ ? 'Use low-risk dry-run defaults for query execution'
1409
+ : 'Use guarded execution with approval and retry policies';
1410
+
1411
+ return [
1412
+ {
1413
+ id: `decision.${toKebabCase(primaryResource)}.risk-strategy`,
1414
+ description: riskDecision,
1415
+ bind_to: lastRef,
1416
+ status: 'resolved',
1417
+ automated: true
1418
+ },
1419
+ {
1420
+ id: `decision.${toKebabCase(primaryResource)}.retry-strategy`,
1421
+ description: 'Apply timeout/retry profile derived from template contract',
1422
+ bind_to: lastRef,
1423
+ status: 'resolved',
1424
+ automated: true
1425
+ }
1426
+ ];
1427
+ }
1428
+
1429
+ function buildAgentHints(pattern, primaryResource, bindings) {
1430
+ const complexity = pattern === 'query' ? 'low' : 'medium';
1431
+ const baseDuration = pattern === 'query' ? 1800 : 3000;
1432
+ const permissions = pattern === 'query'
1433
+ ? ['moqui.read']
1434
+ : ['moqui.read', 'moqui.write'];
1435
+
1436
+ return {
1437
+ summary: `${pattern.toUpperCase()} template extracted for ${primaryResource} with Moqui-aware ontology`,
1438
+ complexity,
1439
+ estimated_duration_ms: baseDuration + (bindings.length * 150),
1440
+ required_permissions: permissions,
1441
+ suggested_sequence: bindings.map((binding) => binding.ref),
1442
+ rollback_strategy: pattern === 'query'
1443
+ ? 'Re-run query with previous filters'
1444
+ : 'Reconcile idempotency key and rollback to pre-mutation snapshot'
1445
+ };
1446
+ }
1447
+
1448
+ // ─── Scene Manifest Generation ────────────────────────────────────
1449
+
1450
+ /**
1451
+ * Generate a scene manifest object for a pattern match.
1452
+ * Produces a manifest with correct apiVersion, kind, bindings, model_scope,
1453
+ * and governance_contract based on the pattern type.
1454
+ *
1455
+ * Pattern rules for bindings:
1456
+ * - "crud": 5 bindings (list, get = query; create, update, delete = mutation with side_effect)
1457
+ * - "query": 2 bindings (list, get = query)
1458
+ * - "workflow": service invoke bindings (type: 'invoke', ref from bindingRefs)
1459
+ *
1460
+ * Governance rules:
1461
+ * - "query": risk_level "low", approval.required false, no idempotency
1462
+ * - "crud"/"workflow": risk_level "medium", approval.required true, idempotency.required true
1463
+ *
1464
+ * @param {PatternMatch} match - Matched pattern
1465
+ * @returns {Object|null} Scene manifest object, or null for invalid input
1466
+ */
1467
+ function generateSceneManifest(match) {
1468
+ if (!match || !match.pattern || !match.primaryResource) {
1469
+ return null;
1470
+ }
1471
+
1472
+ const pattern = match.pattern;
1473
+ const primaryResource = match.primaryResource;
1474
+ const packageName = derivePackageName(match);
1475
+ const gov = match.governance || {};
1476
+
1477
+ const baseBindings = buildBaseBindings(match);
1478
+ const bindings = addBindingSemantics(baseBindings, primaryResource);
1479
+
1480
+ // Build model_scope from match
1481
+ const modelScope = match.modelScope || { read: [], write: [] };
1482
+
1483
+ // Build governance_contract based on pattern type
1484
+ const riskLevel = gov.riskLevel || (pattern === 'query' ? 'low' : 'medium');
1485
+ const approvalRequired = gov.approvalRequired !== undefined
1486
+ ? gov.approvalRequired
1487
+ : (pattern !== 'query');
1488
+ const idempotencyRequired = gov.idempotencyRequired !== undefined
1489
+ ? gov.idempotencyRequired
1490
+ : (pattern !== 'query');
1491
+ const idempotencyKey = gov.idempotencyKey || deriveIdempotencyKey(
1492
+ Array.isArray(match.entities) && match.entities.length > 0
1493
+ ? match.entities[0]
1494
+ : primaryResource
1495
+ );
1496
+
1497
+ const governanceContract = {
1498
+ risk_level: riskLevel,
1499
+ approval: {
1500
+ required: approvalRequired
1501
+ },
1502
+ data_lineage: buildDataLineage(bindings, pattern, primaryResource)
1503
+ };
1504
+
1505
+ if (idempotencyRequired) {
1506
+ governanceContract.idempotency = {
1507
+ required: true,
1508
+ key: idempotencyKey
1509
+ };
1510
+ }
1511
+
1512
+ // Build intent goal based on pattern type
1513
+ let goal;
1514
+
1515
+ if (pattern === 'crud') {
1516
+ goal = `Full CRUD operations for ${primaryResource} entity`;
1517
+ } else if (pattern === 'query') {
1518
+ goal = `Read-only access to ${primaryResource} entity`;
1519
+ } else if (pattern === 'workflow') {
1520
+ goal = `Workflow orchestration for ${primaryResource} service`;
1521
+ } else {
1522
+ goal = `Operations for ${primaryResource}`;
1523
+ }
1524
+
1525
+ return {
1526
+ apiVersion: SCENE_API_VERSION,
1527
+ kind: 'scene',
1528
+ metadata: {
1529
+ obj_id: `scene.extracted.${packageName}`,
1530
+ obj_version: '0.1.0',
1531
+ title: `${pattern.charAt(0).toUpperCase() + pattern.slice(1)} ${primaryResource} Template`
1532
+ },
1533
+ spec: {
1534
+ domain: 'erp',
1535
+ intent: {
1536
+ goal
1537
+ },
1538
+ model_scope: {
1539
+ read: Array.isArray(modelScope.read) ? [...modelScope.read] : [],
1540
+ write: Array.isArray(modelScope.write) ? [...modelScope.write] : []
1541
+ },
1542
+ capability_contract: {
1543
+ bindings
1544
+ },
1545
+ governance_contract: governanceContract
1546
+ }
1547
+ };
1548
+ }
1549
+
1550
+ // ─── Package Contract Generation ──────────────────────────────────
1551
+
1552
+ /**
1553
+ * Generate a package contract object for a pattern match.
1554
+ * Produces a contract with correct apiVersion, kind, metadata, parameters,
1555
+ * artifacts, and governance fields.
1556
+ *
1557
+ * @param {PatternMatch} match - Matched pattern
1558
+ * @returns {Object|null} Package contract object, or null for invalid input
1559
+ */
1560
+ function generatePackageContract(match) {
1561
+ if (!match || !match.pattern || !match.primaryResource) {
1562
+ return null;
1563
+ }
1564
+
1565
+ const pattern = match.pattern;
1566
+ const primaryResource = match.primaryResource;
1567
+ const packageName = derivePackageName(match);
1568
+ const gov = match.governance || {};
1569
+ const baseBindings = buildBaseBindings(match);
1570
+ const bindings = addBindingSemantics(baseBindings, primaryResource);
1571
+ const riskLevel = gov.riskLevel || (pattern === 'query' ? 'low' : 'medium');
1572
+ const approvalRequired = gov.approvalRequired !== undefined
1573
+ ? gov.approvalRequired
1574
+ : (pattern !== 'query');
1575
+ const idempotencyRequired = gov.idempotencyRequired !== undefined
1576
+ ? gov.idempotencyRequired
1577
+ : (pattern !== 'query');
1578
+ const idempotencyKey = gov.idempotencyKey || deriveIdempotencyKey(
1579
+ Array.isArray(match.entities) && match.entities.length > 0
1580
+ ? match.entities[0]
1581
+ : primaryResource
1582
+ );
1583
+ const entityRefs = buildEntityRefs(match, primaryResource);
1584
+ const relations = buildEntityRelations(entityRefs);
1585
+ const hasMetadataView = entityRefs.some((entity) => entity.id === 'metadata_view');
1586
+
1587
+ if (!hasMetadataView) {
1588
+ entityRefs.push({ id: 'metadata_view', type: 'projection' });
1589
+ }
1590
+
1591
+ const hasMetadataRelation = relations.some((relation) => (
1592
+ relation.source === entityRefs[0].id
1593
+ && relation.target === 'metadata_view'
1594
+ && relation.type === 'produces'
1595
+ ));
1596
+
1597
+ if (!hasMetadataRelation) {
1598
+ relations.push({
1599
+ source: entityRefs[0].id,
1600
+ target: 'metadata_view',
1601
+ type: 'produces'
1602
+ });
1603
+ }
1604
+
1605
+ // Derive summary based on pattern type
1606
+ let summary;
1607
+
1608
+ if (pattern === 'crud') {
1609
+ summary = `CRUD template for ${primaryResource} entity extracted from Moqui ERP`;
1610
+ } else if (pattern === 'query') {
1611
+ summary = `Query template for ${primaryResource} entity extracted from Moqui ERP`;
1612
+ } else if (pattern === 'workflow') {
1613
+ summary = `Workflow template for ${primaryResource} service extracted from Moqui ERP`;
1614
+ } else {
1615
+ summary = `Template for ${primaryResource} extracted from Moqui ERP`;
1616
+ }
1617
+
1618
+ const governanceContract = {
1619
+ risk_level: riskLevel,
1620
+ approval: {
1621
+ required: approvalRequired
1622
+ },
1623
+ data_lineage: buildDataLineage(bindings, pattern, primaryResource),
1624
+ business_rules: buildBusinessRules(pattern, bindings, primaryResource),
1625
+ decision_logic: buildDecisionLogic(pattern, bindings, primaryResource)
1626
+ };
1627
+
1628
+ if (idempotencyRequired) {
1629
+ governanceContract.idempotency = {
1630
+ required: true,
1631
+ key: idempotencyKey
1632
+ };
1633
+ }
1634
+
1635
+ return {
1636
+ apiVersion: PACKAGE_API_VERSION,
1637
+ kind: 'scene-template',
1638
+ metadata: {
1639
+ group: 'kse.scene',
1640
+ name: packageName,
1641
+ version: '0.1.0',
1642
+ summary,
1643
+ description: `${summary}. Includes ontology graph hints, lineage tracing, and AI execution metadata.`
1644
+ },
1645
+ compatibility: {
1646
+ kse_version: '>=1.39.0',
1647
+ scene_api_version: SCENE_API_VERSION
1648
+ },
1649
+ parameters: [
1650
+ {
1651
+ id: 'timeout_ms',
1652
+ type: 'number',
1653
+ required: false,
1654
+ default: 2000,
1655
+ description: 'Request timeout in milliseconds'
1656
+ },
1657
+ {
1658
+ id: 'retry_count',
1659
+ type: 'number',
1660
+ required: false,
1661
+ default: 0,
1662
+ description: 'Number of retry attempts'
1663
+ }
1664
+ ],
1665
+ artifacts: {
1666
+ entry_scene: 'scene.yaml',
1667
+ generates: ['scene.yaml', 'scene-package.json']
1668
+ },
1669
+ governance: {
1670
+ risk_level: riskLevel,
1671
+ approval_required: approvalRequired,
1672
+ approval: {
1673
+ required: approvalRequired
1674
+ },
1675
+ idempotency: {
1676
+ required: idempotencyRequired,
1677
+ key: idempotencyKey
1678
+ },
1679
+ rollback_supported: true
1680
+ },
1681
+ capability_contract: {
1682
+ bindings
1683
+ },
1684
+ governance_contract: governanceContract,
1685
+ ontology_model: {
1686
+ entities: entityRefs,
1687
+ relations
1688
+ },
1689
+ agent_hints: buildAgentHints(pattern, primaryResource, bindings)
1690
+ };
1691
+ }
1692
+
1693
+ // ─── File Writing ─────────────────────────────────────────────────
1694
+
1695
+ /**
1696
+ * Write template bundles to the output directory.
1697
+ * Creates one subdirectory per bundle containing scene.yaml and scene-package.json.
1698
+ * Partial failure resilient: if one bundle fails, continues with remaining bundles.
1699
+ *
1700
+ * @param {TemplateBundleOutput[]} bundles - Generated bundles
1701
+ * @param {string} outDir - Output directory path
1702
+ * @param {Object} [fileSystem] - fs-extra compatible file system (for DI/testing)
1703
+ * @returns {Promise<WriteResult[]>}
1704
+ */
1705
+ async function writeTemplateBundles(bundles, outDir, fileSystem) {
1706
+ // Handle null/empty bundles gracefully
1707
+ if (!bundles || !Array.isArray(bundles) || bundles.length === 0) {
1708
+ return [];
1709
+ }
1710
+
1711
+ const fs = fileSystem || require('fs-extra');
1712
+ const results = [];
1713
+
1714
+ // Ensure outDir exists (create recursively if needed)
1715
+ try {
1716
+ fs.ensureDirSync(outDir);
1717
+ } catch (err) {
1718
+ // If we can't create the output directory, all bundles fail
1719
+ for (const bundle of bundles) {
1720
+ results.push({
1721
+ bundleDir: bundle.bundleDir || '',
1722
+ success: false,
1723
+ error: `Failed to create output directory: ${err.message}`
1724
+ });
1725
+ }
1726
+ return results;
1727
+ }
1728
+
1729
+ // Write each bundle with partial failure resilience
1730
+ for (const bundle of bundles) {
1731
+ const bundleDir = bundle.bundleDir || '';
1732
+ const bundlePath = path.join(outDir, bundleDir);
1733
+
1734
+ try {
1735
+ // Create subdirectory for this bundle
1736
+ fs.ensureDirSync(bundlePath);
1737
+
1738
+ // Write scene.yaml
1739
+ const sceneYamlPath = path.join(bundlePath, 'scene.yaml');
1740
+ fs.writeFileSync(sceneYamlPath, bundle.manifestYaml || '');
1741
+
1742
+ // Write scene-package.json
1743
+ const packageJsonPath = path.join(bundlePath, 'scene-package.json');
1744
+ fs.writeFileSync(packageJsonPath, bundle.contractJson || '');
1745
+
1746
+ results.push({ bundleDir, success: true });
1747
+ } catch (err) {
1748
+ // Catch error, add to results, continue with remaining bundles
1749
+ results.push({
1750
+ bundleDir,
1751
+ success: false,
1752
+ error: err.message
1753
+ });
1754
+ }
1755
+ }
1756
+
1757
+ return results;
1758
+ }
1759
+
1760
+ // ─── Discovery ────────────────────────────────────────────────────
1761
+
1762
+ /**
1763
+ * Catalog endpoint definitions for Moqui resource discovery.
1764
+ * Each entry maps a resource type to its API endpoint and response key.
1765
+ */
1766
+ const CATALOG_ENDPOINTS = {
1767
+ entities: { path: '/api/v1/entities', key: 'entities' },
1768
+ services: { path: '/api/v1/services', key: 'services' },
1769
+ screens: { path: '/api/v1/screens', key: 'screens' }
1770
+ };
1771
+
1772
+ /**
1773
+ * Discover resources from a Moqui instance.
1774
+ * Queries catalog endpoints with optional type filtering and partial failure handling.
1775
+ *
1776
+ * @param {MoquiClient} client - Authenticated MoquiClient instance
1777
+ * @param {Object} [options] - Discovery options
1778
+ * @param {string} [options.type] - Filter: 'entities' | 'services' | 'screens'
1779
+ * @returns {Promise<{ entities: string[], services: string[], screens: string[], warnings: string[] }>}
1780
+ */
1781
+ async function discoverResources(client, options = {}) {
1782
+ const result = {
1783
+ entities: [],
1784
+ services: [],
1785
+ screens: [],
1786
+ warnings: []
1787
+ };
1788
+
1789
+ // Determine which types to query
1790
+ const typesToQuery = options.type
1791
+ ? [options.type]
1792
+ : ['entities', 'services', 'screens'];
1793
+
1794
+ for (const typeName of typesToQuery) {
1795
+ const endpoint = CATALOG_ENDPOINTS[typeName];
1796
+ if (!endpoint) {
1797
+ result.warnings.push(`Unknown resource type: ${typeName}`);
1798
+ continue;
1799
+ }
1800
+
1801
+ try {
1802
+ const response = await client.request('GET', endpoint.path);
1803
+
1804
+ if (!response || !response.success) {
1805
+ const errMsg = (response && response.error && response.error.message)
1806
+ || `Failed to query ${typeName}`;
1807
+ result.warnings.push(`${typeName}: ${errMsg}`);
1808
+ continue;
1809
+ }
1810
+
1811
+ // Extract data from response
1812
+ const rawData = response.data;
1813
+ let items;
1814
+
1815
+ if (Array.isArray(rawData)) {
1816
+ items = rawData;
1817
+ } else if (rawData && typeof rawData === 'object') {
1818
+ items = rawData[endpoint.key] || rawData.items || [];
1819
+ } else {
1820
+ items = [];
1821
+ }
1822
+
1823
+ // Ensure items is an array of strings
1824
+ result[typeName] = Array.isArray(items)
1825
+ ? items.map(item => (typeof item === 'string' ? item : String(item)))
1826
+ : [];
1827
+ } catch (err) {
1828
+ // Partial failure: continue with other endpoints
1829
+ result.warnings.push(`${typeName}: ${err.message}`);
1830
+ }
1831
+ }
1832
+
1833
+ return result;
1834
+ }
1835
+
1836
+ // ─── Extraction Pipeline ──────────────────────────────────────────
1837
+
1838
+ /**
1839
+ * Default output directory for extracted templates.
1840
+ */
1841
+ const DEFAULT_OUT_DIR = '.kiro/templates/extracted';
1842
+
1843
+ /**
1844
+ * Run the full extraction pipeline.
1845
+ * Orchestrates: config loading → client creation → login → discover → analyze → generate → write (or dry-run) → dispose.
1846
+ *
1847
+ * @param {Object} [options] - Extraction options
1848
+ * @param {string} [options.config] - Path to moqui-adapter.json
1849
+ * @param {string} [options.type] - Filter: 'entities' | 'services' | 'screens'
1850
+ * @param {string} [options.pattern] - Filter: 'crud' | 'query' | 'workflow'
1851
+ * @param {string} [options.out] - Output directory path
1852
+ * @param {boolean} [options.dryRun] - Preview without writing files
1853
+ * @param {boolean} [options.json] - Output as JSON
1854
+ * @param {Object} [dependencies] - Dependency injection for testing
1855
+ * @param {string} [dependencies.projectRoot] - Project root directory
1856
+ * @param {Object} [dependencies.fileSystem] - fs-extra compatible file system
1857
+ * @param {MoquiClient} [dependencies.client] - Pre-configured MoquiClient (skips config/login)
1858
+ * @returns {Promise<ExtractionResult>}
1859
+ */
1860
+ async function runExtraction(options = {}, dependencies = {}) {
1861
+ const outDir = options.out || DEFAULT_OUT_DIR;
1862
+ let client = dependencies.client || null;
1863
+ let clientOwned = false; // Track if we created the client (and thus must dispose it)
1864
+
1865
+ /**
1866
+ * Build an error ExtractionResult.
1867
+ */
1868
+ function makeErrorResult(code, message, warnings = []) {
1869
+ return {
1870
+ success: false,
1871
+ templates: [],
1872
+ summary: {
1873
+ totalTemplates: 0,
1874
+ patterns: { crud: 0, query: 0, workflow: 0 },
1875
+ outputDir: outDir
1876
+ },
1877
+ warnings,
1878
+ error: { code, message }
1879
+ };
1880
+ }
1881
+
1882
+ try {
1883
+ // ── Step 1: Load and validate config (skip if client injected) ──
1884
+ if (!client) {
1885
+ const projectRoot = dependencies.projectRoot || process.cwd();
1886
+ const configResult = loadAdapterConfig(options.config, projectRoot);
1887
+
1888
+ if (configResult.error) {
1889
+ const code = configResult.error.startsWith('CONFIG_NOT_FOUND')
1890
+ ? 'CONFIG_NOT_FOUND'
1891
+ : 'CONFIG_INVALID';
1892
+ return makeErrorResult(code, configResult.error);
1893
+ }
1894
+
1895
+ const validation = validateAdapterConfig(configResult.config);
1896
+ if (!validation.valid) {
1897
+ return makeErrorResult(
1898
+ 'CONFIG_INVALID',
1899
+ `Invalid adapter config: ${validation.errors.join('; ')}`
1900
+ );
1901
+ }
1902
+
1903
+ // ── Step 2: Create MoquiClient ──
1904
+ client = new MoquiClient(configResult.config);
1905
+ clientOwned = true;
1906
+
1907
+ // ── Step 3: Login ──
1908
+ const loginResult = await client.login();
1909
+ if (!loginResult.success) {
1910
+ return makeErrorResult(
1911
+ 'AUTH_FAILED',
1912
+ loginResult.error || 'Authentication failed'
1913
+ );
1914
+ }
1915
+ }
1916
+
1917
+ // ── Step 4: Discover resources ──
1918
+ const discovery = await discoverResources(client, { type: options.type });
1919
+ const warnings = [...discovery.warnings];
1920
+
1921
+ // ── Step 5: Analyze resources ──
1922
+ const matches = analyzeResources(discovery, { pattern: options.pattern });
1923
+
1924
+ // ── Step 6: Generate manifests and contracts ──
1925
+ const templates = [];
1926
+ const patternCounts = { crud: 0, query: 0, workflow: 0 };
1927
+
1928
+ for (const match of matches) {
1929
+ const manifest = generateSceneManifest(match);
1930
+ const contract = generatePackageContract(match);
1931
+ const manifestYaml = serializeManifestToYaml(manifest);
1932
+ const contractJson = JSON.stringify(contract, null, 2);
1933
+ const bundleDir = deriveBundleDirName(match);
1934
+
1935
+ templates.push({
1936
+ bundleDir,
1937
+ manifest,
1938
+ contract,
1939
+ manifestYaml,
1940
+ contractJson
1941
+ });
1942
+
1943
+ if (patternCounts[match.pattern] !== undefined) {
1944
+ patternCounts[match.pattern]++;
1945
+ }
1946
+ }
1947
+
1948
+ // ── Step 7: Write bundles (unless dry-run) ──
1949
+ if (!options.dryRun && templates.length > 0) {
1950
+ const fs = dependencies.fileSystem || undefined;
1951
+ const writeResults = await writeTemplateBundles(templates, outDir, fs);
1952
+
1953
+ // Collect write warnings
1954
+ for (const wr of writeResults) {
1955
+ if (!wr.success) {
1956
+ warnings.push(`Write failed for ${wr.bundleDir}: ${wr.error}`);
1957
+ }
1958
+ }
1959
+ }
1960
+
1961
+ // ── Step 8: Build and return ExtractionResult ──
1962
+ return {
1963
+ success: true,
1964
+ templates,
1965
+ summary: {
1966
+ totalTemplates: templates.length,
1967
+ patterns: patternCounts,
1968
+ outputDir: outDir
1969
+ },
1970
+ warnings,
1971
+ error: null
1972
+ };
1973
+ } catch (err) {
1974
+ // Wrap unexpected errors
1975
+ const code = (err.code === 'ECONNREFUSED' || err.code === 'ENOTFOUND' || err.code === 'ETIMEDOUT')
1976
+ ? 'NETWORK_ERROR'
1977
+ : 'EXTRACT_FAILED';
1978
+ return makeErrorResult(code, err.message);
1979
+ } finally {
1980
+ // ── Always dispose client if we own it ──
1981
+ if (client && clientOwned) {
1982
+ try {
1983
+ await client.dispose();
1984
+ } catch (_) {
1985
+ // Ignore dispose errors
1986
+ }
1987
+ }
1988
+ }
1989
+ }
1990
+
1991
+ // ─── Module Exports ───────────────────────────────────────────────
1992
+
1993
+ module.exports = {
1994
+ // Constants
1995
+ SUPPORTED_PATTERNS,
1996
+ HEADER_ITEM_SUFFIXES,
1997
+ SCENE_API_VERSION,
1998
+ PACKAGE_API_VERSION,
1999
+ CATALOG_ENDPOINTS,
2000
+ DEFAULT_OUT_DIR,
2001
+ // YAML functions
2002
+ serializeManifestToYaml,
2003
+ parseYaml,
2004
+ // Entity grouping
2005
+ groupRelatedEntities,
2006
+ // Pattern matching
2007
+ matchEntityPattern,
2008
+ matchWorkflowPatterns,
2009
+ // Resource analysis
2010
+ analyzeResources,
2011
+ // Generation
2012
+ generateSceneManifest,
2013
+ generatePackageContract,
2014
+ // Name derivation
2015
+ deriveBundleDirName,
2016
+ derivePackageName,
2017
+ // File writing
2018
+ writeTemplateBundles,
2019
+ // Discovery & extraction pipeline
2020
+ discoverResources,
2021
+ runExtraction,
2022
+ // Internal helpers (exported for testing)
2023
+ needsYamlQuoting,
2024
+ formatYamlValue,
2025
+ parseScalarValue,
2026
+ toKebabCase,
2027
+ deriveIdempotencyKey,
2028
+ generateEntityModelScope
2029
+ };