@massu/core 1.10.8 → 1.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +1303 -0
- package/dist/adapter.js +6 -4
- package/dist/cli.js +517 -397
- package/dist/detect/adapters/.bundle-shasums.json +4 -7
- package/dist/detect/adapters/rails.js +6 -4
- package/dist/detect/adapters/spring.js +6 -4
- package/dist/detect/adapters/tree-sitter-loader.js +6 -4
- package/dist/hooks/auto-learning-pipeline.js +24 -1
- package/dist/hooks/classify-failure.js +32 -3
- package/dist/hooks/cost-tracker.js +24 -0
- package/dist/hooks/fix-detector.js +97 -1
- package/dist/hooks/incident-pipeline.js +30 -1
- package/dist/hooks/intent-suggester.js +6 -3
- package/dist/hooks/post-edit-context.js +24 -1
- package/dist/hooks/post-tool-use.js +66 -18
- package/dist/hooks/pre-compact.js +27 -2
- package/dist/hooks/pre-delete-check.js +48 -22
- package/dist/hooks/pre-tool-use-gate.js +962 -0
- package/dist/hooks/quality-event.js +24 -0
- package/dist/hooks/rule-enforcement-pipeline.js +24 -1
- package/dist/hooks/security-gate.js +29 -16
- package/dist/hooks/session-end.js +24 -0
- package/dist/hooks/session-start.js +8710 -8460
- package/dist/hooks/user-prompt.js +24 -0
- package/package.json +15 -18
- package/commands/README.md +0 -134
- package/src/adapter.ts +0 -121
- package/src/adr-generator.ts +0 -292
- package/src/analytics.ts +0 -373
- package/src/audit-trail.ts +0 -450
- package/src/backfill-sessions.ts +0 -182
- package/src/changelog-generator.ts +0 -178
- package/src/cli.ts +0 -246
- package/src/cloud-sync.ts +0 -234
- package/src/commands/adapters.ts +0 -824
- package/src/commands/changelog.ts +0 -165
- package/src/commands/config-check-drift.ts +0 -133
- package/src/commands/config-refresh.ts +0 -396
- package/src/commands/config-upgrade.ts +0 -127
- package/src/commands/doctor.ts +0 -485
- package/src/commands/hook-runner.ts +0 -145
- package/src/commands/init.ts +0 -1823
- package/src/commands/install-commands.ts +0 -692
- package/src/commands/install-hooks.ts +0 -27
- package/src/commands/permissions.ts +0 -150
- package/src/commands/refresh-log.ts +0 -37
- package/src/commands/show-template.ts +0 -65
- package/src/commands/template-engine.ts +0 -301
- package/src/commands/watch.ts +0 -430
- package/src/config.ts +0 -765
- package/src/cost-tracker.ts +0 -355
- package/src/db.ts +0 -370
- package/src/dependency-scorer.ts +0 -337
- package/src/detect/adapters/aspnet.ts +0 -4
- package/src/detect/adapters/discover.ts +0 -469
- package/src/detect/adapters/file-sampler.ts +0 -225
- package/src/detect/adapters/go-chi.ts +0 -4
- package/src/detect/adapters/index.ts +0 -51
- package/src/detect/adapters/nextjs-trpc.ts +0 -166
- package/src/detect/adapters/parse-guard.ts +0 -133
- package/src/detect/adapters/phoenix.ts +0 -4
- package/src/detect/adapters/python-django.ts +0 -208
- package/src/detect/adapters/python-fastapi.ts +0 -223
- package/src/detect/adapters/python-flask.ts +0 -235
- package/src/detect/adapters/query-helpers.ts +0 -170
- package/src/detect/adapters/rails.ts +0 -4
- package/src/detect/adapters/runner.ts +0 -284
- package/src/detect/adapters/spring.ts +0 -4
- package/src/detect/adapters/swift-swiftui.ts +0 -171
- package/src/detect/adapters/tree-sitter-loader.ts +0 -517
- package/src/detect/adapters/types.ts +0 -191
- package/src/detect/codebase-introspector.ts +0 -210
- package/src/detect/domain-inferrer.ts +0 -197
- package/src/detect/drift.ts +0 -199
- package/src/detect/framework-detector.ts +0 -307
- package/src/detect/index.ts +0 -209
- package/src/detect/manifest-registry.ts +0 -261
- package/src/detect/migrate.ts +0 -362
- package/src/detect/monorepo-detector.ts +0 -348
- package/src/detect/package-detector.ts +0 -828
- package/src/detect/passthrough.ts +0 -108
- package/src/detect/regex-fallback.ts +0 -449
- package/src/detect/source-dir-detector.ts +0 -297
- package/src/detect/vr-command-map.ts +0 -167
- package/src/docs-map.json +0 -100
- package/src/docs-tools.ts +0 -519
- package/src/domains.ts +0 -182
- package/src/hooks/auto-learning-pipeline.ts +0 -228
- package/src/hooks/classify-failure.ts +0 -259
- package/src/hooks/cost-tracker.ts +0 -66
- package/src/hooks/fix-detector.ts +0 -186
- package/src/hooks/incident-pipeline.ts +0 -190
- package/src/hooks/intent-suggester.ts +0 -131
- package/src/hooks/post-edit-context.ts +0 -91
- package/src/hooks/post-tool-use.ts +0 -413
- package/src/hooks/pre-compact.ts +0 -168
- package/src/hooks/pre-delete-check.ts +0 -243
- package/src/hooks/quality-event.ts +0 -127
- package/src/hooks/rule-enforcement-pipeline.ts +0 -159
- package/src/hooks/security-gate.ts +0 -153
- package/src/hooks/session-end.ts +0 -467
- package/src/hooks/session-start.ts +0 -436
- package/src/hooks/user-prompt.ts +0 -157
- package/src/import-resolver.ts +0 -226
- package/src/knowledge-db.ts +0 -176
- package/src/knowledge-indexer.ts +0 -708
- package/src/knowledge-tools.ts +0 -1414
- package/src/lib/fileLock.ts +0 -203
- package/src/lib/framework-supports.ts +0 -85
- package/src/lib/gitToplevel.ts +0 -22
- package/src/lib/hook-registry.ts +0 -43
- package/src/lib/installLock.ts +0 -66
- package/src/lib/memory-path.ts +0 -49
- package/src/lib/pidLiveness.ts +0 -67
- package/src/lib/settings-local.ts +0 -110
- package/src/lib/sql-table-names.ts +0 -71
- package/src/license.ts +0 -547
- package/src/lsp/auto-detect.ts +0 -98
- package/src/lsp/client.ts +0 -776
- package/src/lsp/enrich.ts +0 -127
- package/src/lsp/types.ts +0 -221
- package/src/memory-db.ts +0 -1614
- package/src/memory-file-ingest.ts +0 -128
- package/src/memory-tools.ts +0 -424
- package/src/middleware-tree.ts +0 -71
- package/src/observability-tools.ts +0 -343
- package/src/observation-extractor.ts +0 -418
- package/src/page-deps.ts +0 -285
- package/src/permissions.ts +0 -422
- package/src/prompt-analyzer.ts +0 -332
- package/src/python/coupling-detector.ts +0 -125
- package/src/python/domain-enforcer.ts +0 -84
- package/src/python/impact-analyzer.ts +0 -96
- package/src/python/import-parser.ts +0 -244
- package/src/python/import-resolver.ts +0 -136
- package/src/python/migration-indexer.ts +0 -116
- package/src/python/migration-parser.ts +0 -332
- package/src/python/model-indexer.ts +0 -71
- package/src/python/model-parser.ts +0 -279
- package/src/python/route-indexer.ts +0 -59
- package/src/python/route-parser.ts +0 -317
- package/src/python-tools.ts +0 -630
- package/src/regression-detector.ts +0 -319
- package/src/rules.ts +0 -57
- package/src/schema-mapper.ts +0 -232
- package/src/security/adapter-origin.ts +0 -130
- package/src/security/adapter-verifier.ts +0 -319
- package/src/security/atomic-write.ts +0 -164
- package/src/security/fetcher.ts +0 -200
- package/src/security/install-tracking.ts +0 -319
- package/src/security/license-pubkey.generated.ts +0 -30
- package/src/security/license-response-verifier.ts +0 -149
- package/src/security/local-fingerprint.ts +0 -225
- package/src/security/manifest-cache.ts +0 -333
- package/src/security/manifest-schema.ts +0 -129
- package/src/security/registry-pubkey.generated.ts +0 -35
- package/src/security/telemetry.ts +0 -320
- package/src/security-scorer.ts +0 -405
- package/src/security-utils.ts +0 -133
- package/src/sentinel-db.ts +0 -580
- package/src/sentinel-scanner.ts +0 -406
- package/src/sentinel-tools.ts +0 -513
- package/src/sentinel-types.ts +0 -140
- package/src/server-dispatch.ts +0 -225
- package/src/server.ts +0 -107
- package/src/session-archiver.ts +0 -111
- package/src/session-state-generator.ts +0 -174
- package/src/team-knowledge.ts +0 -407
- package/src/tool-db-needs.ts +0 -232
- package/src/tools.ts +0 -1206
- package/src/transcript-parser.ts +0 -458
- package/src/trpc-index.ts +0 -215
- package/src/validate-features-runner.ts +0 -107
- package/src/validation-engine.ts +0 -358
- package/src/watch/daemon.ts +0 -385
- package/src/watch/lockfile-detector.ts +0 -65
- package/src/watch/paths.ts +0 -279
- 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,
|
package/package.json
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@massu/core",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.12.0",
|
|
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": "
|
|
6
|
+
"main": "./dist/cli.js",
|
|
7
|
+
"types": "./dist/adapter.d.ts",
|
|
7
8
|
"exports": {
|
|
8
|
-
".":
|
|
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-
|
|
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-
|
|
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",
|
package/commands/README.md
DELETED
|
@@ -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
|
-
}
|
package/src/adr-generator.ts
DELETED
|
@@ -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
|
-
}
|