@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
@@ -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 = {
@@ -952,9 +1038,7 @@ function linkSessionToTask(db, sessionId, taskId) {
952
1038
  }
953
1039
 
954
1040
  // src/hooks/user-prompt.ts
955
- import { existsSync as existsSync3, writeFileSync } from "fs";
956
- import { tmpdir } from "os";
957
- import { join } from "path";
1041
+ import { existsSync as existsSync3 } from "fs";
958
1042
  async function main() {
959
1043
  try {
960
1044
  const input = await readStdin();
@@ -1027,35 +1111,6 @@ async function main() {
1027
1111
  }
1028
1112
  } catch (_memoryNagErr) {
1029
1113
  }
1030
- try {
1031
- const failureKeywords = [
1032
- "bug",
1033
- "broken",
1034
- "crash",
1035
- "error",
1036
- "fail",
1037
- "fix",
1038
- "wrong",
1039
- "missing",
1040
- "undefined",
1041
- "null",
1042
- "exception",
1043
- "stack trace",
1044
- "regression",
1045
- "revert",
1046
- "doesn't work",
1047
- "not working",
1048
- "stopped working",
1049
- "broke"
1050
- ];
1051
- const promptLower = prompt.toLowerCase();
1052
- const matched = failureKeywords.filter((kw) => promptLower.includes(kw));
1053
- if (matched.length > 0) {
1054
- const contextFile = join(tmpdir(), `massu-failure-context-${session_id.slice(0, 8)}-${Date.now()}`);
1055
- writeFileSync(contextFile, matched.join(" "), "utf-8");
1056
- }
1057
- } catch {
1058
- }
1059
1114
  } finally {
1060
1115
  db.close();
1061
1116
  }
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@massu/core",
3
- "version": "0.9.2",
3
+ "version": "1.1.0",
4
4
  "type": "module",
5
- "description": "AI Engineering Governance MCP Server - Session memory, knowledge system, feature registry, code intelligence, rule enforcement, auto-learning pipeline, tiered tooling (12 free / 72 total), 55+ workflow commands, 15 agents, 20+ patterns",
5
+ "description": "AI Engineering Governance MCP Server - Session memory, knowledge system, feature registry, code intelligence, rule enforcement, tiered tooling (12 free / 72 total), 55+ workflow commands, 11 agents, 20+ patterns",
6
6
  "main": "src/server.ts",
7
7
  "bin": {
8
8
  "massu": "./dist/cli.js"
@@ -16,7 +16,10 @@
16
16
  "prepublishOnly": "bash ../../scripts/prepublish-check.sh && npm run build"
17
17
  },
18
18
  "dependencies": {
19
+ "@clack/prompts": "^0.9.1",
19
20
  "better-sqlite3": "^12.6.2",
21
+ "fast-glob": "^3.3.0",
22
+ "smol-toml": "^1.3.0",
20
23
  "yaml": "^2.4.0",
21
24
  "zod": "^3.23.0"
22
25
  },
@@ -29,13 +32,20 @@
29
32
  },
30
33
  "files": [
31
34
  "src/**/*",
32
- "!src/__tests__/**",
35
+ "!src/**/__tests__/**",
36
+ "!src/**/*.test.ts",
37
+ "!src/**/*.spec.ts",
38
+ "!**/.build/**",
39
+ "!**/.swiftpm/**",
40
+ "!**/node_modules/**",
41
+ "!**/__pycache__/**",
33
42
  "dist/**/*",
34
43
  "commands/**/*",
35
44
  "agents/**/*",
36
45
  "patterns/**/*",
37
46
  "protocols/**/*",
38
47
  "reference/**/*",
48
+ "templates/**/*",
39
49
  "LICENSE"
40
50
  ],
41
51
  "keywords": [
@@ -52,9 +52,9 @@ Security blocking -> advisory warnings -> matcher-specific -> observability.
52
52
 
53
53
  ---
54
54
 
55
- ## PostToolUse (14 hooks)
55
+ ## PostToolUse (11 hooks)
56
56
 
57
- Security scan -> immediate feedback -> context tracking -> fix detection -> incident capture -> pipeline triggers -> memory sync -> observability.
57
+ Security scan -> immediate feedback -> context tracking -> incident capture -> memory sync -> observability.
58
58
 
59
59
  | position: 1 | CI monitor | standard | Bash(git push) -- immediate push feedback |
60
60
  |---|---|---|---|
@@ -62,23 +62,17 @@ Security scan -> immediate feedback -> context tracking -> fix detection -> inci
62
62
  | position: 3 | `pattern-feedback.sh` | standard | Edit\|Write -- immediate pattern violation feedback |
63
63
  | position: 4 | `post-edit-context.js` | strict | Edit\|Write -- detailed semantic analysis |
64
64
  | position: 5 | `post-tool-use.js` | standard | Edit\|Write\|Bash -- structured context tracking |
65
- | position: 6 | `fix-detector.js` | standard | Edit\|Write -- detect bug fixes via git diff heuristics |
66
- | position: 7 | `auto-ingest-incident.sh` | strict | Edit\|Write -- auto-capture incident patterns |
67
- | position: 8 | `incident-pipeline.js` | standard | Write -- trigger rule derivation on incident report writes |
68
- | position: 9 | `rule-enforcement-pipeline.js` | standard | Write -- trigger enforcement on prevention rule writes |
69
- | position: 10 | `memory-auto-ingest.sh` | standard | Write -- auto-sync memory files to codegraph SQLite DB |
70
- | position: 11 | `validate-deliverables.sh` | strict | Bash\|Edit\|Write -- deliverable validation |
71
- | position: 12 | `pattern-scanner.sh --single-file` | strict | Edit\|Write -- per-file pattern scan |
72
- | position: 13 | `mcp-usage-tracker.sh` | strict | MCP tools -- append-only MCP audit log |
73
- | position: 14 | `compaction-advisor.sh` | standard | Bash\|Edit\|Write\|Read\|Grep\|Glob -- context tracking, widest matcher |
65
+ | position: 6 | `auto-ingest-incident.sh` | strict | Edit\|Write -- auto-capture incident patterns |
66
+ | position: 7 | `memory-auto-ingest.sh` | standard | Write -- auto-sync memory files to codegraph SQLite DB |
67
+ | position: 8 | `validate-deliverables.sh` | strict | Bash\|Edit\|Write -- deliverable validation |
68
+ | position: 9 | `pattern-scanner.sh --single-file` | strict | Edit\|Write -- per-file pattern scan |
69
+ | position: 10 | `mcp-usage-tracker.sh` | strict | MCP tools -- append-only MCP audit log |
70
+ | position: 11 | `compaction-advisor.sh` | standard | Bash\|Edit\|Write\|Read\|Grep\|Glob -- context tracking, widest matcher |
74
71
 
75
72
  **Dependencies**:
76
73
  - `output-secret-filter.sh` MUST run before any feedback hooks -- security first
77
74
  - `pattern-feedback.sh` before `post-tool-use.js` -- immediate feedback before tracking
78
- - `fix-detector.js` after `post-tool-use.js` -- needs structured tracking context
79
- - `incident-pipeline.js` after `auto-ingest-incident.sh` -- incident must be captured first
80
- - `rule-enforcement-pipeline.js` after `incident-pipeline.js` -- rule derivation before enforcement
81
- - `memory-auto-ingest.sh` runs after pipeline hooks -- memory sync is data-writing, before validation
75
+ - `memory-auto-ingest.sh` runs after incident capture -- memory sync is data-writing, before validation
82
76
  - `compaction-advisor.sh` MUST be last -- widest matcher, just counts tool calls
83
77
 
84
78
  ---
@@ -111,23 +105,21 @@ Quick state capture -> full DB snapshot.
111
105
 
112
106
  ---
113
107
 
114
- ## Stop (8 hooks)
108
+ ## Stop (7 hooks)
115
109
 
116
- Session summary -> auto-learning check -> warnings -> memory extraction -> review -> validation.
110
+ Session summary -> warnings -> memory extraction -> review -> validation.
117
111
 
118
112
  | position: 1 | `session-end.js` | standard | Write session summary to memory DB |
119
113
  |---|---|---|---|
120
- | position: 2 | `auto-learning-pipeline.js` | standard | Enforce fix→incident→rule→enforcement pipeline completion |
121
- | position: 3 | Uncommitted changes warning | standard (inline) | Alert user about unstaged work |
122
- | position: 4 | `memory-auto-extract.sh` | standard | Auto-extract memories from DB observations |
123
- | position: 5 | `auto-review-on-stop.sh` | strict | Automated code review of session changes |
124
- | position: 6 | `surface-review-findings.sh` | strict | Display review findings to user |
125
- | position: 7 | `validate-deliverables.sh` | strict | Final deliverable validation |
126
- | position: 8 | `pattern-extractor.sh` | advisory | Extract new patterns from session |
114
+ | position: 2 | Uncommitted changes warning | standard (inline) | Alert user about unstaged work |
115
+ | position: 3 | `memory-auto-extract.sh` | standard | Auto-extract memories from DB observations |
116
+ | position: 4 | `auto-review-on-stop.sh` | strict | Automated code review of session changes |
117
+ | position: 5 | `surface-review-findings.sh` | strict | Display review findings to user |
118
+ | position: 6 | `validate-deliverables.sh` | strict | Final deliverable validation |
119
+ | position: 7 | `pattern-extractor.sh` | advisory | Extract new patterns from session |
127
120
 
128
121
  **Dependencies**:
129
122
  - `session-end.js` MUST be position 1 -- writes DB data that `memory-auto-extract.sh` reads
130
- - `auto-learning-pipeline.js` MUST run early -- needs to output mandatory instructions before session ends
131
123
  - `memory-auto-extract.sh` MUST come after `session-end.js` -- depends on DB observations
132
124
  - `surface-review-findings.sh` MUST come after `auto-review-on-stop.sh` -- displays its output
133
125
  - `pattern-extractor.sh` runs last -- advisory tier (skipped in minimal/standard profiles)
package/src/cli.ts CHANGED
@@ -52,6 +52,10 @@ async function main(): Promise<void> {
52
52
  await runValidateConfig();
53
53
  break;
54
54
  }
55
+ case 'config': {
56
+ await handleConfigSubcommand(args.slice(1));
57
+ break;
58
+ }
55
59
  case '--help':
56
60
  case '-h': {
57
61
  printHelp();
@@ -70,6 +74,56 @@ async function main(): Promise<void> {
70
74
  }
71
75
  }
72
76
 
77
+ async function handleConfigSubcommand(configArgs: string[]): Promise<void> {
78
+ const sub = configArgs[0];
79
+ const flags = new Set(configArgs.slice(1));
80
+ switch (sub) {
81
+ case 'refresh': {
82
+ const { runConfigRefresh } = await import('./commands/config-refresh.ts');
83
+ const result = await runConfigRefresh({ dryRun: flags.has('--dry-run') });
84
+ process.exit(result.exitCode);
85
+ return;
86
+ }
87
+ case 'validate': {
88
+ const { runValidateConfig } = await import('./commands/doctor.ts');
89
+ await runValidateConfig();
90
+ return;
91
+ }
92
+ case 'upgrade': {
93
+ const { runConfigUpgrade } = await import('./commands/config-upgrade.ts');
94
+ const result = await runConfigUpgrade({
95
+ rollback: flags.has('--rollback'),
96
+ ci: flags.has('--ci') || flags.has('--yes'),
97
+ });
98
+ process.exit(result.exitCode);
99
+ return;
100
+ }
101
+ case 'doctor': {
102
+ const { runDoctor } = await import('./commands/doctor.ts');
103
+ await runDoctor();
104
+ return;
105
+ }
106
+ case 'check-drift': {
107
+ const { runConfigCheckDrift } = await import('./commands/config-check-drift.ts');
108
+ const result = await runConfigCheckDrift({ verbose: flags.has('--verbose') });
109
+ process.exit(result.exitCode);
110
+ return;
111
+ }
112
+ case '--help':
113
+ case '-h':
114
+ case undefined: {
115
+ printConfigHelp();
116
+ return;
117
+ }
118
+ default: {
119
+ process.stderr.write(`massu: unknown config subcommand: ${sub}\n`);
120
+ printConfigHelp();
121
+ process.exit(1);
122
+ return;
123
+ }
124
+ }
125
+ }
126
+
73
127
  function printHelp(): void {
74
128
  console.log(`
75
129
  Massu AI - Engineering Governance Platform
@@ -82,19 +136,44 @@ Commands:
82
136
  doctor Check installation health
83
137
  install-hooks Install/update Claude Code hooks
84
138
  install-commands Install/update slash commands
85
- validate-config Validate massu.config.yaml
139
+ validate-config Validate massu.config.yaml (alias: config validate)
140
+ config <sub> Config lifecycle: refresh | validate | upgrade | doctor | check-drift
86
141
 
87
142
  Options:
88
143
  --help, -h Show this help message
89
144
  --version, -v Show version
90
145
 
91
146
  Getting started:
92
- npx massu init # Full setup in one command
147
+ npx massu init # Full setup in one command
148
+ npx massu init --help # Show all init options (--ci, --force, --template)
149
+ npx massu config --help # Show config subcommands
93
150
 
94
151
  Documentation: https://massu.ai/docs
95
152
  `);
96
153
  }
97
154
 
155
+ function printConfigHelp(): void {
156
+ console.log(`
157
+ massu config <subcommand>
158
+
159
+ Subcommands:
160
+ refresh Re-run detection and apply changes to massu.config.yaml.
161
+ --dry-run Print diff and exit without writing.
162
+ validate Validate massu.config.yaml (alias of \`massu validate-config\`).
163
+ upgrade Migrate a v1 config to schema_version=2.
164
+ --rollback Restore from .bak file.
165
+ --ci, --yes Non-interactive mode (no prompts).
166
+ doctor Run the full health check (alias of \`massu doctor\`).
167
+ check-drift CI-safe drift gate; exits 1 on drift.
168
+ --verbose Print detailed changes to stdout.
169
+
170
+ Examples:
171
+ npx massu config refresh --dry-run
172
+ npx massu config upgrade --ci
173
+ npx massu config check-drift --verbose
174
+ `);
175
+ }
176
+
98
177
  function printVersion(): void {
99
178
  try {
100
179
  const pkg = JSON.parse(readFileSync(resolve(__dirname, '../package.json'), 'utf-8'));
@@ -0,0 +1,132 @@
1
+ // Copyright (c) 2026 Massu. All rights reserved.
2
+ // Licensed under BSL 1.1 - see LICENSE file for details.
3
+
4
+ /**
5
+ * `massu config check-drift` — CI-safe drift gate.
6
+ *
7
+ * Reads massu.config.yaml, runs detection, and compares the result with both
8
+ * the stored fingerprint (if present) and the structural detectDrift() check.
9
+ *
10
+ * Flags:
11
+ * --verbose Emit the full changes[] to stdout (field: before -> after).
12
+ *
13
+ * Exit codes:
14
+ * 0 no drift
15
+ * 1 drift detected
16
+ * 2 config missing or unparseable
17
+ */
18
+
19
+ import { existsSync, readFileSync } from 'fs';
20
+ import { resolve } from 'path';
21
+ import { parse as parseYaml } from 'yaml';
22
+ import { runDetection } from '../detect/index.ts';
23
+ import { computeFingerprint, detectDrift, type DriftChange } from '../detect/drift.ts';
24
+ import type { AnyConfig } from '../detect/migrate.ts';
25
+
26
+ export interface ConfigCheckDriftOptions {
27
+ verbose?: boolean;
28
+ cwd?: string;
29
+ silent?: boolean;
30
+ }
31
+
32
+ export interface ConfigCheckDriftResult {
33
+ exitCode: 0 | 1 | 2;
34
+ drifted: boolean;
35
+ changes: DriftChange[];
36
+ storedFingerprint: string | null;
37
+ currentFingerprint: string | null;
38
+ message?: string;
39
+ }
40
+
41
+ function renderChanges(changes: DriftChange[]): string {
42
+ if (changes.length === 0) return '(none)\n';
43
+ return changes
44
+ .map((c) => ` ${c.field}: ${JSON.stringify(c.before)} -> ${JSON.stringify(c.after)}`)
45
+ .join('\n') + '\n';
46
+ }
47
+
48
+ export async function runConfigCheckDrift(
49
+ opts: ConfigCheckDriftOptions = {}
50
+ ): Promise<ConfigCheckDriftResult> {
51
+ const cwd = opts.cwd ?? process.cwd();
52
+ const configPath = resolve(cwd, 'massu.config.yaml');
53
+ const log = opts.silent ? () => {} : (s: string) => process.stdout.write(s);
54
+ const err = opts.silent ? () => {} : (s: string) => process.stderr.write(s);
55
+
56
+ if (!existsSync(configPath)) {
57
+ const message = 'massu.config.yaml not found. Run: npx massu init';
58
+ err(message + '\n');
59
+ return {
60
+ exitCode: 2,
61
+ drifted: false,
62
+ changes: [],
63
+ storedFingerprint: null,
64
+ currentFingerprint: null,
65
+ message,
66
+ };
67
+ }
68
+
69
+ let config: AnyConfig;
70
+ try {
71
+ const content = readFileSync(configPath, 'utf-8');
72
+ const parsed = parseYaml(content);
73
+ if (!parsed || typeof parsed !== 'object') {
74
+ throw new Error('config is not a YAML object');
75
+ }
76
+ config = parsed as AnyConfig;
77
+ } catch (e) {
78
+ const message = `Failed to parse massu.config.yaml: ${e instanceof Error ? e.message : String(e)}`;
79
+ err(message + '\n');
80
+ return {
81
+ exitCode: 2,
82
+ drifted: false,
83
+ changes: [],
84
+ storedFingerprint: null,
85
+ currentFingerprint: null,
86
+ message,
87
+ };
88
+ }
89
+
90
+ const detection = await runDetection(cwd);
91
+ const currentFp = computeFingerprint(detection);
92
+ const storedFp =
93
+ typeof (config.detection as Record<string, unknown> | undefined)?.fingerprint === 'string'
94
+ ? ((config.detection as Record<string, unknown>).fingerprint as string)
95
+ : null;
96
+
97
+ const report = detectDrift(config, detection);
98
+ const fingerprintDrift = storedFp !== null && storedFp !== currentFp;
99
+ const drifted = report.drifted || fingerprintDrift;
100
+
101
+ if (!drifted) {
102
+ log('No drift detected.\n');
103
+ if (opts.verbose) {
104
+ log(`Fingerprint: ${currentFp}\n`);
105
+ }
106
+ return {
107
+ exitCode: 0,
108
+ drifted: false,
109
+ changes: report.changes,
110
+ storedFingerprint: storedFp,
111
+ currentFingerprint: currentFp,
112
+ };
113
+ }
114
+
115
+ err('Config drift detected; run `npx massu config refresh` to update.\n');
116
+ if (opts.verbose) {
117
+ if (storedFp !== null) {
118
+ log(`Fingerprint: ${storedFp.slice(0, 16)} -> ${currentFp.slice(0, 16)}\n`);
119
+ } else {
120
+ log(`Fingerprint (new): ${currentFp.slice(0, 16)}\n`);
121
+ }
122
+ log('Changes:\n');
123
+ log(renderChanges(report.changes));
124
+ }
125
+ return {
126
+ exitCode: 1,
127
+ drifted: true,
128
+ changes: report.changes,
129
+ storedFingerprint: storedFp,
130
+ currentFingerprint: currentFp,
131
+ };
132
+ }