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.
- package/CHANGELOG.md +18 -0
- package/README.md +25 -17
- package/dist/audit.js +5 -2
- package/dist/cli.js +116 -12
- package/dist/drift.js +57 -0
- package/dist/imports.js +42 -0
- package/dist/map.js +13 -1
- package/dist/mcp.js +81 -0
- package/dist/scaffold.js +87 -0
- package/dist/suppress.js +51 -0
- package/docs/architecture/overview.html +3 -3
- package/docs/architecture/threat-model.html +3 -3
- package/docs/community/good-first-issues.html +3 -3
- package/docs/compatibility/matrix.html +3 -3
- package/docs/compatibility/smoke-tests/index.html +3 -3
- package/docs/compatibility/smoke-tests/results-2026-11-07.md +17 -0
- package/docs/decisions/ADR-0001-deterministic-cli-plus-ide-skills.html +3 -3
- package/docs/handbook/01-executive-summary.html +5 -5
- package/docs/handbook/01-executive-summary.md +2 -2
- package/docs/handbook/02-functional-specification.html +5 -5
- package/docs/handbook/02-functional-specification.md +2 -2
- package/docs/handbook/03-technical-specification.html +4 -4
- package/docs/handbook/03-technical-specification.md +1 -1
- package/docs/handbook/04-architecture.html +4 -4
- package/docs/handbook/04-architecture.md +1 -1
- package/docs/handbook/05-user-guide.html +6 -6
- package/docs/handbook/05-user-guide.md +3 -3
- package/docs/handbook/06-cli-reference.html +5 -5
- package/docs/handbook/06-cli-reference.md +9 -2
- package/docs/handbook/07-security-privacy-legal.html +4 -4
- package/docs/handbook/07-security-privacy-legal.md +1 -1
- package/docs/handbook/08-testing-and-proof.html +5 -5
- package/docs/handbook/08-testing-and-proof.md +2 -2
- package/docs/handbook/09-compatibility-and-roadmap.html +11 -11
- package/docs/handbook/09-compatibility-and-roadmap.md +25 -14
- package/docs/handbook/10-contributor-guide.html +4 -4
- package/docs/handbook/10-contributor-guide.md +3 -3
- package/docs/handbook/11-glossary-and-faq.html +4 -4
- package/docs/handbook/11-glossary-and-faq.md +1 -1
- package/docs/handbook/12-rule-catalog.html +3 -3
- package/docs/handbook/13-trust-and-assurance.html +5 -5
- package/docs/handbook/13-trust-and-assurance.md +3 -3
- package/docs/handbook/14-real-world-evaluation.html +3 -3
- package/docs/handbook/15-supply-chain-and-release.html +5 -5
- package/docs/handbook/15-supply-chain-and-release.md +7 -5
- package/docs/handbook/16-performance-and-quality-metrics.html +4 -4
- package/docs/handbook/16-performance-and-quality-metrics.md +1 -1
- package/docs/handbook/README.md +2 -2
- package/docs/handbook/index.html +5 -5
- package/docs/index.html +4 -4
- package/docs/legal/legal-review-brief.html +3 -3
- package/docs/legal/trademark-and-name-clearance.html +5 -5
- package/docs/legal/trademark-and-name-clearance.md +3 -1
- package/docs/proof/accessibility.html +3 -3
- package/docs/proof/test-results.txt +40 -35
- package/docs/rfc/0001-archsutra-v0.1.html +5 -5
- package/docs/rfc/0001-archsutra-v0.1.md +2 -2
- package/docs/sitemap.xml +28 -28
- package/llms.txt +6 -3
- 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)
|
|
12
12
|

|
|
13
13
|

|
|
14
|
-

|
|
15
15
|

|
|
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://
|
|
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
|
|
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
|
-
#
|
|
49
|
-
npx
|
|
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
|
|
54
|
-
npx
|
|
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
|
|
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`) | **
|
|
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.
|
|
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`
|
|
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`, `
|
|
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
|
|
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.
|
|
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
|
-
- [
|
|
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
|
-
|
|
478
|
-
const
|
|
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
|
-
|
|
30
|
-
|
|
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
|
|
37
|
-
|
|
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
|
|
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
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
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
|
+
}
|
package/dist/imports.js
ADDED
|
@@ -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
|
|
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
|
+
}
|