@massu/core 0.9.2 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/dist/cli.js +11182 -1559
  2. package/dist/hooks/auto-learning-pipeline.js +99 -19
  3. package/dist/hooks/classify-failure.js +99 -19
  4. package/dist/hooks/cost-tracker.js +97 -11
  5. package/dist/hooks/fix-detector.js +99 -19
  6. package/dist/hooks/incident-pipeline.js +97 -11
  7. package/dist/hooks/post-edit-context.js +97 -11
  8. package/dist/hooks/post-tool-use.js +101 -20
  9. package/dist/hooks/pre-compact.js +97 -11
  10. package/dist/hooks/pre-delete-check.js +97 -11
  11. package/dist/hooks/quality-event.js +97 -11
  12. package/dist/hooks/rule-enforcement-pipeline.js +97 -11
  13. package/dist/hooks/session-end.js +97 -11
  14. package/dist/hooks/session-start.js +8803 -782
  15. package/dist/hooks/user-prompt.js +98 -43
  16. package/package.json +13 -3
  17. package/reference/hook-execution-order.md +17 -25
  18. package/src/cli.ts +81 -2
  19. package/src/commands/config-check-drift.ts +132 -0
  20. package/src/commands/config-refresh.ts +224 -0
  21. package/src/commands/config-upgrade.ts +126 -0
  22. package/src/commands/doctor.ts +1 -29
  23. package/src/commands/init.ts +756 -216
  24. package/src/config.ts +168 -12
  25. package/src/detect/domain-inferrer.ts +142 -0
  26. package/src/detect/drift.ts +199 -0
  27. package/src/detect/framework-detector.ts +281 -0
  28. package/src/detect/index.ts +174 -0
  29. package/src/detect/migrate.ts +278 -0
  30. package/src/detect/monorepo-detector.ts +347 -0
  31. package/src/detect/package-detector.ts +728 -0
  32. package/src/detect/source-dir-detector.ts +264 -0
  33. package/src/detect/vr-command-map.ts +167 -0
  34. package/src/hooks/auto-learning-pipeline.ts +2 -2
  35. package/src/hooks/classify-failure.ts +2 -2
  36. package/src/hooks/fix-detector.ts +2 -2
  37. package/src/hooks/session-start.ts +43 -2
  38. package/src/hooks/user-prompt.ts +1 -21
  39. package/src/knowledge-indexer.ts +1 -1
  40. package/src/license.ts +1 -2
  41. package/src/memory-db.ts +0 -5
  42. package/src/memory-file-ingest.ts +6 -13
  43. package/src/tools.ts +0 -8
  44. package/templates/multi-runtime/massu.config.yaml +80 -0
  45. package/templates/python-django/massu.config.yaml +51 -0
  46. package/templates/python-fastapi/massu.config.yaml +50 -0
  47. package/templates/rust-actix/massu.config.yaml +38 -0
  48. package/templates/swift-ios/massu.config.yaml +37 -0
  49. package/templates/ts-nestjs/massu.config.yaml +43 -0
  50. package/templates/ts-nextjs/massu.config.yaml +43 -0
  51. package/README.md +0 -40
  52. package/src/claude-md-templates.ts +0 -342
  53. package/src/mcp-bridge-tools.ts +0 -458
@@ -20,7 +20,8 @@ var DomainConfigSchema = z.object({
20
20
  });
21
21
  var PatternRuleConfigSchema = z.object({
22
22
  pattern: z.string().default("**"),
23
- rules: z.array(z.string()).default([])
23
+ rules: z.array(z.string()).default([]),
24
+ language: z.string().optional()
24
25
  });
25
26
  var CostModelSchema = z.object({
26
27
  input_per_million: z.number(),
@@ -232,17 +233,59 @@ var PathsConfigSchema = z.object({
232
233
  components: z.string().optional(),
233
234
  hooks: z.string().optional()
234
235
  });
236
+ var LanguageFrameworkEntrySchema = z.object({
237
+ framework: z.string().optional(),
238
+ test_framework: z.string().optional(),
239
+ test: z.string().optional(),
240
+ runtime: z.string().optional(),
241
+ orm: z.string().optional(),
242
+ router: z.string().optional(),
243
+ ui: z.string().optional()
244
+ }).passthrough();
245
+ var FrameworkConfigSchema = z.object({
246
+ type: z.string().default("typescript"),
247
+ primary: z.string().optional(),
248
+ router: z.string().default("none"),
249
+ orm: z.string().default("none"),
250
+ ui: z.string().default("none"),
251
+ languages: z.record(z.string(), LanguageFrameworkEntrySchema).optional()
252
+ }).passthrough();
253
+ var VerificationEntrySchema = z.object({
254
+ type: z.string().optional(),
255
+ test: z.string().optional(),
256
+ syntax: z.string().optional(),
257
+ lint: z.string().optional(),
258
+ build: z.string().optional()
259
+ }).passthrough();
260
+ var VerificationConfigSchema = z.record(z.string(), VerificationEntrySchema).optional();
261
+ var CanonicalPathsSchema = z.record(z.string(), z.string()).optional();
262
+ var VerificationTypesSchema = z.record(z.string(), z.string()).optional();
263
+ var DetectionRuleEntrySchema = z.object({
264
+ signals: z.array(z.string()).default([]),
265
+ priority: z.number().optional()
266
+ }).passthrough();
267
+ var DetectionConfigSchema = z.object({
268
+ rules: z.record(
269
+ z.string(),
270
+ // language
271
+ z.record(z.string(), DetectionRuleEntrySchema)
272
+ // framework -> rule entry
273
+ ).optional(),
274
+ signal_weights: z.record(z.string(), z.number()).optional(),
275
+ disable_builtin: z.boolean().optional()
276
+ }).passthrough().optional();
235
277
  var RawConfigSchema = z.object({
278
+ schema_version: z.union([z.literal(1), z.literal(2)]).default(1),
236
279
  project: z.object({
237
280
  name: z.string().default("my-project"),
238
281
  root: z.string().default("auto")
239
282
  }).default({ name: "my-project", root: "auto" }),
240
- framework: z.object({
241
- type: z.string().default("typescript"),
242
- router: z.string().default("none"),
243
- orm: z.string().default("none"),
244
- ui: z.string().default("none")
245
- }).default({ type: "typescript", router: "none", orm: "none", ui: "none" }),
283
+ framework: FrameworkConfigSchema.default({
284
+ type: "typescript",
285
+ router: "none",
286
+ orm: "none",
287
+ ui: "none"
288
+ }),
246
289
  paths: PathsConfigSchema.default({ source: "src", aliases: { "@": "src" } }),
247
290
  toolPrefix: z.string().default("massu"),
248
291
  dbAccessPattern: z.string().optional(),
@@ -257,8 +300,13 @@ var RawConfigSchema = z.object({
257
300
  regression: RegressionConfigSchema,
258
301
  cloud: CloudConfigSchema,
259
302
  conventions: ConventionsConfigSchema,
303
+ autoLearning: AutoLearningConfigSchema,
260
304
  python: PythonConfigSchema,
261
- autoLearning: AutoLearningConfigSchema
305
+ // P2-004 / P2-005 / P2-006 / P2-008: v2 extensions (all optional)
306
+ verification: VerificationConfigSchema,
307
+ canonical_paths: CanonicalPathsSchema,
308
+ verification_types: VerificationTypesSchema,
309
+ detection: DetectionConfigSchema
262
310
  }).passthrough();
263
311
  var _config = null;
264
312
  var _projectRoot = null;
@@ -302,14 +350,47 @@ function getConfig() {
302
350
  const content = readFileSync(configPath, "utf-8");
303
351
  rawYaml = parseYaml(content) ?? {};
304
352
  }
305
- const parsed = RawConfigSchema.parse(rawYaml);
353
+ const result = RawConfigSchema.safeParse(rawYaml);
354
+ if (!result.success) {
355
+ const issues = result.error.issues.map((i) => {
356
+ const path = i.path.length > 0 ? i.path.join(".") : "(root)";
357
+ const received = "received" in i && i.received !== void 0 ? ` (received ${JSON.stringify(i.received)})` : "";
358
+ return ` - ${path}: ${i.message}${received}`;
359
+ }).join("\n");
360
+ throw new Error(
361
+ `Invalid massu.config.yaml at ${configPath}:
362
+ ${issues}
363
+ Hint: run \`massu config refresh\` to regenerate a valid config or fix the listed fields manually.`
364
+ );
365
+ }
366
+ const parsed = result.data;
306
367
  const projectRoot = parsed.project.root === "auto" || !parsed.project.root ? root : resolve(root, parsed.project.root);
368
+ const fw = parsed.framework;
369
+ let router = fw.router;
370
+ let orm = fw.orm;
371
+ let ui = fw.ui;
372
+ if (fw.type === "multi" && fw.primary && fw.languages) {
373
+ const primaryEntry = fw.languages[fw.primary];
374
+ if (primaryEntry) {
375
+ if (router === "none" && primaryEntry.router) router = primaryEntry.router;
376
+ if (orm === "none" && primaryEntry.orm) orm = primaryEntry.orm;
377
+ if (ui === "none" && primaryEntry.ui) ui = primaryEntry.ui;
378
+ }
379
+ }
307
380
  _config = {
381
+ schema_version: parsed.schema_version,
308
382
  project: {
309
383
  name: parsed.project.name,
310
384
  root: projectRoot
311
385
  },
312
- framework: parsed.framework,
386
+ framework: {
387
+ type: fw.type,
388
+ router,
389
+ orm,
390
+ ui,
391
+ primary: fw.primary,
392
+ languages: fw.languages
393
+ },
313
394
  paths: parsed.paths,
314
395
  toolPrefix: parsed.toolPrefix,
315
396
  dbAccessPattern: parsed.dbAccessPattern,
@@ -324,7 +405,12 @@ function getConfig() {
324
405
  regression: parsed.regression,
325
406
  cloud: parsed.cloud,
326
407
  conventions: parsed.conventions,
327
- python: parsed.python
408
+ autoLearning: parsed.autoLearning,
409
+ python: parsed.python,
410
+ verification: parsed.verification,
411
+ canonical_paths: parsed.canonical_paths,
412
+ verification_types: parsed.verification_types,
413
+ detection: parsed.detection
328
414
  };
329
415
  if (!_config.cloud?.apiKey && process.env.MASSU_API_KEY) {
330
416
  _config.cloud = {
@@ -21,7 +21,8 @@ var DomainConfigSchema = z.object({
21
21
  });
22
22
  var PatternRuleConfigSchema = z.object({
23
23
  pattern: z.string().default("**"),
24
- rules: z.array(z.string()).default([])
24
+ rules: z.array(z.string()).default([]),
25
+ language: z.string().optional()
25
26
  });
26
27
  var CostModelSchema = z.object({
27
28
  input_per_million: z.number(),
@@ -233,17 +234,59 @@ var PathsConfigSchema = z.object({
233
234
  components: z.string().optional(),
234
235
  hooks: z.string().optional()
235
236
  });
237
+ var LanguageFrameworkEntrySchema = z.object({
238
+ framework: z.string().optional(),
239
+ test_framework: z.string().optional(),
240
+ test: z.string().optional(),
241
+ runtime: z.string().optional(),
242
+ orm: z.string().optional(),
243
+ router: z.string().optional(),
244
+ ui: z.string().optional()
245
+ }).passthrough();
246
+ var FrameworkConfigSchema = z.object({
247
+ type: z.string().default("typescript"),
248
+ primary: z.string().optional(),
249
+ router: z.string().default("none"),
250
+ orm: z.string().default("none"),
251
+ ui: z.string().default("none"),
252
+ languages: z.record(z.string(), LanguageFrameworkEntrySchema).optional()
253
+ }).passthrough();
254
+ var VerificationEntrySchema = z.object({
255
+ type: z.string().optional(),
256
+ test: z.string().optional(),
257
+ syntax: z.string().optional(),
258
+ lint: z.string().optional(),
259
+ build: z.string().optional()
260
+ }).passthrough();
261
+ var VerificationConfigSchema = z.record(z.string(), VerificationEntrySchema).optional();
262
+ var CanonicalPathsSchema = z.record(z.string(), z.string()).optional();
263
+ var VerificationTypesSchema = z.record(z.string(), z.string()).optional();
264
+ var DetectionRuleEntrySchema = z.object({
265
+ signals: z.array(z.string()).default([]),
266
+ priority: z.number().optional()
267
+ }).passthrough();
268
+ var DetectionConfigSchema = z.object({
269
+ rules: z.record(
270
+ z.string(),
271
+ // language
272
+ z.record(z.string(), DetectionRuleEntrySchema)
273
+ // framework -> rule entry
274
+ ).optional(),
275
+ signal_weights: z.record(z.string(), z.number()).optional(),
276
+ disable_builtin: z.boolean().optional()
277
+ }).passthrough().optional();
236
278
  var RawConfigSchema = z.object({
279
+ schema_version: z.union([z.literal(1), z.literal(2)]).default(1),
237
280
  project: z.object({
238
281
  name: z.string().default("my-project"),
239
282
  root: z.string().default("auto")
240
283
  }).default({ name: "my-project", root: "auto" }),
241
- framework: z.object({
242
- type: z.string().default("typescript"),
243
- router: z.string().default("none"),
244
- orm: z.string().default("none"),
245
- ui: z.string().default("none")
246
- }).default({ type: "typescript", router: "none", orm: "none", ui: "none" }),
284
+ framework: FrameworkConfigSchema.default({
285
+ type: "typescript",
286
+ router: "none",
287
+ orm: "none",
288
+ ui: "none"
289
+ }),
247
290
  paths: PathsConfigSchema.default({ source: "src", aliases: { "@": "src" } }),
248
291
  toolPrefix: z.string().default("massu"),
249
292
  dbAccessPattern: z.string().optional(),
@@ -258,8 +301,13 @@ var RawConfigSchema = z.object({
258
301
  regression: RegressionConfigSchema,
259
302
  cloud: CloudConfigSchema,
260
303
  conventions: ConventionsConfigSchema,
304
+ autoLearning: AutoLearningConfigSchema,
261
305
  python: PythonConfigSchema,
262
- autoLearning: AutoLearningConfigSchema
306
+ // P2-004 / P2-005 / P2-006 / P2-008: v2 extensions (all optional)
307
+ verification: VerificationConfigSchema,
308
+ canonical_paths: CanonicalPathsSchema,
309
+ verification_types: VerificationTypesSchema,
310
+ detection: DetectionConfigSchema
263
311
  }).passthrough();
264
312
  var _config = null;
265
313
  var _projectRoot = null;
@@ -303,14 +351,47 @@ function getConfig() {
303
351
  const content = readFileSync(configPath, "utf-8");
304
352
  rawYaml = parseYaml(content) ?? {};
305
353
  }
306
- const parsed = RawConfigSchema.parse(rawYaml);
354
+ const result = RawConfigSchema.safeParse(rawYaml);
355
+ if (!result.success) {
356
+ const issues = result.error.issues.map((i) => {
357
+ const path = i.path.length > 0 ? i.path.join(".") : "(root)";
358
+ const received = "received" in i && i.received !== void 0 ? ` (received ${JSON.stringify(i.received)})` : "";
359
+ return ` - ${path}: ${i.message}${received}`;
360
+ }).join("\n");
361
+ throw new Error(
362
+ `Invalid massu.config.yaml at ${configPath}:
363
+ ${issues}
364
+ Hint: run \`massu config refresh\` to regenerate a valid config or fix the listed fields manually.`
365
+ );
366
+ }
367
+ const parsed = result.data;
307
368
  const projectRoot = parsed.project.root === "auto" || !parsed.project.root ? root : resolve(root, parsed.project.root);
369
+ const fw = parsed.framework;
370
+ let router = fw.router;
371
+ let orm = fw.orm;
372
+ let ui = fw.ui;
373
+ if (fw.type === "multi" && fw.primary && fw.languages) {
374
+ const primaryEntry = fw.languages[fw.primary];
375
+ if (primaryEntry) {
376
+ if (router === "none" && primaryEntry.router) router = primaryEntry.router;
377
+ if (orm === "none" && primaryEntry.orm) orm = primaryEntry.orm;
378
+ if (ui === "none" && primaryEntry.ui) ui = primaryEntry.ui;
379
+ }
380
+ }
308
381
  _config = {
382
+ schema_version: parsed.schema_version,
309
383
  project: {
310
384
  name: parsed.project.name,
311
385
  root: projectRoot
312
386
  },
313
- framework: parsed.framework,
387
+ framework: {
388
+ type: fw.type,
389
+ router,
390
+ orm,
391
+ ui,
392
+ primary: fw.primary,
393
+ languages: fw.languages
394
+ },
314
395
  paths: parsed.paths,
315
396
  toolPrefix: parsed.toolPrefix,
316
397
  dbAccessPattern: parsed.dbAccessPattern,
@@ -325,7 +406,12 @@ function getConfig() {
325
406
  regression: parsed.regression,
326
407
  cloud: parsed.cloud,
327
408
  conventions: parsed.conventions,
328
- python: parsed.python
409
+ autoLearning: parsed.autoLearning,
410
+ python: parsed.python,
411
+ verification: parsed.verification,
412
+ canonical_paths: parsed.canonical_paths,
413
+ verification_types: parsed.verification_types,
414
+ detection: parsed.detection
329
415
  };
330
416
  if (!_config.cloud?.apiKey && process.env.MASSU_API_KEY) {
331
417
  _config.cloud = {
@@ -19,7 +19,8 @@ var DomainConfigSchema = z.object({
19
19
  });
20
20
  var PatternRuleConfigSchema = z.object({
21
21
  pattern: z.string().default("**"),
22
- rules: z.array(z.string()).default([])
22
+ rules: z.array(z.string()).default([]),
23
+ language: z.string().optional()
23
24
  });
24
25
  var CostModelSchema = z.object({
25
26
  input_per_million: z.number(),
@@ -231,17 +232,59 @@ var PathsConfigSchema = z.object({
231
232
  components: z.string().optional(),
232
233
  hooks: z.string().optional()
233
234
  });
235
+ var LanguageFrameworkEntrySchema = z.object({
236
+ framework: z.string().optional(),
237
+ test_framework: z.string().optional(),
238
+ test: z.string().optional(),
239
+ runtime: z.string().optional(),
240
+ orm: z.string().optional(),
241
+ router: z.string().optional(),
242
+ ui: z.string().optional()
243
+ }).passthrough();
244
+ var FrameworkConfigSchema = z.object({
245
+ type: z.string().default("typescript"),
246
+ primary: z.string().optional(),
247
+ router: z.string().default("none"),
248
+ orm: z.string().default("none"),
249
+ ui: z.string().default("none"),
250
+ languages: z.record(z.string(), LanguageFrameworkEntrySchema).optional()
251
+ }).passthrough();
252
+ var VerificationEntrySchema = z.object({
253
+ type: z.string().optional(),
254
+ test: z.string().optional(),
255
+ syntax: z.string().optional(),
256
+ lint: z.string().optional(),
257
+ build: z.string().optional()
258
+ }).passthrough();
259
+ var VerificationConfigSchema = z.record(z.string(), VerificationEntrySchema).optional();
260
+ var CanonicalPathsSchema = z.record(z.string(), z.string()).optional();
261
+ var VerificationTypesSchema = z.record(z.string(), z.string()).optional();
262
+ var DetectionRuleEntrySchema = z.object({
263
+ signals: z.array(z.string()).default([]),
264
+ priority: z.number().optional()
265
+ }).passthrough();
266
+ var DetectionConfigSchema = z.object({
267
+ rules: z.record(
268
+ z.string(),
269
+ // language
270
+ z.record(z.string(), DetectionRuleEntrySchema)
271
+ // framework -> rule entry
272
+ ).optional(),
273
+ signal_weights: z.record(z.string(), z.number()).optional(),
274
+ disable_builtin: z.boolean().optional()
275
+ }).passthrough().optional();
234
276
  var RawConfigSchema = z.object({
277
+ schema_version: z.union([z.literal(1), z.literal(2)]).default(1),
235
278
  project: z.object({
236
279
  name: z.string().default("my-project"),
237
280
  root: z.string().default("auto")
238
281
  }).default({ name: "my-project", root: "auto" }),
239
- framework: z.object({
240
- type: z.string().default("typescript"),
241
- router: z.string().default("none"),
242
- orm: z.string().default("none"),
243
- ui: z.string().default("none")
244
- }).default({ type: "typescript", router: "none", orm: "none", ui: "none" }),
282
+ framework: FrameworkConfigSchema.default({
283
+ type: "typescript",
284
+ router: "none",
285
+ orm: "none",
286
+ ui: "none"
287
+ }),
245
288
  paths: PathsConfigSchema.default({ source: "src", aliases: { "@": "src" } }),
246
289
  toolPrefix: z.string().default("massu"),
247
290
  dbAccessPattern: z.string().optional(),
@@ -256,8 +299,13 @@ var RawConfigSchema = z.object({
256
299
  regression: RegressionConfigSchema,
257
300
  cloud: CloudConfigSchema,
258
301
  conventions: ConventionsConfigSchema,
302
+ autoLearning: AutoLearningConfigSchema,
259
303
  python: PythonConfigSchema,
260
- autoLearning: AutoLearningConfigSchema
304
+ // P2-004 / P2-005 / P2-006 / P2-008: v2 extensions (all optional)
305
+ verification: VerificationConfigSchema,
306
+ canonical_paths: CanonicalPathsSchema,
307
+ verification_types: VerificationTypesSchema,
308
+ detection: DetectionConfigSchema
261
309
  }).passthrough();
262
310
  var _config = null;
263
311
  var _projectRoot = null;
@@ -301,14 +349,47 @@ function getConfig() {
301
349
  const content = readFileSync(configPath, "utf-8");
302
350
  rawYaml = parseYaml(content) ?? {};
303
351
  }
304
- const parsed = RawConfigSchema.parse(rawYaml);
352
+ const result = RawConfigSchema.safeParse(rawYaml);
353
+ if (!result.success) {
354
+ const issues = result.error.issues.map((i) => {
355
+ const path = i.path.length > 0 ? i.path.join(".") : "(root)";
356
+ const received = "received" in i && i.received !== void 0 ? ` (received ${JSON.stringify(i.received)})` : "";
357
+ return ` - ${path}: ${i.message}${received}`;
358
+ }).join("\n");
359
+ throw new Error(
360
+ `Invalid massu.config.yaml at ${configPath}:
361
+ ${issues}
362
+ Hint: run \`massu config refresh\` to regenerate a valid config or fix the listed fields manually.`
363
+ );
364
+ }
365
+ const parsed = result.data;
305
366
  const projectRoot = parsed.project.root === "auto" || !parsed.project.root ? root : resolve(root, parsed.project.root);
367
+ const fw = parsed.framework;
368
+ let router = fw.router;
369
+ let orm = fw.orm;
370
+ let ui = fw.ui;
371
+ if (fw.type === "multi" && fw.primary && fw.languages) {
372
+ const primaryEntry = fw.languages[fw.primary];
373
+ if (primaryEntry) {
374
+ if (router === "none" && primaryEntry.router) router = primaryEntry.router;
375
+ if (orm === "none" && primaryEntry.orm) orm = primaryEntry.orm;
376
+ if (ui === "none" && primaryEntry.ui) ui = primaryEntry.ui;
377
+ }
378
+ }
306
379
  _config = {
380
+ schema_version: parsed.schema_version,
307
381
  project: {
308
382
  name: parsed.project.name,
309
383
  root: projectRoot
310
384
  },
311
- framework: parsed.framework,
385
+ framework: {
386
+ type: fw.type,
387
+ router,
388
+ orm,
389
+ ui,
390
+ primary: fw.primary,
391
+ languages: fw.languages
392
+ },
312
393
  paths: parsed.paths,
313
394
  toolPrefix: parsed.toolPrefix,
314
395
  dbAccessPattern: parsed.dbAccessPattern,
@@ -323,7 +404,12 @@ function getConfig() {
323
404
  regression: parsed.regression,
324
405
  cloud: parsed.cloud,
325
406
  conventions: parsed.conventions,
326
- python: parsed.python
407
+ autoLearning: parsed.autoLearning,
408
+ python: parsed.python,
409
+ verification: parsed.verification,
410
+ canonical_paths: parsed.canonical_paths,
411
+ verification_types: parsed.verification_types,
412
+ detection: parsed.detection
327
413
  };
328
414
  if (!_config.cloud?.apiKey && process.env.MASSU_API_KEY) {
329
415
  _config.cloud = {
@@ -21,7 +21,8 @@ var DomainConfigSchema = z.object({
21
21
  });
22
22
  var PatternRuleConfigSchema = z.object({
23
23
  pattern: z.string().default("**"),
24
- rules: z.array(z.string()).default([])
24
+ rules: z.array(z.string()).default([]),
25
+ language: z.string().optional()
25
26
  });
26
27
  var CostModelSchema = z.object({
27
28
  input_per_million: z.number(),
@@ -233,17 +234,59 @@ var PathsConfigSchema = z.object({
233
234
  components: z.string().optional(),
234
235
  hooks: z.string().optional()
235
236
  });
237
+ var LanguageFrameworkEntrySchema = z.object({
238
+ framework: z.string().optional(),
239
+ test_framework: z.string().optional(),
240
+ test: z.string().optional(),
241
+ runtime: z.string().optional(),
242
+ orm: z.string().optional(),
243
+ router: z.string().optional(),
244
+ ui: z.string().optional()
245
+ }).passthrough();
246
+ var FrameworkConfigSchema = z.object({
247
+ type: z.string().default("typescript"),
248
+ primary: z.string().optional(),
249
+ router: z.string().default("none"),
250
+ orm: z.string().default("none"),
251
+ ui: z.string().default("none"),
252
+ languages: z.record(z.string(), LanguageFrameworkEntrySchema).optional()
253
+ }).passthrough();
254
+ var VerificationEntrySchema = z.object({
255
+ type: z.string().optional(),
256
+ test: z.string().optional(),
257
+ syntax: z.string().optional(),
258
+ lint: z.string().optional(),
259
+ build: z.string().optional()
260
+ }).passthrough();
261
+ var VerificationConfigSchema = z.record(z.string(), VerificationEntrySchema).optional();
262
+ var CanonicalPathsSchema = z.record(z.string(), z.string()).optional();
263
+ var VerificationTypesSchema = z.record(z.string(), z.string()).optional();
264
+ var DetectionRuleEntrySchema = z.object({
265
+ signals: z.array(z.string()).default([]),
266
+ priority: z.number().optional()
267
+ }).passthrough();
268
+ var DetectionConfigSchema = z.object({
269
+ rules: z.record(
270
+ z.string(),
271
+ // language
272
+ z.record(z.string(), DetectionRuleEntrySchema)
273
+ // framework -> rule entry
274
+ ).optional(),
275
+ signal_weights: z.record(z.string(), z.number()).optional(),
276
+ disable_builtin: z.boolean().optional()
277
+ }).passthrough().optional();
236
278
  var RawConfigSchema = z.object({
279
+ schema_version: z.union([z.literal(1), z.literal(2)]).default(1),
237
280
  project: z.object({
238
281
  name: z.string().default("my-project"),
239
282
  root: z.string().default("auto")
240
283
  }).default({ name: "my-project", root: "auto" }),
241
- framework: z.object({
242
- type: z.string().default("typescript"),
243
- router: z.string().default("none"),
244
- orm: z.string().default("none"),
245
- ui: z.string().default("none")
246
- }).default({ type: "typescript", router: "none", orm: "none", ui: "none" }),
284
+ framework: FrameworkConfigSchema.default({
285
+ type: "typescript",
286
+ router: "none",
287
+ orm: "none",
288
+ ui: "none"
289
+ }),
247
290
  paths: PathsConfigSchema.default({ source: "src", aliases: { "@": "src" } }),
248
291
  toolPrefix: z.string().default("massu"),
249
292
  dbAccessPattern: z.string().optional(),
@@ -258,8 +301,13 @@ var RawConfigSchema = z.object({
258
301
  regression: RegressionConfigSchema,
259
302
  cloud: CloudConfigSchema,
260
303
  conventions: ConventionsConfigSchema,
304
+ autoLearning: AutoLearningConfigSchema,
261
305
  python: PythonConfigSchema,
262
- autoLearning: AutoLearningConfigSchema
306
+ // P2-004 / P2-005 / P2-006 / P2-008: v2 extensions (all optional)
307
+ verification: VerificationConfigSchema,
308
+ canonical_paths: CanonicalPathsSchema,
309
+ verification_types: VerificationTypesSchema,
310
+ detection: DetectionConfigSchema
263
311
  }).passthrough();
264
312
  var _config = null;
265
313
  var _projectRoot = null;
@@ -303,14 +351,47 @@ function getConfig() {
303
351
  const content = readFileSync(configPath, "utf-8");
304
352
  rawYaml = parseYaml(content) ?? {};
305
353
  }
306
- const parsed = RawConfigSchema.parse(rawYaml);
354
+ const result = RawConfigSchema.safeParse(rawYaml);
355
+ if (!result.success) {
356
+ const issues = result.error.issues.map((i) => {
357
+ const path = i.path.length > 0 ? i.path.join(".") : "(root)";
358
+ const received = "received" in i && i.received !== void 0 ? ` (received ${JSON.stringify(i.received)})` : "";
359
+ return ` - ${path}: ${i.message}${received}`;
360
+ }).join("\n");
361
+ throw new Error(
362
+ `Invalid massu.config.yaml at ${configPath}:
363
+ ${issues}
364
+ Hint: run \`massu config refresh\` to regenerate a valid config or fix the listed fields manually.`
365
+ );
366
+ }
367
+ const parsed = result.data;
307
368
  const projectRoot = parsed.project.root === "auto" || !parsed.project.root ? root : resolve(root, parsed.project.root);
369
+ const fw = parsed.framework;
370
+ let router = fw.router;
371
+ let orm = fw.orm;
372
+ let ui = fw.ui;
373
+ if (fw.type === "multi" && fw.primary && fw.languages) {
374
+ const primaryEntry = fw.languages[fw.primary];
375
+ if (primaryEntry) {
376
+ if (router === "none" && primaryEntry.router) router = primaryEntry.router;
377
+ if (orm === "none" && primaryEntry.orm) orm = primaryEntry.orm;
378
+ if (ui === "none" && primaryEntry.ui) ui = primaryEntry.ui;
379
+ }
380
+ }
308
381
  _config = {
382
+ schema_version: parsed.schema_version,
309
383
  project: {
310
384
  name: parsed.project.name,
311
385
  root: projectRoot
312
386
  },
313
- framework: parsed.framework,
387
+ framework: {
388
+ type: fw.type,
389
+ router,
390
+ orm,
391
+ ui,
392
+ primary: fw.primary,
393
+ languages: fw.languages
394
+ },
314
395
  paths: parsed.paths,
315
396
  toolPrefix: parsed.toolPrefix,
316
397
  dbAccessPattern: parsed.dbAccessPattern,
@@ -325,7 +406,12 @@ function getConfig() {
325
406
  regression: parsed.regression,
326
407
  cloud: parsed.cloud,
327
408
  conventions: parsed.conventions,
328
- python: parsed.python
409
+ autoLearning: parsed.autoLearning,
410
+ python: parsed.python,
411
+ verification: parsed.verification,
412
+ canonical_paths: parsed.canonical_paths,
413
+ verification_types: parsed.verification_types,
414
+ detection: parsed.detection
329
415
  };
330
416
  if (!_config.cloud?.apiKey && process.env.MASSU_API_KEY) {
331
417
  _config.cloud = {