archsutra-ai 0.1.3 → 0.1.4

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 (60) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +25 -17
  3. package/dist/audit.js +5 -2
  4. package/dist/cli.js +116 -12
  5. package/dist/drift.js +57 -0
  6. package/dist/imports.js +42 -0
  7. package/dist/map.js +13 -1
  8. package/dist/mcp.js +81 -0
  9. package/dist/scaffold.js +87 -0
  10. package/dist/suppress.js +51 -0
  11. package/docs/architecture/overview.html +3 -3
  12. package/docs/architecture/threat-model.html +3 -3
  13. package/docs/community/good-first-issues.html +3 -3
  14. package/docs/compatibility/matrix.html +3 -3
  15. package/docs/compatibility/smoke-tests/index.html +3 -3
  16. package/docs/compatibility/smoke-tests/results-2026-11-07.md +17 -0
  17. package/docs/decisions/ADR-0001-deterministic-cli-plus-ide-skills.html +3 -3
  18. package/docs/handbook/01-executive-summary.html +5 -5
  19. package/docs/handbook/01-executive-summary.md +2 -2
  20. package/docs/handbook/02-functional-specification.html +5 -5
  21. package/docs/handbook/02-functional-specification.md +2 -2
  22. package/docs/handbook/03-technical-specification.html +4 -4
  23. package/docs/handbook/03-technical-specification.md +1 -1
  24. package/docs/handbook/04-architecture.html +4 -4
  25. package/docs/handbook/04-architecture.md +1 -1
  26. package/docs/handbook/05-user-guide.html +6 -6
  27. package/docs/handbook/05-user-guide.md +3 -3
  28. package/docs/handbook/06-cli-reference.html +5 -5
  29. package/docs/handbook/06-cli-reference.md +9 -2
  30. package/docs/handbook/07-security-privacy-legal.html +4 -4
  31. package/docs/handbook/07-security-privacy-legal.md +1 -1
  32. package/docs/handbook/08-testing-and-proof.html +5 -5
  33. package/docs/handbook/08-testing-and-proof.md +2 -2
  34. package/docs/handbook/09-compatibility-and-roadmap.html +11 -11
  35. package/docs/handbook/09-compatibility-and-roadmap.md +25 -14
  36. package/docs/handbook/10-contributor-guide.html +4 -4
  37. package/docs/handbook/10-contributor-guide.md +3 -3
  38. package/docs/handbook/11-glossary-and-faq.html +4 -4
  39. package/docs/handbook/11-glossary-and-faq.md +1 -1
  40. package/docs/handbook/12-rule-catalog.html +3 -3
  41. package/docs/handbook/13-trust-and-assurance.html +5 -5
  42. package/docs/handbook/13-trust-and-assurance.md +3 -3
  43. package/docs/handbook/14-real-world-evaluation.html +3 -3
  44. package/docs/handbook/15-supply-chain-and-release.html +5 -5
  45. package/docs/handbook/15-supply-chain-and-release.md +7 -5
  46. package/docs/handbook/16-performance-and-quality-metrics.html +4 -4
  47. package/docs/handbook/16-performance-and-quality-metrics.md +1 -1
  48. package/docs/handbook/README.md +2 -2
  49. package/docs/handbook/index.html +5 -5
  50. package/docs/index.html +4 -4
  51. package/docs/legal/legal-review-brief.html +3 -3
  52. package/docs/legal/trademark-and-name-clearance.html +5 -5
  53. package/docs/legal/trademark-and-name-clearance.md +3 -1
  54. package/docs/proof/accessibility.html +3 -3
  55. package/docs/proof/test-results.txt +40 -35
  56. package/docs/rfc/0001-archsutra-v0.1.html +5 -5
  57. package/docs/rfc/0001-archsutra-v0.1.md +2 -2
  58. package/docs/sitemap.xml +28 -28
  59. package/llms.txt +6 -3
  60. package/package.json +5 -2
package/CHANGELOG.md CHANGED
@@ -2,6 +2,24 @@
2
2
 
3
3
  All notable changes follow [Semantic Versioning](https://semver.org/).
4
4
 
5
+ ## 0.1.4 - 2026-11-07
6
+
7
+ ### Added
8
+ - `.archsutra/suppress.json` hides a finding when `rule`, `path`, and `reason` match. A row without a reason is ignored. A row whose `expires` date is past is ignored.
9
+ - Import edges for Go, Java, Kotlin, C#, and Rust by matching an import suffix to a module directory.
10
+ - `drift` and `compare` diff two `findings.json` files, or a project against `--baseline`. `--fail-on` applies to added findings. `compare` does not run Semgrep.
11
+ - `review`, `design`, `threat-model`, `modernize`, and `cost` write an evidence starter. Sections the scanner cannot decide stay `unknown`. Exit code 3 is unused.
12
+ - `archsutra mcp` speaks MCP on stdio. Tools: `archsutra_audit`, `archsutra_map`, `archsutra_drift`.
13
+
14
+ ### Changed
15
+ - Release workflow publishes `npm publish --provenance --access public` after the GitHub release. This needs an npm trusted publisher for `release.yml`. `0.1.3` is not republished.
16
+
17
+ ## Unreleased
18
+
19
+ ### Changed
20
+ - Docs now match the registry and CI. `archsutra-ai` is on npm (`0.1.2`, `0.1.3`). Homepage stats are 31 tests and Ubuntu plus Windows. OpenSSF Scorecard **5.7 / 10** (2026-11-06) is quoted with the failing checks. Install examples pin `archsutra-ai@0.1.3` because the `v0.1.2` tag has no `archsutra-ai` bin.
21
+ - Recorded the release split: no `v0.1.3` git tag, npm `0.1.3` `gitHead` is `4a7ab27`, no npm provenance. Next publish has to be `0.1.4` or later, from Actions.
22
+
5
23
  ## 0.1.3 - 2026-11-06
6
24
 
7
25
  ### Fixed
package/README.md CHANGED
@@ -11,7 +11,7 @@
11
11
  [![License: Apache-2.0](https://img.shields.io/badge/license-Apache--2.0-blue.svg)](LICENSE)
12
12
  ![Node](https://img.shields.io/badge/node-%E2%89%A520-339933)
13
13
  ![Runtime dependencies](https://img.shields.io/badge/runtime%20dependencies-0-brightgreen)
14
- ![Tests](https://img.shields.io/badge/tests-31%20passing-brightgreen)
14
+ ![Tests](https://img.shields.io/badge/tests-36%20passing-brightgreen)
15
15
  ![Coverage](https://img.shields.io/badge/coverage-92%25%20lines-brightgreen)
16
16
 
17
17
  [**Documentation site**](https://shahvatsalofficial.github.io/archsutra-ai/) · [Handbook](docs/handbook/README.md) · [**Trust and assurance register**](docs/handbook/13-trust-and-assurance.md) · [Real-world evaluation](docs/handbook/14-real-world-evaluation.md) · [Sample report](examples/brownfield-monolith/sample-output/audit-report.md)
@@ -24,7 +24,7 @@ ArchSutra AI is an open-source **software architecture agent**. It ships portabl
24
24
 
25
25
  > **No extra API key.** Inside a supported AI IDE, ArchSutra uses that IDE's own model. You need access to the IDE, not to an ArchSutra account. The CLI is offline, read-only by default, and has no telemetry. Optional provider-backed server inference is **not** part of v0.1; if added later it will be opt-in, separately configured, and may cost money.
26
26
 
27
- Created by **[Vatsal Shah](https://github.com/shahvatsalofficial)**. Licensed under Apache-2.0.
27
+ Created by **[Vatsal Shah](https://shahvatsal.com)**. Licensed under Apache-2.0.
28
28
 
29
29
  ## Contents
30
30
 
@@ -38,23 +38,26 @@ Created by **[Vatsal Shah](https://github.com/shahvatsalofficial)**. Licensed un
38
38
  | Different answer every run | A deterministic CLI produces reproducible findings with stable rule IDs |
39
39
  | Another tool, another key, another bill | Offline CLI; skills run on the model you already use in your IDE |
40
40
  | Reviews are slow and rare | First-pass audit in seconds; fits in CI with `--fail-on high` |
41
- | "Trust me" scanners | Open source, zero runtime dependencies, tests, CI on 3 OSes, accuracy measured on 52 real repositories with the false positives published |
41
+ | "Trust me" scanners | Open source, zero runtime dependencies, tests, CI on Ubuntu and Windows, accuracy measured on 52 real repositories with the false positives published |
42
42
 
43
43
  ## 60-second start
44
44
 
45
45
  Requires Node.js 20+.
46
46
 
47
47
  ```bash
48
- # audit any repo: offline, no API key. Writes Markdown and HTML.
49
- npx github:shahvatsalofficial/archsutra-ai audit . --output docs/architecture
48
+ # Windows and npm: this version installs a command named archsutra-ai.
49
+ npx --yes archsutra-ai@0.1.3 audit . --output docs/architecture
50
50
  # open docs/architecture/index.html
51
51
 
52
+ # or from git (default branch has the same bin; the v0.1.2 tag does not):
53
+ npx github:shahvatsalofficial/archsutra-ai audit . --output docs/architecture
54
+
52
55
  # install the skills into your project for your AI IDE (preview first)
53
- npx github:shahvatsalofficial/archsutra-ai install --host claude --target . --dry-run
54
- npx github:shahvatsalofficial/archsutra-ai install --host claude --target .
56
+ npx --yes archsutra-ai@0.1.3 install --host claude --target . --dry-run
57
+ npx --yes archsutra-ai@0.1.3 install --host claude --target .
55
58
  ```
56
59
 
57
- For anything beyond a first try, **pin a tag or commit** so the code cannot change under you: `npx github:shahvatsalofficial/archsutra-ai#v0.1.2 audit . --output docs/architecture` (see [supply chain](docs/handbook/15-supply-chain-and-release.md)). Install **one** host per project; several hosts read the same skill folders.
60
+ For anything beyond a first try, **pin a version** so the code cannot change under you. On npm, including Windows: `npx --yes archsutra-ai@0.1.3 audit . --output docs/architecture`. The git tag `v0.1.2` is the attested GitHub release, but that tarball's bin is only `archsutra`, so `npx archsutra-ai` against `v0.1.2` fails on Windows. There is no `v0.1.3` git tag, and the npm `0.1.3` tarball was not built by Actions (no npm provenance). Details: [supply chain](docs/handbook/15-supply-chain-and-release.md). Install **one** host per project; several hosts read the same skill folders.
58
61
 
59
62
  Without `--output`, `audit` and `map` print one format to the terminal (`--format md` or `--format html`) and write nothing. With `--output`, every report is written as both a Markdown file and an HTML page.
60
63
 
@@ -101,7 +104,7 @@ We published the 50% baseline, fixed the false positives it exposed (each has a
101
104
 
102
105
  | Evidence | Result |
103
106
  |---|---|
104
- | Automated tests (`npm test`) | **31 passed, 0 failed** ([output](docs/proof/test-results.txt)) |
107
+ | Automated tests (`npm test`) | **36 passed, 0 failed** ([output](docs/proof/test-results.txt)) |
105
108
  | Test coverage | 92% lines, 81% branches, 91% functions ([report](docs/proof/coverage.txt)) |
106
109
  | CI matrix | Ubuntu and Windows x Node 20, 22, 24 plus Ubuntu on the newest Node ([Actions](https://github.com/shahvatsalofficial/archsutra-ai/actions)). macOS is not in CI: GitHub cancelled those jobs before a runner started, and there is no Mac available to re-check them |
107
110
  | Robustness | ReDoS guard, hostile file names, binary files, path traversal, symlinks, byte and file budgets, all tested |
@@ -112,7 +115,7 @@ We published the 50% baseline, fixed the false positives it exposed (each has a
112
115
 
113
116
  ### Not yet proven (we say so)
114
117
 
115
- Live behavior inside each AI IDE (layouts match vendor docs; [smoke-test protocol](docs/compatibility/smoke-tests/README.md) is open for results), an independent security review, a head-to-head comparison with other scanners (workflow ready, not yet run), trademark clearance for the name, and a lawyer's review of these documents. No OpenSSF Scorecard result exists until the first run; none is quoted.
118
+ Live behavior inside each AI IDE (layouts match vendor docs; [smoke-test protocol](docs/compatibility/smoke-tests/README.md) is open for results), an independent security review, a head-to-head comparison with other scanners (workflow ready, not yet run), trademark clearance for the name, and a lawyer's review of these documents. OpenSSF Scorecard scored this repository **5.7 / 10** on 2026-11-06 (Scorecard v5.6.0, commit `06c87c4`). That score is real and low: branch protection, code review, signed-releases (as Scorecard counts them), fuzzing, and the Best Practices badge are 0. Quote the score with those zeros, not as a pass.
116
119
 
117
120
  ## Sample output
118
121
 
@@ -174,21 +177,24 @@ Real sub-agent spawning depends on the host. Where unavailable, Prime performs t
174
177
  ## CLI reference
175
178
 
176
179
  ```text
177
- archsutra init|doctor|map|audit|verify|export [path]
180
+ archsutra init|doctor|map|audit|review|design|threat-model|modernize|cost|verify|export [path]
181
+ archsutra drift|compare <before.json> <after.json>
182
+ archsutra drift|compare [path] --baseline <findings.json>
178
183
  archsutra html <file.md>
184
+ archsutra mcp
179
185
  archsutra install|uninstall --host <claude|cursor|codex|copilot|gemini> [--target dir] [--dry-run] [--force]
180
186
  archsutra hosts
181
- Flags: --json --format <md|html> --dry-run --output <dir> --fail-on <critical|high|medium|low> --verbose --version --help
187
+ Flags: --json --format <md|html> --dry-run --output <dir> --baseline <file> --fail-on <critical|high|medium|low> --verbose --version --help
182
188
  ```
183
189
 
184
- Exit codes: `0` ok, `1` findings at or above `--fail-on` / install conflict / failed doctor, `2` usage error, `3` command needs a model (use the skill), `4` verification failed, `5` internal error.
190
+ Exit codes: `0` ok, `1` findings at or above `--fail-on` / install conflict / failed doctor, `2` usage error, `3` reserved, `4` verification failed, `5` internal error.
185
191
 
186
- `design`, `review`, `threat-model`, `modernize`, `compare`, `cost` and `drift` need semantic reasoning, so the CLI exits `3` and points you to the matching skill. It never pretends a parser can reason and never calls a model API. Full reference: [CLI reference](docs/handbook/06-cli-reference.md). Rule list: [rule catalog](docs/handbook/12-rule-catalog.md).
192
+ `design`, `review`, `threat-model`, `modernize`, `cost`, `compare`, and `drift` run offline. They write evidence starters. Sections a parser cannot decide stay marked `unknown`. The IDE skill fills those. No model API is called. `compare` diffs two ArchSutra findings files. Semgrep overlap is `node scripts/compare.mjs` and is not run by the CLI. Full reference: [CLI reference](docs/handbook/06-cli-reference.md). Rule list: [rule catalog](docs/handbook/12-rule-catalog.md).
187
193
 
188
194
  ## Use it in CI
189
195
 
190
196
  ```yaml
191
- - run: npx --yes github:shahvatsalofficial/archsutra-ai#v0.1.2 audit . --fail-on high
197
+ - run: npx --yes archsutra-ai@0.1.3 audit . --fail-on high
192
198
  ```
193
199
 
194
200
  Upload SARIF to GitHub code scanning: see the [user guide](docs/handbook/05-user-guide.md). Start with `--fail-on critical` on an existing codebase and tighten later; roughly half of valid findings need no action.
@@ -231,7 +237,8 @@ When you use the skills, **your AI IDE's model provider** processes your prompts
231
237
  - Static, heuristic rules: expect false positives and false negatives; every finding lists caveats, and about half of valid findings are not worth acting on.
232
238
  - Import graph covers JS/TS and Python. Other languages are detected but not graphed.
233
239
  - Reliability, performance, scalability, data integrity and cost are `not assessed` without runtime evidence.
234
- - Adapters are not yet verified in live hosts. Not published to npm (use the `npx github:` path).
240
+ - Adapters are not yet verified in live hosts.
241
+ - npm package `archsutra-ai` is published (`0.1.2` and `0.1.3`). `0.1.3` adds the `archsutra-ai` command name for Windows. It has no git tag and no npm provenance. The name `archsutra` (no `-ai`) was still unclaimed on 2026-11-06. Do not publish the stub in the parent folder.
235
242
 
236
243
  ## FAQ
237
244
 
@@ -260,7 +267,8 @@ More: [glossary and FAQ](docs/handbook/11-glossary-and-faq.md).
260
267
  - [ ] Suppression/baseline file for accepted findings
261
268
  - [ ] Independent security review; trademark clearance; legal review
262
269
  - [ ] Optional local-model and bring-your-own-key server mode, strictly opt-in
263
- - [ ] npm publication after name and trademark checks
270
+ - [x] npm package `archsutra-ai` published (`0.1.2`, `0.1.3`) — laptop publish, no provenance, no `v0.1.3` tag
271
+ - [ ] npm publish from GitHub Actions with provenance, one digest shared with the GitHub release (`v0.1.4` or later; `0.1.3` cannot be replaced)
264
272
 
265
273
  Full plan with acceptance tests: [roadmap](docs/handbook/09-compatibility-and-roadmap.md). Starter tasks: [good first issues](docs/community/good-first-issues.md).
266
274
 
package/dist/audit.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import path from 'node:path';
2
2
  import { readLines, walk } from './fsutil.js';
3
3
  import { buildMap, readJson } from './map.js';
4
+ import { isSuppressed, loadSuppressions } from './suppress.js';
4
5
  import { DEV_TOOLING_PATH, TEST_PATH, commentMask, isDataLike, isMinified, isVendoredPath, looksThirdParty } from './heuristics.js';
5
6
  import { detectSecrets, excerpt } from './redact.js';
6
7
  const CODE_EXT = new Set([
@@ -474,8 +475,10 @@ export async function runAudit(root, corpusIn) {
474
475
  }
475
476
  // ---- Assign stable IDs ------------------------------------------------
476
477
  const sevOrder = { critical: 0, high: 1, medium: 2, low: 3, info: 4 };
477
- drafts.sort((a, b) => sevOrder[a.severity] - sevOrder[b.severity] || a.rule.localeCompare(b.rule));
478
- const findings = drafts.map((x, i) => ({ ...x, id: `${x.rule}#${String(i + 1).padStart(3, '0')}`, status: 'open' }));
478
+ const suppressions = await loadSuppressions(root);
479
+ const visible = drafts.filter((d) => !isSuppressed(d, suppressions));
480
+ visible.sort((a, b) => sevOrder[a.severity] - sevOrder[b.severity] || a.rule.localeCompare(b.rule));
481
+ const findings = visible.map((x, i) => ({ ...x, id: `${x.rule}#${String(i + 1).padStart(3, '0')}`, status: 'open' }));
479
482
  return { map, findings, scorecard: scorecard(findings, codeCount), failedChecks };
480
483
  }
481
484
  function scorecard(findings, codeCount) {
package/dist/cli.js CHANGED
@@ -7,9 +7,11 @@ import { exists } from './fsutil.js';
7
7
  import { buildMap } from './map.js';
8
8
  import { markdownTitle, renderAuditHtml, renderCurrentStateHtml, renderMarkdownHtml, renderOverviewHtml } from './html.js';
9
9
  import { findingsJson, registerJson, renderAuditReport, renderCurrentState, renderOverview, renderSarif } from './report.js';
10
+ import { diffFindings, renderDrift } from './drift.js';
11
+ import { serveMcp } from './mcp.js';
12
+ import { renderCost, renderDesign, renderModernize, renderReview, renderThreatModel } from './scaffold.js';
10
13
  import { verifyProject } from './verify.js';
11
14
  const EXIT = { ok: 0, findings: 1, usage: 2, needsModel: 3, verifyFailed: 4, error: 5 };
12
- const MODEL_COMMANDS = ['design', 'review', 'threat-model', 'modernize', 'compare', 'cost', 'drift'];
13
15
  const HELP = `ArchSutra AI - Design. Audit. Evolve.
14
16
 
15
17
  Usage: archsutra <command> [path] [options]
@@ -19,33 +21,41 @@ Deterministic commands (no AI, no network, read-only unless noted):
19
21
  doctor [path] check Node, hosts, installed files, conflicts
20
22
  map [path] reverse-engineer modules, dependencies, cycles
21
23
  audit [path] evidence-linked findings + scorecard
24
+ review [path] same evidence as audit, written as a review starter
25
+ design [path] design starter from the module map; decisions stay unknown
26
+ threat-model [path] STRIDE starter from security findings; the rest stays unknown
27
+ modernize [path] one slice per finding; target architecture stays unknown
28
+ cost [path] states that cost is not assessed
29
+ drift diff two findings.json files, or a project against --baseline
30
+ compare same diff as drift, for two ArchSutra snapshots
22
31
  verify [path] validate skills, agents, links, diagrams, findings.json
23
32
  export [path] write every report as Markdown and HTML (use --output)
24
33
  html <file.md> write a readable HTML page next to a Markdown report
34
+ mcp MCP server on stdio (tools: audit, map, drift)
25
35
  install install skills/agents into a project (--host, --target)
26
36
  uninstall remove files ArchSutra installed (--host, --target)
27
37
  hosts list adapters and their support level
28
38
 
29
- Model-backed workflows (${MODEL_COMMANDS.join(', ')}):
30
- Run these through the ArchSutra skills inside your AI IDE.
39
+ Semantic judgment (options, STRIDE completion, spend) stays marked unknown.
40
+ Run the matching skill in your IDE to fill those sections. No model API is called.
31
41
 
32
42
  Options:
33
43
  --json machine-readable output
34
44
  --format <md|html> stdout format for audit and map when not writing files (default md)
35
45
  --dry-run show what would change; write nothing
36
- --output <dir> directory for audit, map and export (export default docs/architecture).
37
- audit and map write Markdown and HTML together. For html, a .html file path.
46
+ --output <dir> directory for audit, map, export, and the starter commands
47
+ --baseline <file> findings.json to diff against (drift, compare)
38
48
  --host <id> claude | cursor | codex | copilot | gemini
39
49
  --target <dir> project to install into (default .)
40
50
  --force overwrite modified files (backups kept)
41
- --fail-on <sev> audit exits 1 if a finding is at or above: critical|high|medium|low
51
+ --fail-on <sev> audit, review, and drift exit 1 at or above this severity
42
52
  --verbose extra detail
43
53
  --version, --help
44
54
  `;
45
55
  function parse(argv) {
46
56
  const flags = {};
47
57
  const pos = [];
48
- const withValue = new Set(['output', 'host', 'target', 'fail-on', 'format']);
58
+ const withValue = new Set(['output', 'host', 'target', 'fail-on', 'format', 'baseline']);
49
59
  for (let i = 0; i < argv.length; i++) {
50
60
  const a = argv[i];
51
61
  if (a.startsWith('--')) {
@@ -94,6 +104,22 @@ async function writeReportFiles(outDir, files, dry) {
94
104
  function wroteLine(dry, outDir, files, hint) {
95
105
  return `${dry ? 'Would write' : 'Wrote'} ${files.length} files to ${outDir}\n${files.map((f) => ` ${f.path}`).join('\n')}\n${hint}`;
96
106
  }
107
+ async function readFindings(file) {
108
+ const raw = JSON.parse(await fs.readFile(file, 'utf8'));
109
+ const list = Array.isArray(raw) ? raw : raw.findings;
110
+ if (!Array.isArray(list))
111
+ throw new Error(`${file} has no findings array`);
112
+ return list;
113
+ }
114
+ function severityExit(findings, th) {
115
+ if (!th)
116
+ return undefined;
117
+ const order = ['critical', 'high', 'medium', 'low', 'info'];
118
+ const i = order.indexOf(th);
119
+ if (i < 0)
120
+ return EXIT.usage;
121
+ return findings.some((f) => order.indexOf(f.severity) <= i) ? EXIT.findings : EXIT.ok;
122
+ }
97
123
  async function main() {
98
124
  const args = parse(process.argv.slice(2));
99
125
  const json = Boolean(args.flags.json);
@@ -262,14 +288,92 @@ async function main() {
262
288
  out(json, { ...res, dryRun: dry }, `${dry ? 'DRY RUN\n' : ''}removed: ${res.removed.length}\n${res.removed.map((r) => ` - ${r}`).join('\n')}${res.kept.length ? `\nkept (modified by you): \n${res.kept.map((r) => ` - ${r}`).join('\n')}` : ''}`);
263
289
  return EXIT.ok;
264
290
  }
265
- default:
266
- if (MODEL_COMMANDS.includes(args.cmd)) {
267
- const msg = `"${args.cmd}" needs semantic reasoning, which a deterministic CLI cannot do honestly.\nRun it inside your AI IDE through the matching ArchSutra skill (install with: archsutra install --host <id>).\nNo API call was made.`;
268
- out(json, { command: args.cmd, requiresModel: true, message: msg }, msg);
269
- return EXIT.needsModel;
291
+ case 'mcp': {
292
+ await serveMcp();
293
+ return EXIT.ok;
294
+ }
295
+ case 'drift':
296
+ case 'compare': {
297
+ let before;
298
+ let after;
299
+ if (args.flags.baseline) {
300
+ before = await readFindings(path.resolve(String(args.flags.baseline)));
301
+ after = (await runAudit(target)).findings;
302
+ }
303
+ else if (args.pos.length >= 2) {
304
+ before = await readFindings(path.resolve(args.pos[0]));
305
+ after = await readFindings(path.resolve(args.pos[1]));
306
+ }
307
+ else {
308
+ process.stderr.write(`${args.cmd} needs two findings.json files, or a project path and --baseline <findings.json>.\nSemgrep overlap stays in scripts/compare.mjs. This command does not download rules or clone repositories.\n`);
309
+ return EXIT.usage;
310
+ }
311
+ const report = diffFindings(before, after);
312
+ const title = args.cmd === 'compare' ? 'Findings comparison' : 'Architecture drift';
313
+ const note = args.cmd === 'compare' ? '\nSemgrep overlap is scripts/compare.mjs (needs semgrep on PATH). Not run by this command.\n' : '';
314
+ const md = `${renderDrift(report, title)}${note}`;
315
+ if ('output' in args.flags) {
316
+ const outDir = insideProject(path.resolve(args.pos[0] ?? '.'), String(args.flags.output || 'docs/architecture'));
317
+ if (!outDir) {
318
+ process.stderr.write('--output must stay inside the project directory.\n');
319
+ return EXIT.usage;
320
+ }
321
+ const files = [
322
+ { path: `${args.cmd}.md`, content: md },
323
+ { path: `${args.cmd}.html`, content: renderMarkdownHtml(md, title) },
324
+ ];
325
+ await writeReportFiles(outDir, files, dry);
326
+ }
327
+ out(json, report, md);
328
+ const added = report.added.map((r) => ({ severity: (r.after ?? 'info') }));
329
+ const gate = severityExit(added, args.flags['fail-on']);
330
+ if (gate === EXIT.usage) {
331
+ process.stderr.write(`Invalid --fail-on "${args.flags['fail-on']}"\n`);
332
+ return EXIT.usage;
333
+ }
334
+ return gate ?? EXIT.ok;
335
+ }
336
+ case 'review':
337
+ case 'design':
338
+ case 'threat-model':
339
+ case 'modernize':
340
+ case 'cost': {
341
+ const res = await runAudit(target);
342
+ const md = args.cmd === 'review' ? renderReview(res)
343
+ : args.cmd === 'design' ? renderDesign(res)
344
+ : args.cmd === 'threat-model' ? renderThreatModel(res)
345
+ : args.cmd === 'modernize' ? renderModernize(res)
346
+ : renderCost(res);
347
+ if ('output' in args.flags) {
348
+ const outDir = insideProject(target, String(args.flags.output || 'docs/architecture'));
349
+ if (!outDir) {
350
+ process.stderr.write('--output must stay inside the project directory.\n');
351
+ return EXIT.usage;
352
+ }
353
+ const files = [
354
+ { path: `${args.cmd}.md`, content: md },
355
+ { path: `${args.cmd}.html`, content: renderMarkdownHtml(md, args.cmd) },
356
+ ];
357
+ await writeReportFiles(outDir, files, dry);
358
+ out(json, { dryRun: dry, command: args.cmd }, wroteLine(dry, outDir, files, 'Unknown sections stay unknown. The IDE skill fills those.'));
359
+ }
360
+ else {
361
+ out(json, { command: args.cmd, markdown: md }, md);
362
+ }
363
+ if (args.cmd === 'review') {
364
+ const gate = severityExit(res.findings, args.flags['fail-on']);
365
+ if (gate === EXIT.usage) {
366
+ process.stderr.write(`Invalid --fail-on "${args.flags['fail-on']}"\n`);
367
+ return EXIT.usage;
368
+ }
369
+ return gate ?? EXIT.ok;
270
370
  }
371
+ return EXIT.ok;
372
+ }
373
+ default: {
271
374
  process.stderr.write(`Unknown command "${args.cmd}".\n${HELP}`);
272
375
  return EXIT.usage;
376
+ }
273
377
  }
274
378
  }
275
379
  main().then((code) => {
package/dist/drift.js ADDED
@@ -0,0 +1,57 @@
1
+ function keyOf(f) {
2
+ const paths = [...new Set(f.evidence.map((e) => e.path))].sort();
3
+ const path = paths[0] ?? '.';
4
+ return { key: `${f.rule}@${paths.join('|') || '.'}`, path };
5
+ }
6
+ /** Diff two findings lists. Identity is rule plus evidence paths, not the generated sequence id. */
7
+ export function diffFindings(before, after) {
8
+ const prev = new Map();
9
+ const next = new Map();
10
+ for (const f of before) {
11
+ const k = keyOf(f);
12
+ prev.set(k.key, { sev: f.severity, path: k.path, rule: f.rule });
13
+ }
14
+ for (const f of after) {
15
+ const k = keyOf(f);
16
+ next.set(k.key, { sev: f.severity, path: k.path, rule: f.rule });
17
+ }
18
+ const added = [];
19
+ const removed = [];
20
+ const changed = [];
21
+ for (const [key, row] of next) {
22
+ const old = prev.get(key);
23
+ if (!old)
24
+ added.push({ key, rule: row.rule, path: row.path, after: row.sev });
25
+ else if (old.sev !== row.sev)
26
+ changed.push({ key, rule: row.rule, path: row.path, before: old.sev, after: row.sev });
27
+ }
28
+ for (const [key, row] of prev) {
29
+ if (!next.has(key))
30
+ removed.push({ key, rule: row.rule, path: row.path, before: row.sev });
31
+ }
32
+ const byKey = (a, b) => a.key.localeCompare(b.key);
33
+ added.sort(byKey);
34
+ removed.sort(byKey);
35
+ changed.sort(byKey);
36
+ return { added, removed, changed };
37
+ }
38
+ export function renderDrift(report, title) {
39
+ const line = (r) => `- ${r.rule} ${r.path}${r.before ? ` ${r.before}` : ''}${r.before && r.after ? ' ->' : ''} ${r.after ?? ''}`.replace(/\s+/g, ' ').trim();
40
+ return [
41
+ `# ${title}`,
42
+ '',
43
+ 'Deterministic diff of two ArchSutra findings files. This is not a Semgrep comparison and not a model review.',
44
+ '',
45
+ `Added: ${report.added.length}. Removed: ${report.removed.length}. Severity changed: ${report.changed.length}.`,
46
+ '',
47
+ '## Added',
48
+ report.added.length ? report.added.map(line).join('\n') : '_None._',
49
+ '',
50
+ '## Removed',
51
+ report.removed.length ? report.removed.map(line).join('\n') : '_None._',
52
+ '',
53
+ '## Severity changed',
54
+ report.changed.length ? report.changed.map(line).join('\n') : '_None._',
55
+ '',
56
+ ].join('\n');
57
+ }
@@ -0,0 +1,42 @@
1
+ /** Longest known module path that is a suffix of an import. Stdlib names only match when a repo module is actually named that. */
2
+ export function moduleForImport(spec, moduleNames) {
3
+ const norm = spec.replace(/::/g, '/').replace(/\./g, '/').replace(/^\/+|\/+$/g, '');
4
+ const parts = norm.split('/').filter((p) => p && p !== 'crate' && p !== 'super' && p !== 'self');
5
+ let best;
6
+ for (let i = 0; i < parts.length; i++) {
7
+ const suffix = parts.slice(i).join('/');
8
+ for (const cand of [suffix, `src/${suffix}`]) {
9
+ if (moduleNames.has(cand) && (best === undefined || cand.length > best.length))
10
+ best = cand;
11
+ }
12
+ }
13
+ return best;
14
+ }
15
+ const GO_IMPORT = /"([^"]+)"/g;
16
+ const DOT_IMPORT = /^\s*import\s+(?:static\s+)?([A-Za-z_][\w.]*)/;
17
+ const CS_USING = /^\s*using\s+(?:static\s+)?([A-Za-z_][\w.]*)\s*;/;
18
+ const RUST_USE = /^\s*(?:pub\s+)?use\s+([A-Za-z_][\w:]*)/;
19
+ const RUST_MOD = /^\s*(?:pub\s+)?mod\s+([A-Za-z_][\w]*)\s*;/;
20
+ export function importSpecs(ext, line, fromRel) {
21
+ if (ext === '.go')
22
+ return [...line.matchAll(GO_IMPORT)].map((m) => m[1]).filter((s) => s.includes('/'));
23
+ if (ext === '.java' || ext === '.kt') {
24
+ const m = DOT_IMPORT.exec(line);
25
+ return m ? [m[1]] : [];
26
+ }
27
+ if (ext === '.cs') {
28
+ const m = CS_USING.exec(line);
29
+ return m ? [m[1]] : [];
30
+ }
31
+ if (ext === '.rs') {
32
+ const use = RUST_USE.exec(line);
33
+ if (use)
34
+ return [use[1]];
35
+ const mod = RUST_MOD.exec(line);
36
+ if (!mod)
37
+ return [];
38
+ const dir = fromRel.split('/').slice(0, -1).join('/');
39
+ return [dir ? `${dir}/${mod[1]}` : mod[1]];
40
+ }
41
+ return [];
42
+ }
package/dist/map.js CHANGED
@@ -2,6 +2,7 @@ import path from 'node:path';
2
2
  import { promises as fs } from 'node:fs';
3
3
  import { readLines, walk } from './fsutil.js';
4
4
  import { TEST_PATH, commentMask } from './heuristics.js';
5
+ import { importSpecs, moduleForImport } from './imports.js';
5
6
  import { excerpt } from './redact.js';
6
7
  const ECOSYSTEM_BY_EXT = {
7
8
  '.ts': 'TypeScript', '.tsx': 'TypeScript', '.js': 'JavaScript', '.jsx': 'JavaScript', '.mjs': 'JavaScript',
@@ -206,6 +207,17 @@ export async function buildMap(root, corpus) {
206
207
  addEdge(cand, i + 1, text);
207
208
  });
208
209
  }
210
+ else if (f.ext === '.go' || f.ext === '.java' || f.ext === '.kt' || f.ext === '.cs' || f.ext === '.rs') {
211
+ lines.forEach((text, i) => {
212
+ if (cmask[i])
213
+ return;
214
+ for (const spec of importSpecs(f.ext, text, f.rel)) {
215
+ const to = moduleForImport(spec, moduleNames);
216
+ if (to)
217
+ addEdge(to, i + 1, text);
218
+ }
219
+ });
220
+ }
209
221
  else {
210
222
  blind.add(`Import graph not implemented for ${eco}; module dependencies are not computed for those files.`);
211
223
  }
@@ -226,7 +238,7 @@ export async function buildMap(root, corpus) {
226
238
  if (c.skipped.symlinks)
227
239
  blind.add(`${c.skipped.symlinks} symlink(s) were not followed.`);
228
240
  blind.add('Runtime behaviour, deployed topology, and data volumes are not observable from source alone.');
229
- blind.add('Dependency edges are module-level and based on relative imports; dynamic imports and DI wiring may be missed.');
241
+ blind.add('Dependency edges are module-level. JS/TS uses relative imports. Python uses the top package name. Go, Java, Kotlin, C#, and Rust match an import suffix to a module path. Dynamic imports, path aliases, and DI wiring may be missed. PHP, Ruby, Swift, and Dart are detected and not graphed.');
230
242
  const sortedEco = [...ecosystems].sort((a, b) => (ecoCount.get(b) ?? 0) - (ecoCount.get(a) ?? 0));
231
243
  return {
232
244
  root: c.root,
package/dist/mcp.js ADDED
@@ -0,0 +1,81 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { runAudit } from './audit.js';
3
+ import { diffFindings } from './drift.js';
4
+ import { buildMap } from './map.js';
5
+ import { findingsJson } from './report.js';
6
+ const TOOLS = [
7
+ { name: 'archsutra_audit', description: 'Offline architecture audit. Returns findings JSON. Read-only. No network.', inputSchema: { type: 'object', properties: { path: { type: 'string' } }, required: ['path'] } },
8
+ { name: 'archsutra_map', description: 'Offline module map: ecosystems, edges, cycles, blind spots. Read-only.', inputSchema: { type: 'object', properties: { path: { type: 'string' } }, required: ['path'] } },
9
+ { name: 'archsutra_drift', description: 'Diff two findings.json files already on disk.', inputSchema: { type: 'object', properties: { before: { type: 'string' }, after: { type: 'string' } }, required: ['before', 'after'] } },
10
+ ];
11
+ function frame(obj) {
12
+ const body = JSON.stringify(obj);
13
+ return `Content-Length: ${Buffer.byteLength(body)}\r\n\r\n${body}`;
14
+ }
15
+ async function callTool(name, args) {
16
+ if (name === 'archsutra_audit') {
17
+ const res = await runAudit(String(args.path ?? '.'));
18
+ return JSON.stringify(findingsJson(res));
19
+ }
20
+ if (name === 'archsutra_map') {
21
+ return JSON.stringify(await buildMap(String(args.path ?? '.')));
22
+ }
23
+ if (name === 'archsutra_drift') {
24
+ const before = JSON.parse(await import('node:fs/promises').then((fs) => fs.readFile(String(args.before), 'utf8')));
25
+ const after = JSON.parse(await import('node:fs/promises').then((fs) => fs.readFile(String(args.after), 'utf8')));
26
+ const b = Array.isArray(before) ? before : before.findings;
27
+ const a = Array.isArray(after) ? after : after.findings;
28
+ return JSON.stringify(diffFindings(b, a));
29
+ }
30
+ throw new Error(`Unknown tool ${name}`);
31
+ }
32
+ /** Speak MCP over stdio (Content-Length framing). One process, then exit when stdin closes. */
33
+ export async function serveMcp() {
34
+ const chunks = [];
35
+ for await (const chunk of process.stdin)
36
+ chunks.push(chunk);
37
+ const buf = Buffer.concat(chunks);
38
+ let offset = 0;
39
+ const text = buf.toString('utf8');
40
+ while (offset < text.length) {
41
+ const headerEnd = text.indexOf('\r\n\r\n', offset);
42
+ if (headerEnd < 0)
43
+ break;
44
+ const header = text.slice(offset, headerEnd);
45
+ const m = /Content-Length:\s*(\d+)/i.exec(header);
46
+ if (!m)
47
+ break;
48
+ const len = Number(m[1]);
49
+ const start = headerEnd + 4;
50
+ const body = text.slice(start, start + len);
51
+ offset = start + len;
52
+ let msg;
53
+ try {
54
+ msg = JSON.parse(body);
55
+ }
56
+ catch {
57
+ continue;
58
+ }
59
+ if (msg.id === undefined || msg.id === null)
60
+ continue;
61
+ if (msg.method === 'initialize') {
62
+ const pkg = JSON.parse(await readFile(new URL('../package.json', import.meta.url), 'utf8'));
63
+ process.stdout.write(frame({ jsonrpc: '2.0', id: msg.id, result: { protocolVersion: '2024-11-05', capabilities: { tools: {} }, serverInfo: { name: 'archsutra', version: pkg.version } } }));
64
+ }
65
+ else if (msg.method === 'tools/list') {
66
+ process.stdout.write(frame({ jsonrpc: '2.0', id: msg.id, result: { tools: TOOLS } }));
67
+ }
68
+ else if (msg.method === 'tools/call') {
69
+ try {
70
+ const textOut = await callTool(String(msg.params?.name), msg.params?.arguments ?? {});
71
+ process.stdout.write(frame({ jsonrpc: '2.0', id: msg.id, result: { content: [{ type: 'text', text: textOut }] } }));
72
+ }
73
+ catch (e) {
74
+ process.stdout.write(frame({ jsonrpc: '2.0', id: msg.id, error: { code: -32000, message: e instanceof Error ? e.message : String(e) } }));
75
+ }
76
+ }
77
+ else {
78
+ process.stdout.write(frame({ jsonrpc: '2.0', id: msg.id, error: { code: -32601, message: `Method not found: ${msg.method}` } }));
79
+ }
80
+ }
81
+ }