@massu/core 1.10.7 → 1.12.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 (177) hide show
  1. package/CHANGELOG.md +1303 -0
  2. package/dist/adapter.js +6 -4
  3. package/dist/cli.js +559 -400
  4. package/dist/detect/adapters/.bundle-shasums.json +4 -7
  5. package/dist/detect/adapters/rails.js +6 -4
  6. package/dist/detect/adapters/spring.js +6 -4
  7. package/dist/detect/adapters/tree-sitter-loader.js +6 -4
  8. package/dist/hooks/auto-learning-pipeline.js +24 -1
  9. package/dist/hooks/classify-failure.js +32 -3
  10. package/dist/hooks/cost-tracker.js +24 -0
  11. package/dist/hooks/fix-detector.js +97 -1
  12. package/dist/hooks/incident-pipeline.js +30 -1
  13. package/dist/hooks/intent-suggester.js +6 -3
  14. package/dist/hooks/post-edit-context.js +24 -1
  15. package/dist/hooks/post-tool-use.js +66 -18
  16. package/dist/hooks/pre-compact.js +27 -2
  17. package/dist/hooks/pre-delete-check.js +48 -22
  18. package/dist/hooks/pre-tool-use-gate.js +962 -0
  19. package/dist/hooks/quality-event.js +24 -0
  20. package/dist/hooks/rule-enforcement-pipeline.js +24 -1
  21. package/dist/hooks/security-gate.js +29 -16
  22. package/dist/hooks/session-end.js +24 -0
  23. package/dist/hooks/session-start.js +8710 -8460
  24. package/dist/hooks/user-prompt.js +24 -0
  25. package/package.json +15 -18
  26. package/commands/README.md +0 -134
  27. package/src/adapter.ts +0 -121
  28. package/src/adr-generator.ts +0 -292
  29. package/src/analytics.ts +0 -373
  30. package/src/audit-trail.ts +0 -450
  31. package/src/backfill-sessions.ts +0 -182
  32. package/src/changelog-generator.ts +0 -178
  33. package/src/cli.ts +0 -246
  34. package/src/cloud-sync.ts +0 -234
  35. package/src/commands/adapters.ts +0 -824
  36. package/src/commands/changelog.ts +0 -165
  37. package/src/commands/config-check-drift.ts +0 -133
  38. package/src/commands/config-refresh.ts +0 -396
  39. package/src/commands/config-upgrade.ts +0 -127
  40. package/src/commands/doctor.ts +0 -485
  41. package/src/commands/hook-runner.ts +0 -145
  42. package/src/commands/init.ts +0 -1823
  43. package/src/commands/install-commands.ts +0 -692
  44. package/src/commands/install-hooks.ts +0 -27
  45. package/src/commands/permissions.ts +0 -150
  46. package/src/commands/refresh-log.ts +0 -37
  47. package/src/commands/show-template.ts +0 -65
  48. package/src/commands/template-engine.ts +0 -301
  49. package/src/commands/watch.ts +0 -430
  50. package/src/config.ts +0 -765
  51. package/src/cost-tracker.ts +0 -355
  52. package/src/db.ts +0 -370
  53. package/src/dependency-scorer.ts +0 -337
  54. package/src/detect/adapters/aspnet.ts +0 -4
  55. package/src/detect/adapters/discover.ts +0 -469
  56. package/src/detect/adapters/file-sampler.ts +0 -225
  57. package/src/detect/adapters/go-chi.ts +0 -4
  58. package/src/detect/adapters/index.ts +0 -51
  59. package/src/detect/adapters/nextjs-trpc.ts +0 -166
  60. package/src/detect/adapters/parse-guard.ts +0 -133
  61. package/src/detect/adapters/phoenix.ts +0 -4
  62. package/src/detect/adapters/python-django.ts +0 -208
  63. package/src/detect/adapters/python-fastapi.ts +0 -223
  64. package/src/detect/adapters/python-flask.ts +0 -235
  65. package/src/detect/adapters/query-helpers.ts +0 -170
  66. package/src/detect/adapters/rails.ts +0 -4
  67. package/src/detect/adapters/runner.ts +0 -284
  68. package/src/detect/adapters/spring.ts +0 -4
  69. package/src/detect/adapters/swift-swiftui.ts +0 -171
  70. package/src/detect/adapters/tree-sitter-loader.ts +0 -517
  71. package/src/detect/adapters/types.ts +0 -191
  72. package/src/detect/codebase-introspector.ts +0 -210
  73. package/src/detect/domain-inferrer.ts +0 -197
  74. package/src/detect/drift.ts +0 -199
  75. package/src/detect/framework-detector.ts +0 -307
  76. package/src/detect/index.ts +0 -209
  77. package/src/detect/manifest-registry.ts +0 -261
  78. package/src/detect/migrate.ts +0 -362
  79. package/src/detect/monorepo-detector.ts +0 -348
  80. package/src/detect/package-detector.ts +0 -828
  81. package/src/detect/passthrough.ts +0 -108
  82. package/src/detect/regex-fallback.ts +0 -449
  83. package/src/detect/source-dir-detector.ts +0 -297
  84. package/src/detect/vr-command-map.ts +0 -167
  85. package/src/docs-map.json +0 -100
  86. package/src/docs-tools.ts +0 -519
  87. package/src/domains.ts +0 -182
  88. package/src/hooks/auto-learning-pipeline.ts +0 -228
  89. package/src/hooks/classify-failure.ts +0 -259
  90. package/src/hooks/cost-tracker.ts +0 -66
  91. package/src/hooks/fix-detector.ts +0 -186
  92. package/src/hooks/incident-pipeline.ts +0 -190
  93. package/src/hooks/intent-suggester.ts +0 -131
  94. package/src/hooks/post-edit-context.ts +0 -91
  95. package/src/hooks/post-tool-use.ts +0 -413
  96. package/src/hooks/pre-compact.ts +0 -168
  97. package/src/hooks/pre-delete-check.ts +0 -243
  98. package/src/hooks/quality-event.ts +0 -127
  99. package/src/hooks/rule-enforcement-pipeline.ts +0 -159
  100. package/src/hooks/security-gate.ts +0 -153
  101. package/src/hooks/session-end.ts +0 -467
  102. package/src/hooks/session-start.ts +0 -436
  103. package/src/hooks/user-prompt.ts +0 -157
  104. package/src/import-resolver.ts +0 -226
  105. package/src/knowledge-db.ts +0 -176
  106. package/src/knowledge-indexer.ts +0 -708
  107. package/src/knowledge-tools.ts +0 -1414
  108. package/src/lib/fileLock.ts +0 -203
  109. package/src/lib/gitToplevel.ts +0 -22
  110. package/src/lib/hook-registry.ts +0 -43
  111. package/src/lib/installLock.ts +0 -66
  112. package/src/lib/memory-path.ts +0 -49
  113. package/src/lib/pidLiveness.ts +0 -67
  114. package/src/lib/settings-local.ts +0 -110
  115. package/src/lib/sql-table-names.ts +0 -71
  116. package/src/license.ts +0 -547
  117. package/src/lsp/auto-detect.ts +0 -98
  118. package/src/lsp/client.ts +0 -776
  119. package/src/lsp/enrich.ts +0 -127
  120. package/src/lsp/types.ts +0 -221
  121. package/src/memory-db.ts +0 -1614
  122. package/src/memory-file-ingest.ts +0 -128
  123. package/src/memory-tools.ts +0 -424
  124. package/src/middleware-tree.ts +0 -71
  125. package/src/observability-tools.ts +0 -343
  126. package/src/observation-extractor.ts +0 -418
  127. package/src/page-deps.ts +0 -285
  128. package/src/permissions.ts +0 -422
  129. package/src/prompt-analyzer.ts +0 -332
  130. package/src/python/coupling-detector.ts +0 -125
  131. package/src/python/domain-enforcer.ts +0 -84
  132. package/src/python/impact-analyzer.ts +0 -96
  133. package/src/python/import-parser.ts +0 -244
  134. package/src/python/import-resolver.ts +0 -136
  135. package/src/python/migration-indexer.ts +0 -116
  136. package/src/python/migration-parser.ts +0 -332
  137. package/src/python/model-indexer.ts +0 -71
  138. package/src/python/model-parser.ts +0 -279
  139. package/src/python/route-indexer.ts +0 -59
  140. package/src/python/route-parser.ts +0 -317
  141. package/src/python-tools.ts +0 -630
  142. package/src/regression-detector.ts +0 -319
  143. package/src/rules.ts +0 -57
  144. package/src/schema-mapper.ts +0 -232
  145. package/src/security/adapter-origin.ts +0 -130
  146. package/src/security/adapter-verifier.ts +0 -319
  147. package/src/security/atomic-write.ts +0 -164
  148. package/src/security/fetcher.ts +0 -200
  149. package/src/security/install-tracking.ts +0 -319
  150. package/src/security/license-pubkey.generated.ts +0 -30
  151. package/src/security/license-response-verifier.ts +0 -149
  152. package/src/security/local-fingerprint.ts +0 -225
  153. package/src/security/manifest-cache.ts +0 -333
  154. package/src/security/manifest-schema.ts +0 -129
  155. package/src/security/registry-pubkey.generated.ts +0 -35
  156. package/src/security/telemetry.ts +0 -320
  157. package/src/security-scorer.ts +0 -405
  158. package/src/security-utils.ts +0 -133
  159. package/src/sentinel-db.ts +0 -580
  160. package/src/sentinel-scanner.ts +0 -406
  161. package/src/sentinel-tools.ts +0 -513
  162. package/src/sentinel-types.ts +0 -140
  163. package/src/server-dispatch.ts +0 -225
  164. package/src/server.ts +0 -107
  165. package/src/session-archiver.ts +0 -111
  166. package/src/session-state-generator.ts +0 -174
  167. package/src/team-knowledge.ts +0 -407
  168. package/src/tool-db-needs.ts +0 -232
  169. package/src/tools.ts +0 -1205
  170. package/src/transcript-parser.ts +0 -458
  171. package/src/trpc-index.ts +0 -215
  172. package/src/validate-features-runner.ts +0 -107
  173. package/src/validation-engine.ts +0 -358
  174. package/src/watch/daemon.ts +0 -385
  175. package/src/watch/lockfile-detector.ts +0 -65
  176. package/src/watch/paths.ts +0 -279
  177. package/src/watch/state.ts +0 -178
@@ -1,362 +0,0 @@
1
- // Copyright (c) 2026 Massu. All rights reserved.
2
- // Licensed under BSL 1.1 - see LICENSE file for details.
3
-
4
- /**
5
- * Config migration logic (P7-003).
6
- *
7
- * Pure functions that upgrade legacy v1-shaped `massu.config.yaml` objects to
8
- * schema_version=2. Driven by the Phase 1 `DetectionResult` so the v2 output
9
- * reflects the actual runtime stack.
10
- *
11
- * User overrides ALWAYS win — this migrator never overwrites fields that the
12
- * user set explicitly in v1. The only thing that gets replaced is content
13
- * whose v1 value is a schema default ('typescript', 'none', 'src', etc.) AND
14
- * detection discovered a different value.
15
- *
16
- * Idempotence: passing a v2 config back in should be a no-op (beyond `schema_version`
17
- * normalization). The test suite asserts this.
18
- *
19
- * No filesystem I/O, no network, no child processes. Pure data in, pure data
20
- * out. Consumers (future `massu config upgrade` CLI, drift-repair flow) are
21
- * responsible for reading/writing the YAML.
22
- */
23
-
24
- import type { DetectionResult, SupportedLanguage, VRCommandSet } from './index.ts';
25
- import { copyUnknownKeys, preserveNestedSubkeys } from './passthrough.ts';
26
-
27
- /**
28
- * Shape accepted for input. We intentionally use `Record<string, unknown>`
29
- * rather than the full `Config` Zod type — legacy configs may contain fields
30
- * that don't parse through the current schema (e.g., invalid enum values for
31
- * `framework.router` from eko-ultra-automations), and a strict shape would
32
- * reject the very configs we're trying to fix.
33
- */
34
- export type AnyConfig = Record<string, unknown>;
35
-
36
- /** Fields the migrator preserves verbatim when present on the v1 input. */
37
- const PRESERVED_FIELDS: readonly string[] = [
38
- 'rules',
39
- 'domains',
40
- 'canonical_paths',
41
- 'verification_types',
42
- 'detection',
43
- 'accessScopes',
44
- 'knownMismatches',
45
- 'dbAccessPattern',
46
- 'analytics',
47
- 'governance',
48
- 'security',
49
- 'team',
50
- 'regression',
51
- 'cloud',
52
- 'conventions',
53
- 'autoLearning',
54
- ];
55
-
56
- function getRecord(obj: unknown): Record<string, unknown> {
57
- if (obj !== null && typeof obj === 'object' && !Array.isArray(obj)) {
58
- return obj as Record<string, unknown>;
59
- }
60
- return {};
61
- }
62
-
63
- function isNoneOrDefault(v: unknown): boolean {
64
- if (v === undefined || v === null) return true;
65
- if (typeof v !== 'string') return false;
66
- return v === 'none' || v === '';
67
- }
68
-
69
- function chooseString(user: unknown, detected: string | null | undefined): string {
70
- // User wins unless it's falsey / 'none' and the detector has something concrete.
71
- if (typeof user === 'string' && !isNoneOrDefault(user)) return user;
72
- if (typeof detected === 'string' && detected !== '') return detected;
73
- if (typeof user === 'string') return user;
74
- return 'none';
75
- }
76
-
77
- function buildLanguageEntries(
78
- detection: DetectionResult
79
- ): Record<string, Record<string, unknown>> {
80
- const languages = Array.from(
81
- new Set(detection.manifests.map((m) => m.language))
82
- ) as SupportedLanguage[];
83
- const entries: Record<string, Record<string, unknown>> = {};
84
- for (const lang of languages) {
85
- const fw = detection.frameworks[lang];
86
- const dirInfo = detection.sourceDirs[lang];
87
- const sourceDirs = dirInfo?.source_dirs ?? [];
88
- const entry: Record<string, unknown> = {};
89
- if (fw?.framework) entry.framework = fw.framework;
90
- if (fw?.test_framework) entry.test_framework = fw.test_framework;
91
- if (fw?.orm) entry.orm = fw.orm;
92
- if (fw?.router) entry.router = fw.router;
93
- if (fw?.ui_library) entry.ui = fw.ui_library;
94
- if (sourceDirs.length > 0) entry.source_dirs = sourceDirs;
95
- if (Object.keys(entry).length > 0) {
96
- entries[lang] = entry;
97
- }
98
- }
99
- return entries;
100
- }
101
-
102
- function pickPrimary(detection: DetectionResult): SupportedLanguage | null {
103
- const counts = new Map<SupportedLanguage, number>();
104
- for (const m of detection.manifests) {
105
- counts.set(m.language, (counts.get(m.language) ?? 0) + 1);
106
- }
107
- const sorted = [...counts.entries()].sort((a, b) => {
108
- if (b[1] !== a[1]) return b[1] - a[1];
109
- return a[0].localeCompare(b[0]);
110
- });
111
- return sorted.length > 0 ? sorted[0][0] : null;
112
- }
113
-
114
- function buildVerificationBlock(
115
- detection: DetectionResult,
116
- userVerification: Record<string, Record<string, string>> | undefined
117
- ): Record<string, Record<string, string>> {
118
- const ver: Record<string, Record<string, string>> = {};
119
- const languages = Array.from(
120
- new Set(detection.manifests.map((m) => m.language))
121
- ) as SupportedLanguage[];
122
- for (const lang of languages) {
123
- const cmds: VRCommandSet | undefined = detection.verificationCommands[lang];
124
- if (!cmds) continue;
125
- const entry: Record<string, string> = {};
126
- if (cmds.test) entry.test = cmds.test;
127
- if (cmds.type) entry.type = cmds.type;
128
- if (cmds.build) entry.build = cmds.build;
129
- if (cmds.syntax) entry.syntax = cmds.syntax;
130
- if (cmds.lint) entry.lint = cmds.lint;
131
- if (Object.keys(entry).length > 0) ver[lang] = entry;
132
- }
133
- // Overlay user overrides — any user verification entry wins over detected.
134
- if (userVerification) {
135
- for (const [lang, userEntry] of Object.entries(userVerification)) {
136
- if (typeof userEntry !== 'object' || userEntry === null) continue;
137
- ver[lang] = { ...(ver[lang] ?? {}), ...userEntry };
138
- }
139
- }
140
- return ver;
141
- }
142
-
143
- /**
144
- * Migrate a v1-shaped (or malformed) config to v2, using `detection` as the
145
- * source of truth for the stack layout. User-authored overrides (rules,
146
- * domains, canonical_paths, etc.) are preserved unchanged.
147
- *
148
- * Idempotent: passing a valid v2 config back in returns an equivalent v2 config.
149
- */
150
- export function migrateV1ToV2(
151
- v1Config: AnyConfig,
152
- detection: DetectionResult
153
- ): AnyConfig {
154
- const v1 = getRecord(v1Config);
155
- const v1Framework = getRecord(v1.framework);
156
- const v1Paths = getRecord(v1.paths);
157
- const v1Project = getRecord(v1.project);
158
- const v1Verification = v1.verification as
159
- | Record<string, Record<string, string>>
160
- | undefined;
161
-
162
- const languages = Array.from(
163
- new Set(detection.manifests.map((m) => m.language))
164
- ) as SupportedLanguage[];
165
- const languageEntries = buildLanguageEntries(detection);
166
- const primary = pickPrimary(detection);
167
-
168
- const frameworkType = languages.length > 1
169
- ? 'multi'
170
- : (languages[0] ?? (typeof v1Framework.type === 'string' ? v1Framework.type : 'typescript'));
171
-
172
- // Legacy top-level router/orm/ui: prefer user value; fall back to primary lang.
173
- const primaryEntry = primary ? languageEntries[primary] : undefined;
174
- const primaryRouter = primaryEntry?.router as string | undefined;
175
- const primaryOrm = primaryEntry?.orm as string | undefined;
176
- const primaryUi = primaryEntry?.ui as string | undefined;
177
-
178
- const legacyRouter = chooseString(v1Framework.router, primaryRouter);
179
- const legacyOrm = chooseString(v1Framework.orm, primaryOrm);
180
- const legacyUi = chooseString(v1Framework.ui, primaryUi);
181
-
182
- const framework: Record<string, unknown> = {
183
- type: frameworkType,
184
- router: legacyRouter,
185
- orm: legacyOrm,
186
- ui: legacyUi,
187
- };
188
- if (languages.length > 1 && primary) {
189
- framework.primary = primary;
190
- }
191
- if (Object.keys(languageEntries).length > 0) {
192
- framework.languages = languageEntries;
193
- }
194
- // P1-004: preserve any v1Framework subkey the explicit rebuild didn't emit
195
- // (e.g., a multi-runtime monorepo's `framework.{python, rust, swift, typescript}` language sub-blocks).
196
- preserveNestedSubkeys(v1Framework, framework);
197
-
198
- // Paths: preserve user-set fields; fill `source` from detection if user had 'src' default.
199
- // P1-003 (mirror of init.ts:367-390): when primary language has no source dir
200
- // AND this is a monorepo, fall back to the common parent of workspace packages
201
- // instead of leaving the nonexistent default 'src' in place.
202
- let pathsSource: string = typeof v1Paths.source === 'string' ? v1Paths.source : 'src';
203
- if (pathsSource === 'src' && primary) {
204
- const primaryDirs = detection.sourceDirs[primary]?.source_dirs ?? [];
205
- if (primaryDirs.length > 0) {
206
- pathsSource = primaryDirs[0];
207
- } else if (
208
- detection.monorepo?.type !== undefined &&
209
- detection.monorepo.type !== 'single' &&
210
- detection.monorepo.packages.length > 0
211
- ) {
212
- pathsSource = monorepoCommonRootMigrate(detection.monorepo.packages);
213
- }
214
- }
215
- const aliases = v1Paths.aliases && typeof v1Paths.aliases === 'object'
216
- ? (v1Paths.aliases as Record<string, string>)
217
- : { '@': pathsSource };
218
- const paths: Record<string, unknown> = {
219
- source: pathsSource,
220
- aliases,
221
- };
222
- // P1-005 mirror: emit monorepo_roots for upgraded v2 configs (additive,
223
- // only when monorepo + not already user-specified).
224
- if (
225
- detection.monorepo?.type !== undefined &&
226
- detection.monorepo.type !== 'single' &&
227
- detection.monorepo.packages.length > 0 &&
228
- !('monorepo_roots' in v1Paths)
229
- ) {
230
- const roots = monorepoDistinctRootsMigrate(detection.monorepo.packages);
231
- if (roots.length > 0) paths.monorepo_roots = roots;
232
- }
233
- for (const k of ['routers', 'routerRoot', 'pages', 'middleware', 'schema', 'components', 'hooks']) {
234
- if (typeof v1Paths[k] === 'string') paths[k] = v1Paths[k];
235
- }
236
- // P1-005: preserve any v1Paths subkey the explicit rebuild didn't emit
237
- // (e.g., a downstream consumer's 19 custom `paths.*` entries like adr, plans, monorepo_root).
238
- preserveNestedSubkeys(v1Paths, paths);
239
-
240
- const verification = buildVerificationBlock(detection, v1Verification);
241
-
242
- // P1-006: build project block with nested passthrough so custom subkeys
243
- // (e.g., a downstream consumer's `project.description`) survive the migration.
244
- const project: Record<string, unknown> = {
245
- name: typeof v1Project.name === 'string' ? v1Project.name : 'my-project',
246
- root: typeof v1Project.root === 'string' ? v1Project.root : 'auto',
247
- };
248
- preserveNestedSubkeys(v1Project, project);
249
-
250
- // Construct v2 output.
251
- const v2: AnyConfig = {
252
- schema_version: 2,
253
- project,
254
- framework,
255
- paths,
256
- toolPrefix: typeof v1.toolPrefix === 'string' ? v1.toolPrefix : 'massu',
257
- };
258
-
259
- // Preserve user-authored collections verbatim.
260
- for (const field of PRESERVED_FIELDS) {
261
- if (field in v1 && v1[field] !== undefined) {
262
- v2[field] = v1[field];
263
- }
264
- }
265
-
266
- // P1-001: preserve any v1 top-level key not already handled by the explicit
267
- // migrator. This is the generalization of PRESERVED_FIELDS — custom sections
268
- // like `services`, `workflow`, `north_stars` now pass through.
269
- //
270
- // `detection` is intentionally NOT in handledTopLevel: when a v2 config is
271
- // fed back in (idempotence check at migrate.ts:16), the existing `detection`
272
- // block round-trips via this passthrough path. It gets re-stamped with a
273
- // fresh fingerprint by the caller at config-upgrade.ts:96-99 right after
274
- // migrateV1ToV2 returns. Any future v2-only top-level key added here must
275
- // either appear in this list (with explicit handling above) or round-trip
276
- // through this passthrough — never add a v2-only key that does neither.
277
- // (A-006 architecture-review follow-up.)
278
- const handledTopLevel = new Set<string>([
279
- 'schema_version', 'project', 'framework', 'paths', 'toolPrefix',
280
- 'verification', 'python', ...PRESERVED_FIELDS,
281
- ]);
282
- copyUnknownKeys(v1, v2, handledTopLevel);
283
-
284
- // Ensure domains / rules exist as arrays (v2 requires them).
285
- if (!Array.isArray(v2.domains)) {
286
- v2.domains = [];
287
- }
288
- if (!Array.isArray(v2.rules)) {
289
- v2.rules = [];
290
- }
291
-
292
- if (Object.keys(verification).length > 0) {
293
- v2.verification = verification;
294
- }
295
-
296
- // Preserve/update the v1 `python` sub-block when python is detected.
297
- if (languages.includes('python')) {
298
- const existing = getRecord(v1.python);
299
- const pyFw = detection.frameworks.python;
300
- const pySourceDirs = detection.sourceDirs.python?.source_dirs ?? [];
301
- const pyRoot =
302
- typeof existing.root === 'string' && existing.root !== ''
303
- ? existing.root
304
- : (pySourceDirs.length > 0 ? pySourceDirs[0] : '.');
305
- const pythonBlock: Record<string, unknown> = {
306
- root: pyRoot,
307
- exclude_dirs: Array.isArray(existing.exclude_dirs)
308
- ? existing.exclude_dirs
309
- : ['__pycache__', '.venv', 'venv', '.mypy_cache', '.pytest_cache'],
310
- };
311
- if (existing.domains !== undefined) pythonBlock.domains = existing.domains;
312
- if (existing.alembic_dir !== undefined) pythonBlock.alembic_dir = existing.alembic_dir;
313
- if (pyFw?.framework && existing.framework === undefined) {
314
- pythonBlock.framework = pyFw.framework;
315
- } else if (existing.framework !== undefined) {
316
- pythonBlock.framework = existing.framework;
317
- }
318
- if (pyFw?.orm && existing.orm === undefined) {
319
- pythonBlock.orm = pyFw.orm;
320
- } else if (existing.orm !== undefined) {
321
- pythonBlock.orm = existing.orm;
322
- }
323
- // P1-007: preserve any v1 python subkey not already handled above
324
- // (e.g., `python.test_framework`, `python.database`).
325
- preserveNestedSubkeys(v1.python, pythonBlock);
326
- v2.python = pythonBlock;
327
- } else if (v1.python !== undefined) {
328
- // Preserve even if detection didn't find python (e.g. non-monorepo-with-python).
329
- v2.python = v1.python;
330
- }
331
-
332
- return v2;
333
- }
334
-
335
- /**
336
- * Return the common top-level parent directory across every workspace
337
- * package (mirror of init.ts:monorepoCommonRoot). Returns `'.'` when
338
- * packages span multiple parents.
339
- */
340
- function monorepoCommonRootMigrate(
341
- packages: ReadonlyArray<{ path: string }>
342
- ): string {
343
- const roots = monorepoDistinctRootsMigrate(packages);
344
- return roots.length === 1 ? roots[0] : '.';
345
- }
346
-
347
- /**
348
- * Return the distinct top-level parent directories of every workspace
349
- * package (mirror of init.ts:monorepoDistinctRoots). Sorted for determinism.
350
- */
351
- function monorepoDistinctRootsMigrate(
352
- packages: ReadonlyArray<{ path: string }>
353
- ): string[] {
354
- const set = new Set<string>();
355
- for (const p of packages) {
356
- const parts = p.path.split('/');
357
- if (parts.length > 1 && parts[0] !== '' && parts[0] !== '.') {
358
- set.add(parts[0]);
359
- }
360
- }
361
- return [...set].sort();
362
- }