@massu/core 1.10.8 → 1.12.1

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 (178) hide show
  1. package/CHANGELOG.md +1334 -0
  2. package/dist/adapter.js +6 -4
  3. package/dist/cli.js +581 -448
  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 +67 -19
  16. package/dist/hooks/pre-compact.js +28 -3
  17. package/dist/hooks/pre-delete-check.js +53 -27
  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 +31 -6
  23. package/dist/hooks/session-start.js +8709 -8459
  24. package/dist/hooks/user-prompt.js +25 -1
  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/framework-supports.ts +0 -85
  110. package/src/lib/gitToplevel.ts +0 -22
  111. package/src/lib/hook-registry.ts +0 -43
  112. package/src/lib/installLock.ts +0 -66
  113. package/src/lib/memory-path.ts +0 -49
  114. package/src/lib/pidLiveness.ts +0 -67
  115. package/src/lib/settings-local.ts +0 -110
  116. package/src/lib/sql-table-names.ts +0 -71
  117. package/src/license.ts +0 -547
  118. package/src/lsp/auto-detect.ts +0 -98
  119. package/src/lsp/client.ts +0 -776
  120. package/src/lsp/enrich.ts +0 -127
  121. package/src/lsp/types.ts +0 -221
  122. package/src/memory-db.ts +0 -1614
  123. package/src/memory-file-ingest.ts +0 -128
  124. package/src/memory-tools.ts +0 -424
  125. package/src/middleware-tree.ts +0 -71
  126. package/src/observability-tools.ts +0 -343
  127. package/src/observation-extractor.ts +0 -418
  128. package/src/page-deps.ts +0 -285
  129. package/src/permissions.ts +0 -422
  130. package/src/prompt-analyzer.ts +0 -332
  131. package/src/python/coupling-detector.ts +0 -125
  132. package/src/python/domain-enforcer.ts +0 -84
  133. package/src/python/impact-analyzer.ts +0 -96
  134. package/src/python/import-parser.ts +0 -244
  135. package/src/python/import-resolver.ts +0 -136
  136. package/src/python/migration-indexer.ts +0 -116
  137. package/src/python/migration-parser.ts +0 -332
  138. package/src/python/model-indexer.ts +0 -71
  139. package/src/python/model-parser.ts +0 -279
  140. package/src/python/route-indexer.ts +0 -59
  141. package/src/python/route-parser.ts +0 -317
  142. package/src/python-tools.ts +0 -630
  143. package/src/regression-detector.ts +0 -319
  144. package/src/rules.ts +0 -57
  145. package/src/schema-mapper.ts +0 -232
  146. package/src/security/adapter-origin.ts +0 -130
  147. package/src/security/adapter-verifier.ts +0 -319
  148. package/src/security/atomic-write.ts +0 -164
  149. package/src/security/fetcher.ts +0 -200
  150. package/src/security/install-tracking.ts +0 -319
  151. package/src/security/license-pubkey.generated.ts +0 -30
  152. package/src/security/license-response-verifier.ts +0 -149
  153. package/src/security/local-fingerprint.ts +0 -225
  154. package/src/security/manifest-cache.ts +0 -333
  155. package/src/security/manifest-schema.ts +0 -129
  156. package/src/security/registry-pubkey.generated.ts +0 -35
  157. package/src/security/telemetry.ts +0 -320
  158. package/src/security-scorer.ts +0 -405
  159. package/src/security-utils.ts +0 -133
  160. package/src/sentinel-db.ts +0 -580
  161. package/src/sentinel-scanner.ts +0 -406
  162. package/src/sentinel-tools.ts +0 -513
  163. package/src/sentinel-types.ts +0 -140
  164. package/src/server-dispatch.ts +0 -225
  165. package/src/server.ts +0 -107
  166. package/src/session-archiver.ts +0 -111
  167. package/src/session-state-generator.ts +0 -174
  168. package/src/team-knowledge.ts +0 -407
  169. package/src/tool-db-needs.ts +0 -232
  170. package/src/tools.ts +0 -1206
  171. package/src/transcript-parser.ts +0 -458
  172. package/src/trpc-index.ts +0 -215
  173. package/src/validate-features-runner.ts +0 -107
  174. package/src/validation-engine.ts +0 -358
  175. package/src/watch/daemon.ts +0 -385
  176. package/src/watch/lockfile-detector.ts +0 -65
  177. package/src/watch/paths.ts +0 -279
  178. package/src/watch/state.ts +0 -178
@@ -348,6 +348,22 @@ var RawConfigSchema = z.object({
348
348
  accessScopes: z.array(z.string()).optional(),
349
349
  domains: z.array(DomainConfigSchema).default([]),
350
350
  rules: z.array(PatternRuleConfigSchema).default([]),
351
+ // P-M-036 (plan-stage-d-medium-sweep): customer-authored CR-style
352
+ // governance rules. DISTINCT from `rules:` above (path-scoped lint hints
353
+ // used by pattern-scanner). At config-refresh time these entries are
354
+ // loaded into the `knowledge_rules` SQLite table with
355
+ // `source = 'customer-config'` so `massu_knowledge_rule` and the
356
+ // governance docs surface customer-defined rules alongside framework CRs.
357
+ governance_rules: z.array(
358
+ z.object({
359
+ id: z.string().min(1, "governance_rules[].id is required"),
360
+ title: z.string().min(1, "governance_rules[].title is required"),
361
+ description: z.string().min(1, "governance_rules[].description is required"),
362
+ vr_type: z.string().default("VR-CUSTOM"),
363
+ reference_path: z.string().optional(),
364
+ severity: z.enum(["critical", "high", "medium", "low", "info"]).default("medium")
365
+ }).passthrough()
366
+ ).default([]),
351
367
  analytics: AnalyticsConfigSchema,
352
368
  governance: GovernanceConfigSchema,
353
369
  security: SecurityConfigSchema,
@@ -466,6 +482,8 @@ Hint: run \`massu config refresh\` to regenerate a valid config or fix the liste
466
482
  accessScopes: parsed.accessScopes,
467
483
  domains: parsed.domains,
468
484
  rules: parsed.rules,
485
+ // P-M-036: customer-authored CR-style governance rules.
486
+ governance_rules: parsed.governance_rules,
469
487
  analytics: parsed.analytics,
470
488
  governance: parsed.governance,
471
489
  security: parsed.security,
@@ -1018,6 +1036,12 @@ function initMemorySchema(db) {
1018
1036
  features TEXT DEFAULT '[]'
1019
1037
  );
1020
1038
  `);
1039
+ const licenseCacheCols = db.prepare(`PRAGMA table_info(license_cache)`).all();
1040
+ if (!licenseCacheCols.some((c) => c.name === "signed_payload_json")) {
1041
+ db.exec(
1042
+ `ALTER TABLE license_cache ADD COLUMN signed_payload_json TEXT NOT NULL DEFAULT ''`
1043
+ );
1044
+ }
1021
1045
  db.exec(`
1022
1046
  CREATE TABLE IF NOT EXISTS failure_classes (
1023
1047
  id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -1148,7 +1172,7 @@ async function main() {
1148
1172
  try {
1149
1173
  const placeholders = fileRefs.map(() => "?").join(",");
1150
1174
  const matches = kdb.prepare(
1151
- `SELECT DISTINCT file_path FROM knowledge_documents WHERE file_path IN (${placeholders})`
1175
+ `SELECT DISTINCT file_path FROM knowledge_documents WHERE file_path IN (${placeholders}) LIMIT 10000`
1152
1176
  ).all(...fileRefs);
1153
1177
  if (matches.length > 0) {
1154
1178
  addObservation(
package/package.json CHANGED
@@ -1,11 +1,16 @@
1
1
  {
2
2
  "name": "@massu/core",
3
- "version": "1.10.8",
3
+ "version": "1.12.1",
4
4
  "type": "module",
5
5
  "description": "AI Engineering Governance MCP Server - Session memory, knowledge system, feature registry, code intelligence, rule enforcement, tiered tooling (12 free / 73 total), 59 workflow commands, 11 agents, 20+ patterns",
6
- "main": "src/server.ts",
6
+ "main": "./dist/cli.js",
7
+ "types": "./dist/adapter.d.ts",
7
8
  "exports": {
8
- ".": "./src/server.ts",
9
+ ".": {
10
+ "types": "./dist/adapter.d.ts",
11
+ "import": "./dist/cli.js",
12
+ "default": "./dist/cli.js"
13
+ },
9
14
  "./adapter": {
10
15
  "types": "./dist/adapter.d.ts",
11
16
  "import": "./dist/adapter.js",
@@ -22,18 +27,16 @@
22
27
  "build:adapter-types": "tsc -p tsconfig.adapter-types.json",
23
28
  "build:adapter-subpath": "tsx scripts/bundle-adapters.ts --subpath-only",
24
29
  "build:bundle-adapters": "tsx scripts/bundle-adapters.ts",
25
- "build:cli": "esbuild --bundle --platform=node --format=esm --outfile=dist/cli.js src/cli.ts --external:better-sqlite3 --external:yaml --external:zod --external:chokidar --external:proper-lockfile --external:fsevents --external:web-tree-sitter --external:tweetnacl --external:tar --external:smol-toml --external:vscode-languageserver-protocol --external:@massu/adapter-rails --external:@massu/adapter-phoenix --external:@massu/adapter-aspnet --external:@massu/adapter-spring --external:@massu/adapter-go-chi --banner:js='#!/usr/bin/env node\nimport{createRequire as __cr}from\"module\";const require=__cr(import.meta.url);'",
26
- "build:hooks": "esbuild --bundle --platform=node --format=esm --outdir=dist/hooks src/hooks/*.ts --external:better-sqlite3 --external:yaml --external:zod --external:chokidar --external:proper-lockfile --external:fsevents --external:web-tree-sitter --external:tweetnacl --external:tar --external:smol-toml --external:vscode-languageserver-protocol --external:@massu/adapter-rails --external:@massu/adapter-phoenix --external:@massu/adapter-aspnet --external:@massu/adapter-spring --external:@massu/adapter-go-chi --banner:js='import{createRequire as __cr}from\"module\";const require=__cr(import.meta.url);'",
27
- "prepublishOnly": "bash ../../scripts/prepublish-check.sh && node ../../scripts/bundle-pubkey.mjs && node ../../scripts/bundle-license-pubkey.mjs && npm run build",
30
+ "build:cli": "esbuild --bundle --platform=node --format=esm --outfile=dist/cli.js src/cli.ts --external:better-sqlite3 --external:yaml --external:zod --external:chokidar --external:proper-lockfile --external:fsevents --external:web-tree-sitter --external:tweetnacl --external:tar --external:smol-toml --external:vscode-languageserver-protocol --external:@massu/adapter-rails --external:@massu/adapter-spring --banner:js='#!/usr/bin/env node\nimport{createRequire as __cr}from\"module\";const require=__cr(import.meta.url);'",
31
+ "build:hooks": "esbuild --bundle --platform=node --format=esm --outdir=dist/hooks src/hooks/*.ts --external:better-sqlite3 --external:yaml --external:zod --external:chokidar --external:proper-lockfile --external:fsevents --external:web-tree-sitter --external:tweetnacl --external:tar --external:smol-toml --external:vscode-languageserver-protocol --external:@massu/adapter-rails --external:@massu/adapter-spring --banner:js='import{createRequire as __cr}from\"module\";const require=__cr(import.meta.url);'",
32
+ "prepublishOnly": "bash ../../scripts/prepublish-check.sh && node ../../scripts/bundle-pubkey.mjs && node ../../scripts/bundle-license-pubkey.mjs && cp ../../CHANGELOG.md ./CHANGELOG.md && npm run build",
28
33
  "bench:watch": "tsx test/perf/watch-benchmark.ts"
29
34
  },
30
35
  "dependencies": {
31
36
  "@clack/prompts": "^0.9.1",
32
- "@massu/adapter-aspnet": "^1.0.0",
33
- "@massu/adapter-go-chi": "^1.0.0",
34
- "@massu/adapter-phoenix": "^1.0.0",
35
37
  "@massu/adapter-rails": "^1.0.0",
36
38
  "@massu/adapter-spring": "^1.0.0",
39
+ "@massu/types": "1.12.0",
37
40
  "better-sqlite3": "^12.6.2",
38
41
  "chokidar": "^3.6.0",
39
42
  "fast-glob": "^3.3.0",
@@ -55,23 +58,17 @@
55
58
  "vitest": "^4.0.18"
56
59
  },
57
60
  "files": [
58
- "src/**/*",
59
- "!src/**/__tests__/**",
60
- "!src/**/*.test.ts",
61
- "!src/**/*.spec.ts",
62
- "!**/.build/**",
63
- "!**/.swiftpm/**",
64
- "!**/node_modules/**",
65
- "!**/__pycache__/**",
66
61
  "dist/**/*",
67
62
  "commands/**/*",
63
+ "!commands/README.md",
68
64
  "agents/**/*",
69
65
  "docs/**/*",
70
66
  "patterns/**/*",
71
67
  "protocols/**/*",
72
68
  "reference/**/*",
73
69
  "templates/**/*",
74
- "LICENSE"
70
+ "LICENSE",
71
+ "CHANGELOG.md"
75
72
  ],
76
73
  "keywords": [
77
74
  "mcp",
@@ -1,134 +0,0 @@
1
- # Massu Slash-Command Templates
2
-
3
- This directory ships the slash commands installed into a consumer project's `<claudeDirName>/commands/` (default `.claude/commands/`) when the consumer runs `npx @massu/core install-commands` (or `npx @massu/core init`, which calls install-commands as part of its flow).
4
-
5
- As of `@massu/core@1.3.0`, command files are **stack-aware**: a template can ship one or more language-specific variants alongside the default, and the installer picks the variant that matches the consumer's `massu.config.yaml`. Local edits are preserved across reinstalls via a manifest written to `<claudeDirName>/.massu/install-manifest.json`.
6
-
7
- This README covers:
8
-
9
- 1. The variant filename convention.
10
- 2. The variant-resolution algorithm (priority order + tie-break).
11
- 3. The local-edit-protection manifest.
12
- 4. The `npx @massu/core show-template <name>` debugging command.
13
-
14
- ## 1. Variant filename convention
15
-
16
- ```
17
- <base>.md # default — used when no variant matches
18
- <base>.python.md # FastAPI / Django / generic Python (one-axis)
19
- <base>.python-fastapi.md # Python + FastAPI (two-axis: lang + sub-framework)
20
- <base>.python-django.md # Python + Django (two-axis: lang + sub-framework)
21
- <base>.swift.md # SwiftUI / iOS / visionOS
22
- <base>.rust.md # axum / actix / generic Rust
23
- <base>.typescript.md # reserved for future use; currently no variants ship
24
- ```
25
-
26
- The two-axis form (`<base>.<lang>-<framework>.md`) is tried BEFORE the one-axis (`<base>.<lang>.md`) form. It is selected when the consumer's config declares `framework.languages.<lang>.framework = "<sub-framework>"` (e.g. `fastapi` or `django`).
27
-
28
- The variant convention applies **only at the top level** of `packages/core/commands/`. Files inside subdirectories (e.g., `_shared-references/`, `massu-loop/references/`) are copied recursively as-is — no variant resolution, no dot-skip filtering. Future authors can use dotted filenames in nested dirs without losing them to the variant filter.
29
-
30
- The variant convention is **opt-in**: a template that does NOT ship any `<base>.<variant>.md` siblings is variant-agnostic and the unsuffixed `<base>.md` is used everywhere.
31
-
32
- ## 2. Variant-resolution algorithm
33
-
34
- Given a base template name `B` and the consumer's `framework` config from `massu.config.yaml`:
35
-
36
- 1. **Build the candidate list `V`** in priority order:
37
- 1. `framework.primary` (or `framework.type` if `primary` is undefined). Skipped if the value is `"multi"`.
38
- 2. Each declared `framework.languages.<lang>` entry whose `framework` field is a non-empty string, in **YAML declaration order** (first-declared wins on ties).
39
- 3. **Passthrough fallback**: well-known top-level `framework.<lang>` blocks (typescript / javascript / python / swift / rust / go) with a non-empty `framework` field, in fixed order, excluding any language already covered. This is what lets a project that declares `framework.swift` at the top level (alongside `framework.languages.python`) still pick up the `.swift.md` variant when the languages block doesn't contain swift.
40
- 4. The unsuffixed default (`""`) as the last fallback.
41
- 2. **Probe disk** in order: for each candidate `c`, check whether `<base>.<c>.md` exists in the bundled commands directory.
42
- 3. **Return**:
43
- - First hit → copy that file.
44
- - No hit AND `framework.type === "multi"` AND `framework.primary` is undefined → write a one-line warning to stderr and copy the unsuffixed default.
45
- - No hit otherwise → skip the file (this only happens if the base template was deleted and only orphan variants remain, which the Phase 0 audit prevents).
46
-
47
- The target filename in the consumer dir is always the BASE name (`<base>.md`) — variant suffixes are an internal package detail.
48
-
49
- ### Tie-break — which variant wins?
50
-
51
- If a consumer declares both `python` and `swift` in `framework.languages` AND a template ships both `.python.md` and `.swift.md`, the variant declared FIRST in `framework.languages` wins. Consumers control this by reordering keys in `massu.config.yaml`. Passthrough-fallback entries (rule 1.3) are appended AFTER all `framework.languages` entries.
52
-
53
- There is no per-template override mechanism. If you need one, file an issue.
54
-
55
- ## 3. Local-edit protection — the manifest
56
-
57
- `@massu/core@1.3.0` introduces a manifest at:
58
-
59
- ```
60
- <consumer-root>/<claudeDirName>/.massu/install-manifest.json
61
- ```
62
-
63
- (where `claudeDirName` is the value of `conventions.claudeDirName` in `massu.config.yaml`, defaulting to `.claude`).
64
-
65
- Each entry maps an asset-relative path (e.g., `commands/massu-scaffold-router.md`) to the SHA-256 of its content at the last successful install.
66
-
67
- ### What the installer does on each run
68
-
69
- For each file `<asset>` to be installed, three hashes are computed:
70
-
71
- | Hash | What it represents |
72
- |------|--------------------|
73
- | `sourceHash` | The bundled upstream content NOW. |
74
- | `existingHash` | The current consumer file content. `undefined` if missing. |
75
- | `lastInstalledHash` | The hash recorded in the manifest. `undefined` on first install. |
76
-
77
- Decision matrix:
78
-
79
- | Condition | Action | Counter |
80
- |-----------|--------|---------|
81
- | target missing | write upstream, record `sourceHash` in manifest | `installed` |
82
- | `existingHash === sourceHash` | already in sync; record `sourceHash` (covers manifest healing) | `skipped` |
83
- | `lastInstalledHash` undefined AND `existingHash !== sourceHash` | first-install heuristic: keep the consumer file, record `existingHash`, print a one-line notice | `kept` |
84
- | `existingHash !== lastInstalledHash` (consumer edited it) | preserve, print `kept your version` notice + diff hint | `kept` |
85
- | `existingHash === lastInstalledHash` AND `sourceHash !== lastInstalledHash` (clean upstream upgrade) | write upstream, record `sourceHash` | `updated` |
86
-
87
- The manifest is written **atomically** (`tempfile + renameSync`) so a crash mid-install never leaves a partially-written manifest.
88
-
89
- ### What this means for you
90
-
91
- - Edit your `<claudeDirName>/commands/*.md` freely. The installer will not stomp your edits.
92
- - To accept the upstream version of a file you've edited: delete it and rerun `install-commands`.
93
- - To diff your version against upstream:
94
- ```bash
95
- diff .claude/commands/massu-scaffold-router.md \
96
- <(npx @massu/core show-template massu-scaffold-router)
97
- ```
98
-
99
- ## 4. `npx @massu/core show-template <name>`
100
-
101
- Prints the resolved template content (post-variant-resolution) to stdout. Used in the diff one-liner above. Accepts both `massu-scaffold-router` and `massu-scaffold-router.md`. Exits 1 on unknown names.
102
-
103
- ## 5. Currently shipped variants
104
-
105
- | Base | Variants |
106
- |------|----------|
107
- | `massu-scaffold-router` | `.python-fastapi.md` (FastAPI — two-axis), `.python-django.md` (Django — two-axis) |
108
- | `massu-scaffold-page` | `.swift.md` (SwiftUI), regenerated default with embedded Next.js / FastAPI / SwiftUI / Rust examples |
109
- | `massu-deploy` | `.python-launchd.md`, `.python-systemd.md`, `.python-docker.md`, `.python-fly.md` (all two-axis) |
110
-
111
- All other top-level templates ship as variant-agnostic defaults (one `<base>.md`).
112
-
113
- ### Template variable substitution
114
-
115
- As of `@massu/core@1.3.0`, template files may contain `{{variable.path}}` and `{{variable.path | default("fallback")}}` placeholders. These are rendered against the consumer's `massu.config.yaml` (all `framework.*`, `paths.*`, and `config.*` fields) plus the `detected.*` block written by the codebase introspector (e.g. `detected.python.auth_dep`, `detected.swift.api_client_class`).
116
-
117
- Rules:
118
- - Every `{{detected.*}}` reference MUST include a `| default("...")` fallback — introspection may return nothing.
119
- - Use `\{{` to emit a literal `{{` in the rendered output (e.g. Go/Docker format strings).
120
- - A render error on a single file causes that file to be skipped (stderr message only); the rest of the install continues.
121
-
122
- Pass `--skip-commands` to `massu init` or `massu refresh` to suppress command installation entirely.
123
-
124
- ## 6. Adding a new variant
125
-
126
- 1. Create `<base>.<lang>.md` in this directory using the same frontmatter shape as the default.
127
- 2. The default `<base>.md` should remain generic (or be regenerated if it was previously stack-specific — see `massu-scaffold-page.md` for the pattern of an embedded multi-stack default).
128
- 3. Add a row to the table in section 5.
129
- 4. Add a test case to `packages/core/src/__tests__/install-commands.test.ts` that exercises the new variant against a fixture config.
130
-
131
- ## 7. Reference
132
-
133
- - Implementation: `packages/core/src/commands/install-commands.ts`
134
- - Tests: `packages/core/src/__tests__/install-commands.test.ts` and `show-template.test.ts`
package/src/adapter.ts DELETED
@@ -1,121 +0,0 @@
1
- /**
2
- * `@massu/core/adapter` — public adapter authoring SDK (Plan 3c gap-31, gap-35).
3
- *
4
- * Adapter authors import from this subpath ONLY; everything inside @massu/core
5
- * outside this entry point is implementation detail subject to change without
6
- * a major version bump. The contract surface here is part of the SemVer-
7
- * stable API.
8
- *
9
- * Usage (adapter package author):
10
- *
11
- * import { defineAdapter, type CodebaseAdapter } from '@massu/core/adapter';
12
- *
13
- * export default defineAdapter({
14
- * id: 'rails-active-record',
15
- * languages: ['ruby'],
16
- * matches(signals) {
17
- * return Boolean(signals.gemfile?.includes('rails'));
18
- * },
19
- * async introspect(files, rootDir) {
20
- * // Tree-sitter queries, AST walks, file sampling…
21
- * return {
22
- * conventions: { router: 'rails' },
23
- * provenance: [{ field: 'router', value: 'config/routes.rb:1', query: 'gemfile' }],
24
- * confidence: 'high',
25
- * };
26
- * },
27
- * });
28
- *
29
- * Then in the adapter package's package.json:
30
- * {
31
- * "name": "@massu/adapter-rails",
32
- * "version": "0.1.0",
33
- * "main": "dist/index.js",
34
- * "type": "module",
35
- * "massu-adapter": true,
36
- * "massu-adapter-api-version": "1",
37
- * "peerDependencies": { "@massu/core": ">=1.5.0 <2.0.0" }
38
- * }
39
- *
40
- * The adapter loader (Plan 3b runner.ts) does
41
- * const mod = await import(`<package-dir>/${main}`);
42
- * const adapter = (mod.default ?? mod) as CodebaseAdapter;
43
- * and dispatches matches() + introspect() accordingly.
44
- *
45
- * defineAdapter is a NO-OP at runtime — it's an identity function that
46
- * exists for compile-time type narrowing (so adapter authors get IDE
47
- * autocomplete + type errors for missing fields). The factory's only job
48
- * is to anchor the type contract; it does NOT validate at runtime, register
49
- * anywhere, or mutate state. Runtime validation happens at the loader
50
- * (Plan 3b runner.ts) which checks the dispatched object shape before
51
- * invoking matches/introspect.
52
- *
53
- * Stability: every export here is part of @massu/core's public SemVer
54
- * surface. Breaking changes to the CodebaseAdapter shape (renamed fields,
55
- * removed methods) require a major version bump per the
56
- * massu-adapter-api-version contract. Adapter packages declare
57
- * `"massu-adapter-api-version": "1"` so the loader refuses incompatible
58
- * majors at startup.
59
- */
60
-
61
- export {
62
- // The contract every adapter package must implement.
63
- type CodebaseAdapter,
64
- // Inputs the runner provides to matches() / introspect().
65
- type DetectionSignals,
66
- type SourceFile,
67
- type TreeSitterLanguage,
68
- // Output shapes from introspect() + the runner's merge step.
69
- type AdapterResult,
70
- type Provenance,
71
- type AdapterResolved,
72
- type MergedAdapterOutput,
73
- } from './detect/adapters/types.js';
74
-
75
- import type { CodebaseAdapter } from './detect/adapters/types.js';
76
-
77
- // ============================================================
78
- // Plan 3c Phase 9b P-A-001a: runtime helper re-exports.
79
- //
80
- // The 5 first-party framework adapters (rails/phoenix/aspnet/spring/go-chi)
81
- // migrated to `packages/adapter-<f>/src/index.ts` workspace packages need
82
- // these helpers from `@massu/core/adapter` rather than reaching into
83
- // `@massu/core` internals (which would couple the workspace package to
84
- // every transitive .ts file).
85
- //
86
- // CR-46: a single SemVer-stable authoring surface. Every export from this
87
- // file is part of the public adapter API; breaking changes require a
88
- // major version bump per `massu-adapter-api-version`.
89
- // ============================================================
90
-
91
- // Tree-sitter query helpers (compileQuery is intentionally NOT re-exported —
92
- // only `runQuery` returns the cooked record shape adapters consume).
93
- export { runQuery, InvalidQueryError, type RunQueryHit } from './detect/adapters/query-helpers.js';
94
-
95
- // Grammar acquisition (runtime: downloads + verifies SHA-256 + caches).
96
- export { loadGrammar } from './detect/adapters/tree-sitter-loader.js';
97
-
98
- // Parse-time safety guards (size cap + nested-depth check).
99
- export {
100
- isParsableSource,
101
- MAX_AST_FILE_BYTES,
102
- MAX_AST_PARSE_DEPTH,
103
- MAX_AST_PARSE_MS,
104
- type ParseSkip,
105
- type ParseSkipReason,
106
- } from './detect/adapters/parse-guard.js';
107
-
108
- /**
109
- * Identity factory — narrows the input's type to `CodebaseAdapter` so
110
- * authors get compile-time checking + IDE autocomplete for missing /
111
- * mistyped fields. Runtime: returns the input unchanged. Use this in
112
- * place of an inline `const adapter: CodebaseAdapter = { ... }`
113
- * annotation.
114
- *
115
- * Returning the input (instead of `void`) means adapter packages can do
116
- * `export default defineAdapter({ ... })` and the loader's
117
- * `mod.default` destructuring just works.
118
- */
119
- export function defineAdapter(spec: CodebaseAdapter): CodebaseAdapter {
120
- return spec;
121
- }
@@ -1,292 +0,0 @@
1
- // Copyright (c) 2026 Massu. All rights reserved.
2
- // Licensed under BSL 1.1 - see LICENSE file for details.
3
-
4
- import type Database from 'better-sqlite3';
5
- import type { ToolDefinition, ToolResult } from './tools.ts';
6
- import { getConfig } from './config.ts';
7
-
8
- // ============================================================
9
- // ADR (Architecture Decision Record) Auto-Generation
10
- // ============================================================
11
-
12
- /** Prefix a base tool name with the configured tool prefix. */
13
- function p(baseName: string): string {
14
- return `${getConfig().toolPrefix}_${baseName}`;
15
- }
16
-
17
- /** Default decision detection phrases. Configurable via governance.adr.detection_phrases */
18
- const DEFAULT_DETECTION_PHRASES = ['chose', 'decided', 'switching to', 'moving from', 'going with'];
19
-
20
- /**
21
- * Get decision detection phrases from config or defaults.
22
- */
23
- function getDetectionPhrases(): string[] {
24
- return getConfig().governance?.adr?.detection_phrases ?? DEFAULT_DETECTION_PHRASES;
25
- }
26
-
27
- /**
28
- * Detect decision patterns in text.
29
- */
30
- export function detectDecisionPatterns(text: string): boolean {
31
- const phrases = getDetectionPhrases();
32
- const lower = text.toLowerCase();
33
- return phrases.some(phrase => lower.includes(phrase));
34
- }
35
-
36
- /**
37
- * Extract alternatives from a decision description.
38
- */
39
- export function extractAlternatives(description: string): string[] {
40
- const alternatives: string[] = [];
41
-
42
- // Pattern: "X over Y"
43
- const overMatch = description.match(/(\w[\w\s-]+)\s+over\s+(\w[\w\s-]+)/i);
44
- if (overMatch) {
45
- alternatives.push(overMatch[1].trim());
46
- alternatives.push(overMatch[2].trim());
47
- }
48
-
49
- // Pattern: "X instead of Y"
50
- const insteadMatch = description.match(/(\w[\w\s-]+)\s+instead\s+of\s+(\w[\w\s-]+)/i);
51
- if (insteadMatch) {
52
- alternatives.push(insteadMatch[1].trim());
53
- alternatives.push(insteadMatch[2].trim());
54
- }
55
-
56
- // Pattern: "switching from X to Y"
57
- const switchMatch = description.match(/switching\s+from\s+(\w[\w\s-]+)\s+to\s+(\w[\w\s-]+)/i);
58
- if (switchMatch) {
59
- alternatives.push(switchMatch[2].trim()); // Chosen
60
- alternatives.push(switchMatch[1].trim()); // Rejected
61
- }
62
-
63
- return [...new Set(alternatives)];
64
- }
65
-
66
- /**
67
- * Store an architecture decision.
68
- */
69
- export function storeDecision(
70
- db: Database.Database,
71
- decision: {
72
- title: string;
73
- context: string;
74
- decision: string;
75
- alternatives: string[];
76
- consequences: string;
77
- sessionId?: string;
78
- status?: string;
79
- }
80
- ): number {
81
- const result = db.prepare(`
82
- INSERT INTO architecture_decisions
83
- (title, context, decision, alternatives, consequences, session_id, status)
84
- VALUES (?, ?, ?, ?, ?, ?, ?)
85
- `).run(
86
- decision.title,
87
- decision.context,
88
- decision.decision,
89
- JSON.stringify(decision.alternatives),
90
- decision.consequences,
91
- decision.sessionId ?? null,
92
- decision.status ?? 'accepted'
93
- );
94
-
95
- return Number(result.lastInsertRowid);
96
- }
97
-
98
- // ============================================================
99
- // MCP Tool Definitions & Handlers
100
- // ============================================================
101
-
102
- export function getAdrToolDefinitions(): ToolDefinition[] {
103
- return [
104
- {
105
- name: p('adr_list'),
106
- description: 'List all recorded architecture decisions. Filter by status or search text.',
107
- inputSchema: {
108
- type: 'object',
109
- properties: {
110
- status: { type: 'string', description: 'Filter by status: accepted, superseded, deprecated' },
111
- search: { type: 'string', description: 'Search in title and context' },
112
- },
113
- required: [],
114
- },
115
- },
116
- {
117
- name: p('adr_detail'),
118
- description: 'Get full details of a specific architecture decision by ID.',
119
- inputSchema: {
120
- type: 'object',
121
- properties: {
122
- id: { type: 'number', description: 'ADR ID' },
123
- },
124
- required: ['id'],
125
- },
126
- },
127
- {
128
- name: p('adr_create'),
129
- description: 'Generate an ADR from a description. Extracts alternatives, context, and consequences.',
130
- inputSchema: {
131
- type: 'object',
132
- properties: {
133
- title: { type: 'string', description: 'Decision title' },
134
- context: { type: 'string', description: 'Why was this decision needed?' },
135
- decision: { type: 'string', description: 'What was decided?' },
136
- consequences: { type: 'string', description: 'What are the consequences?' },
137
- },
138
- required: ['title', 'decision'],
139
- },
140
- },
141
- ];
142
- }
143
-
144
- const ADR_BASE_NAMES = new Set(['adr_list', 'adr_detail', 'adr_create']);
145
-
146
- export function isAdrTool(name: string): boolean {
147
- const pfx = getConfig().toolPrefix + '_';
148
- const baseName = name.startsWith(pfx) ? name.slice(pfx.length) : name;
149
- return ADR_BASE_NAMES.has(baseName);
150
- }
151
-
152
- export function handleAdrToolCall(
153
- name: string,
154
- args: Record<string, unknown>,
155
- memoryDb: Database.Database
156
- ): ToolResult {
157
- try {
158
- const pfx = getConfig().toolPrefix + '_';
159
- const baseName = name.startsWith(pfx) ? name.slice(pfx.length) : name;
160
-
161
- switch (baseName) {
162
- case 'adr_list':
163
- return handleAdrList(args, memoryDb);
164
- case 'adr_detail':
165
- return handleAdrDetail(args, memoryDb);
166
- case 'adr_create':
167
- return handleAdrGenerate(args, memoryDb);
168
- default:
169
- return text(`Unknown ADR tool: ${name}`);
170
- }
171
- } catch (error) {
172
- return text(`Error in ${name}: ${error instanceof Error ? error.message : String(error)}\n\nUsage: ${p('adr_list')} {}, ${p('adr_create')} { title: "...", decision: "..." }`);
173
- }
174
- }
175
-
176
- function handleAdrList(args: Record<string, unknown>, db: Database.Database): ToolResult {
177
- let sql = 'SELECT id, title, status, created_at FROM architecture_decisions WHERE 1=1';
178
- const params: string[] = [];
179
-
180
- if (args.status) {
181
- sql += ' AND status = ?';
182
- params.push(args.status as string);
183
- }
184
-
185
- if (args.search) {
186
- sql += ' AND (title LIKE ? OR context LIKE ?)';
187
- const searchTerm = `%${args.search}%`;
188
- params.push(searchTerm, searchTerm);
189
- }
190
-
191
- sql += ' ORDER BY created_at DESC';
192
-
193
- const decisions = db.prepare(sql).all(...params) as Array<Record<string, unknown>>;
194
-
195
- if (decisions.length === 0) {
196
- return text(`No architecture decisions found. Decisions are recorded when you use ${p('adr_create')} during design discussions. Try: ${p('adr_create')} { title: "Use Redis for caching", decision: "We chose Redis over Memcached" }`);
197
- }
198
-
199
- const lines = [
200
- `## Architecture Decisions (${decisions.length})`,
201
- '',
202
- '| ID | Title | Status | Date |',
203
- '|----|-------|--------|------|',
204
- ];
205
-
206
- for (const d of decisions) {
207
- lines.push(`| ${d.id} | ${d.title} | ${d.status} | ${(d.created_at as string).slice(0, 10)} |`);
208
- }
209
-
210
- return text(lines.join('\n'));
211
- }
212
-
213
- function handleAdrDetail(args: Record<string, unknown>, db: Database.Database): ToolResult {
214
- const id = args.id as number;
215
- if (!id) return text(`Usage: ${p('adr_detail')} { id: 1 } - Get full details of an architecture decision.`);
216
-
217
- const decision = db.prepare(
218
- 'SELECT * FROM architecture_decisions WHERE id = ?'
219
- ).get(id) as Record<string, unknown> | undefined;
220
-
221
- if (!decision) {
222
- return text(`ADR #${id} not found. Decisions are stored when created via ${p('adr_create')}. Try: ${p('adr_list')} {} to see all recorded decisions.`);
223
- }
224
-
225
- const alternatives = JSON.parse((decision.alternatives as string) || '[]') as string[];
226
-
227
- const lines = [
228
- `# ADR-${decision.id}: ${decision.title}`,
229
- `Status: ${decision.status}`,
230
- `Date: ${(decision.created_at as string).slice(0, 10)}`,
231
- '',
232
- '## Context',
233
- decision.context as string || 'Not specified',
234
- '',
235
- '## Decision',
236
- decision.decision as string,
237
- '',
238
- ];
239
-
240
- if (alternatives.length > 0) {
241
- lines.push('## Alternatives Considered');
242
- for (const alt of alternatives) {
243
- lines.push(`- ${alt}`);
244
- }
245
- lines.push('');
246
- }
247
-
248
- if (decision.consequences) {
249
- lines.push('## Consequences');
250
- lines.push(decision.consequences as string);
251
- }
252
-
253
- return text(lines.join('\n'));
254
- }
255
-
256
- function handleAdrGenerate(args: Record<string, unknown>, db: Database.Database): ToolResult {
257
- const title = args.title as string;
258
- const decisionText = args.decision as string;
259
- if (!title || !decisionText) return text(`Usage: ${p('adr_create')} { title: "Use Redis for caching", decision: "We chose Redis over Memcached for caching", context: "...", consequences: "..." }`);
260
-
261
- const context = (args.context as string) ?? '';
262
- const consequences = (args.consequences as string) ?? '';
263
-
264
- // Extract alternatives from the decision text
265
- const alternatives = extractAlternatives(decisionText);
266
-
267
- const id = storeDecision(db, {
268
- title,
269
- context,
270
- decision: decisionText,
271
- alternatives,
272
- consequences,
273
- });
274
-
275
- const lines = [
276
- `## ADR-${id} Created: ${title}`,
277
- '',
278
- `**Status**: accepted`,
279
- `**Decision**: ${decisionText}`,
280
- alternatives.length > 0 ? `**Alternatives**: ${alternatives.join(', ')}` : '',
281
- context ? `**Context**: ${context}` : '',
282
- consequences ? `**Consequences**: ${consequences}` : '',
283
- '',
284
- `View full details: ${p('adr_detail')} { id: ${id} }`,
285
- ];
286
-
287
- return text(lines.filter(Boolean).join('\n'));
288
- }
289
-
290
- function text(content: string): ToolResult {
291
- return { content: [{ type: 'text', text: content }] };
292
- }