weavatrix 0.1.3 → 0.2.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/README.md +168 -63
- package/SECURITY.md +25 -8
- package/package.json +2 -2
- package/skill/SKILL.md +108 -39
- package/src/analysis/architecture-contract.js +343 -0
- package/src/analysis/audit-debt.js +94 -0
- package/src/analysis/change-classification.js +509 -0
- package/src/analysis/cycle-route.js +14 -0
- package/src/analysis/dead-check.js +13 -6
- package/src/analysis/dead-code-review.js +245 -0
- package/src/analysis/dep-check-ecosystems.js +163 -0
- package/src/analysis/dep-check.js +69 -147
- package/src/analysis/dep-rules.js +47 -31
- package/src/analysis/duplicates.compute.js +47 -7
- package/src/analysis/endpoints-java.js +186 -0
- package/src/analysis/endpoints-rust.js +124 -0
- package/src/analysis/endpoints.js +18 -5
- package/src/analysis/findings.js +8 -1
- package/src/analysis/git-history.js +549 -0
- package/src/analysis/git-ref-graph.js +74 -0
- package/src/analysis/graph-analysis.aggregate.js +29 -28
- package/src/analysis/graph-analysis.edges.js +24 -0
- package/src/analysis/graph-analysis.js +1 -1
- package/src/analysis/graph-analysis.summaries.js +4 -1
- package/src/analysis/http-contracts.js +581 -0
- package/src/analysis/internal-audit.collect.js +43 -2
- package/src/analysis/internal-audit.reach.js +52 -1
- package/src/analysis/internal-audit.run.js +66 -13
- package/src/analysis/java-source.js +36 -0
- package/src/analysis/package-reachability.js +39 -0
- package/src/analysis/static-test-reachability.js +133 -0
- package/src/build-graph.js +72 -13
- package/src/graph/build-worker.js +3 -4
- package/src/graph/builder/lang-java.js +177 -14
- package/src/graph/builder/lang-js.js +71 -12
- package/src/graph/builder/lang-rust.js +129 -6
- package/src/graph/community.js +27 -0
- package/src/graph/file-lock.js +69 -0
- package/src/graph/freshness-probe.js +141 -0
- package/src/graph/graph-filter.js +28 -11
- package/src/graph/incremental-refresh.js +232 -0
- package/src/graph/internal-builder.barrels.js +169 -0
- package/src/graph/internal-builder.build.js +111 -16
- package/src/graph/internal-builder.java.js +33 -0
- package/src/graph/internal-builder.langs.js +2 -1
- package/src/graph/internal-builder.resolvers.js +109 -2
- package/src/graph/layout.js +21 -5
- package/src/graph/relations.js +4 -0
- package/src/graph/repo-registry.js +124 -0
- package/src/mcp/catalog.mjs +64 -21
- package/src/mcp/evidence-snapshot.architecture.mjs +80 -0
- package/src/mcp/evidence-snapshot.common.mjs +160 -0
- package/src/mcp/evidence-snapshot.duplicates.mjs +217 -0
- package/src/mcp/evidence-snapshot.health.mjs +95 -0
- package/src/mcp/evidence-snapshot.inventory.mjs +148 -0
- package/src/mcp/evidence-snapshot.mjs +50 -0
- package/src/mcp/evidence-snapshot.package-graph.mjs +249 -0
- package/src/mcp/evidence-snapshot.structure.mjs +81 -0
- package/src/mcp/graph-context.mjs +106 -181
- package/src/mcp/graph-diff.mjs +217 -0
- package/src/mcp/staleness-notice.mjs +20 -0
- package/src/mcp/sync-evidence.mjs +418 -0
- package/src/mcp/sync-payload.mjs +194 -8
- package/src/mcp/tool-result.mjs +51 -0
- package/src/mcp/tools-actions.mjs +114 -33
- package/src/mcp/tools-architecture.mjs +144 -0
- package/src/mcp/tools-company.mjs +273 -0
- package/src/mcp/tools-graph-hubs.mjs +58 -0
- package/src/mcp/tools-graph.mjs +36 -68
- package/src/mcp/tools-health.mjs +354 -20
- package/src/mcp/tools-history.mjs +22 -0
- package/src/mcp/tools-impact-change.mjs +261 -0
- package/src/mcp/tools-impact.mjs +35 -11
- package/src/mcp-server.mjs +100 -17
- package/src/mcp-source-tools.mjs +11 -3
- package/src/path-classification.js +201 -0
- package/src/path-ignore.js +69 -0
- package/src/security/typosquat.js +1 -1
package/skill/SKILL.md
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: weavatrix
|
|
3
|
-
description: Drive the weavatrix MCP — code graph, blast-radius (get_dependents/change_impact), health audit, duplicates, coverage, endpoints — over any local repo. Use when analyzing code structure, refactor risk, dead code, dependency health, or before opening a PR.
|
|
3
|
+
description: Drive the weavatrix MCP — code graph, blast-radius (get_dependents/change_impact), dead-code review, health audit, duplicates, coverage, endpoints — over any local repo. Use when analyzing code structure, refactor risk, dead code, dependency health, or before opening a PR.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# weavatrix MCP
|
|
7
7
|
|
|
8
8
|
Structure-analysis tools over a prebuilt code graph plus the weavatrix analysis engines. The default
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
`offline` profile has every HTTP tool disabled and permits repository switching only through an
|
|
10
|
+
explicit local `open_repo` call. Use `pinned` when a shared MCP process must never change or inspect
|
|
11
|
+
outside its startup repository.
|
|
11
12
|
|
|
12
13
|
## Step 0 — if the tools are missing
|
|
13
14
|
|
|
@@ -15,45 +16,71 @@ Tools are named `mcp__weavatrix__…`. If none are available, ask the user to re
|
|
|
15
16
|
(`claude mcp add -s user weavatrix -- npx -y weavatrix <repoRoot>`; Codex:
|
|
16
17
|
`codex mcp add weavatrix -- npx -y weavatrix <repoRoot>`), then retry.
|
|
17
18
|
|
|
18
|
-
If
|
|
19
|
-
|
|
19
|
+
If `refresh_advisories`, `pull_architecture_contract`, or `sync_graph` is missing, do not diagnose a
|
|
20
|
+
broken installation: network tools are intentionally absent from `offline` and `pinned`.
|
|
21
|
+
|
|
22
|
+
Profiles use the same npm package and binary:
|
|
23
|
+
|
|
24
|
+
- `offline` (default): all local analysis and explicit `open_repo`; no HTTP tools.
|
|
25
|
+
- `pinned`: local analysis with no `open_repo`, no global/cross-repository graph access, and no HTTP tools.
|
|
26
|
+
- `osv`: `offline` plus explicit `refresh_advisories`.
|
|
27
|
+
- `hosted` / `full`: `osv` plus `pull_architecture_contract` and `sync_graph`.
|
|
28
|
+
|
|
29
|
+
The legacy `online` capability remains a compatibility alias for `advisories,hosted`; prefer the
|
|
30
|
+
named profiles for new registrations.
|
|
20
31
|
|
|
21
32
|
## Ground rules
|
|
22
33
|
|
|
23
34
|
- **Evidence, not verdicts**: treat audit, hub, orphan and duplicate output as hypotheses. Confirm a
|
|
24
35
|
finding in source and check framework/runtime conventions before deleting, merging or redesigning
|
|
25
36
|
code. A same-name/different-body pair is a divergence candidate, not proof of duplication.
|
|
26
|
-
- **Freshness**:
|
|
27
|
-
|
|
28
|
-
|
|
37
|
+
- **Freshness**: graph/health calls automatically reconcile the active graph, while cross-repository
|
|
38
|
+
tracing reconciles every selected registered graph. Read the structured `refresh` /
|
|
39
|
+
`graphReconciliation` status: `none`, `incremental`, `full`, or explicitly `PARTIAL`. Use
|
|
40
|
+
`rebuild_graph` only when automatic reconciliation reports a fallback/error or when intentionally
|
|
41
|
+
changing build mode. A normal `open_repo` builds missing graphs and upgrades legacy schemas;
|
|
42
|
+
`build:false` deliberately refuses that upgrade.
|
|
29
43
|
- **Ambiguity**: `get_node`/`get_neighbors`/`get_dependents` disclose `matched N nodes; using the
|
|
30
44
|
best-connected` — read that note before trusting the answer; pass an exact node id to pin it.
|
|
31
|
-
- **Runtime versus compile time**: keep runtime cycles
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
45
|
+
- **Runtime versus compile time**: keep runtime cycles separate from TypeScript type-only and
|
|
46
|
+
language compile-only coupling (Rust `mod`/`use`/`pub use`, Java imports). `module_map`,
|
|
47
|
+
`change_impact` and `graph_diff` label the distinction; `god_nodes` ranks unique connectivity and
|
|
48
|
+
reports repeated references separately. Do not schedule a runtime-cycle refactor from
|
|
49
|
+
compile-time-only edges.
|
|
35
50
|
- **Repository universe**: in Git repositories, graph and duplicate scans include tracked plus
|
|
36
51
|
non-ignored untracked files. If an old graph still contains packaged/generated output, rebuild it
|
|
37
|
-
before interpreting the result.
|
|
52
|
+
before interpreting the result. Repository-root `.weavatrixignore` applies the same tracked-file
|
|
53
|
+
exclusions to graph, audit and duplicate passes; use it for generated/e2e fixtures that must stay
|
|
54
|
+
committed. `no-tests` also recognizes Cypress, Playwright, `test-e2e`, acceptance and integration
|
|
55
|
+
roots. Dead-code/clone/audit review also suppresses `benchmarks/**` and `**/__temp/**` as classified
|
|
56
|
+
non-production surfaces. A verified production benchmark can opt back in narrowly through
|
|
57
|
+
`.weavatrix.json` `classify.product`, for example `{"classify":{"product":["benchmarks/core/**"]}}`.
|
|
58
|
+
- **Architectural queries**: for bootstrap/routing/authentication questions, inspect the returned
|
|
59
|
+
seeds before trusting the traversal. Pass exact repository-relative `seed_files` to `query_graph`
|
|
60
|
+
when the intended entry points are already known.
|
|
38
61
|
- **Coverage**: `coverage_map` reads an existing report. `unavailable` means no supported report was
|
|
39
62
|
found, not 0% coverage; do not rank testing risk from that state.
|
|
40
63
|
- **Audit completeness**: read `checks.osv.status` and `checks.malware.status`. For OSV, `OK` is
|
|
41
64
|
complete for the recorded dependency fingerprint; `PARTIAL` is incomplete or stale,
|
|
42
65
|
`NOT_CHECKED` has no repository-specific result, and `ERROR` means the local check failed. Treat
|
|
43
66
|
the same non-`OK` states as incomplete for malware scanning. Refresh OSV only when the user has
|
|
44
|
-
authorized
|
|
67
|
+
authorized selecting the optional `osv` profile (or `advisories` capability) and then
|
|
45
68
|
invoking `refresh_advisories`; enabling the group alone sends nothing.
|
|
46
69
|
- **Offline by design**: scans and graph queries run in-process against local files; coverage tools
|
|
47
|
-
read existing reports and never run tests. The ONLY network-touching tools live in the
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
70
|
+
read existing reports and never run tests. The ONLY network-touching tools live in the optional
|
|
71
|
+
`advisories` / `hosted` capabilities and run solely when explicitly called:
|
|
72
|
+
`refresh_advisories` (queries OSV.dev with
|
|
73
|
+
package names + versions so `run_audit` has fresh vulnerability data) and `sync_graph` (derives a
|
|
74
|
+
bounded evidence snapshot locally, then pushes only an allowlisted graph/evidence contract to a
|
|
75
|
+
user-configured endpoint; analyzers may read local source, but the wire contract has no body,
|
|
76
|
+
snippet, absolute-host-path or environment fields, and unknown fields are discarded; disabled until
|
|
77
|
+
`WEAVATRIX_SYNC_URL` is set). `pull_architecture_contract` sends only the active repository's opaque
|
|
78
|
+
stable UUID, downloads the owner-approved contract, validates it, and caches it locally; it requires
|
|
79
|
+
`WEAVATRIX_SYNC_URL` and `WEAVATRIX_SYNC_TOKEN`. All three are absent from the default profile.
|
|
54
80
|
- **Repository boundary**: source reads and graph-derived paths are realpath-contained. `open_repo`
|
|
55
|
-
intentionally changes the active boundary through an explicit offline tool call. It is
|
|
56
|
-
|
|
81
|
+
intentionally changes the active boundary through an explicit offline tool call. It is included in
|
|
82
|
+
`offline`, absent from `pinned`, and available in a custom registration only when `retarget` is
|
|
83
|
+
named. Concurrent non-mutating calls retain the graph/root snapshot with which they started.
|
|
57
84
|
|
|
58
85
|
## Recipes
|
|
59
86
|
|
|
@@ -64,12 +91,37 @@ If only `refresh_advisories` or `sync_graph` is missing, do not diagnose a broke
|
|
|
64
91
|
and untracked work, coverage attached, untested hotspots called out) → drill with `get_dependents`.
|
|
65
92
|
- **Impact of a PR that is NOT checked out**: pass its changed-file list explicitly —
|
|
66
93
|
`change_impact files=[…]` — same blast-radius + coverage view, no checkout.
|
|
67
|
-
- **Validate a refactor structurally**:
|
|
68
|
-
|
|
69
|
-
`
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
- **
|
|
94
|
+
- **Validate a refactor structurally**: call `graph_diff base_ref=HEAD~1` (or `main` / `origin/main`)
|
|
95
|
+
to build an immutable baseline without checkout and compare it with the current graph. Alternatively,
|
|
96
|
+
edit → `rebuild_graph`, then use `graph_diff` without `base_ref` to compare `graph.prev.json` with
|
|
97
|
+
the rebuilt graph. Either route can be scoped by `path`; look for cycle, module-dependency and
|
|
98
|
+
orphan drift rather than raw edge counts.
|
|
99
|
+
- **Health sweep**: for a branch/PR review prefer
|
|
100
|
+
`run_audit base_ref=<merge-base> debt=new changed_files=[…]`; for repository maintenance use
|
|
101
|
+
`run_audit debt=all`. Then call `find_dead_code`, `find_duplicates` and `coverage_map`. A changed-file-only audit is
|
|
102
|
+
scope, not proof of new debt. Inspect the source behind shortlisted findings before proposing edits.
|
|
103
|
+
- **Dead-code and dead-method review**: call `find_dead_code` for a bounded production-code queue of
|
|
104
|
+
files, functions, methods and symbols. Narrow with `path` or `kinds=["method"]`; keep the default
|
|
105
|
+
`min_confidence=medium` for actionable internal candidates. Use `min_confidence=low` only when the
|
|
106
|
+
task explicitly needs public/exported, framework, dynamic-loading or reflection-sensitive review,
|
|
107
|
+
and carry each returned caveat into the recommendation. Pair it with
|
|
108
|
+
`run_audit category=unused debt=all` (or `base_ref=<merge-base> debt=new`) for unused exports and
|
|
109
|
+
dependencies. Before deletion, run `read_source`, `get_dependents`, exact `search_code`, inspect
|
|
110
|
+
framework discovery/annotations, package scripts, CLI/Docker/manifests, generated consumers and
|
|
111
|
+
test-only use, then run the repository tests. `REVIEW_REQUIRED` and `autoDelete:false` are hard
|
|
112
|
+
safety semantics, not boilerplate. Use `.weavatrix-deps.json` entrypoints/nonRuntimeRoots only for
|
|
113
|
+
verified conventions.
|
|
114
|
+
- **API inventory**: `list_endpoints` (including Next.js App Router, Rust axum and actix-web).
|
|
115
|
+
- **Cross-repository API impact**: ensure both repositories are in `list_known_repos`, then call
|
|
116
|
+
`trace_api_contract backend=<uuid-or-label> clients=[<uuid-or-label>]`; narrow with `method`, `path`,
|
|
117
|
+
or backend `changed_files`. Treat unresolved/dynamic URLs as incomplete evidence, not a clean result.
|
|
118
|
+
- **Target architecture before editing**: `get_architecture_contract` → `prepare_change` with the
|
|
119
|
+
intended files → edit and rebuild → `verify_architecture`. A missing contract returns a starter
|
|
120
|
+
proposal, not an automatically approved architecture. Pull an owner-approved hosted contract only
|
|
121
|
+
when the user selected `hosted` and explicitly asks for it.
|
|
122
|
+
- **Behavioral architecture**: `git_history` ranks churn × connectivity hotspots and hidden
|
|
123
|
+
co-change coupling from bounded local numstat history. Use it as review evidence, not proof that two
|
|
124
|
+
files must be merged.
|
|
73
125
|
- **Find code**: `search_code` (regex + glob) → `get_node` → `read_source`.
|
|
74
126
|
- **Another repo**: `list_known_repos` → `open_repo <path>`
|
|
75
127
|
(builds or upgrades the graph when needed; `build:false` probes without building).
|
|
@@ -77,13 +129,15 @@ If only `refresh_advisories` or `sync_graph` is missing, do not diagnose a broke
|
|
|
77
129
|
## Repository-specific conventions
|
|
78
130
|
|
|
79
131
|
Weavatrix understands nearest workspace manifests, nested `tsconfig`/`jsconfig` aliases,
|
|
80
|
-
framework-owned runtime peers such as Next.js + `react-dom`,
|
|
81
|
-
For project-specific entry points
|
|
82
|
-
`.weavatrix-deps.json` at the repository
|
|
132
|
+
framework-owned runtime peers such as Next.js + `react-dom`, generated NAPI-RS platform loaders,
|
|
133
|
+
and Next.js App Router route exports. For project-specific entry points, reusable template catalogs,
|
|
134
|
+
or Python dependencies supplied by an external runtime, add `.weavatrix-deps.json` at the repository
|
|
135
|
+
root:
|
|
83
136
|
|
|
84
137
|
```json
|
|
85
138
|
{
|
|
86
139
|
"entrypoints": ["scripts/publish-release.mjs"],
|
|
140
|
+
"nonRuntimeRoots": ["library", "catalogs/examples"],
|
|
87
141
|
"python": {
|
|
88
142
|
"managedDependencies": ["numpy", "openvino-genai"],
|
|
89
143
|
"ignoreDependencies": ["vendor-sdk"]
|
|
@@ -91,22 +145,37 @@ For project-specific entry points or Python dependencies supplied by an external
|
|
|
91
145
|
}
|
|
92
146
|
```
|
|
93
147
|
|
|
94
|
-
Keep exceptions narrow. `
|
|
95
|
-
|
|
148
|
+
Keep exceptions narrow. `nonRuntimeRoots` (alias `templateRoots`) marks reusable examples that are
|
|
149
|
+
not one deployed application. It suppresses orphan/dead/unused-export noise and missing/unresolved
|
|
150
|
+
dependency findings when every use is inside those roots; graph edges, cycles and boundaries remain
|
|
151
|
+
visible. `managedDependencies` documents modules provided outside the repo's Python manifest;
|
|
152
|
+
`ignoreDependencies` suppresses intentionally unresolved imports.
|
|
96
153
|
|
|
97
154
|
## Sync
|
|
98
155
|
|
|
99
|
-
`sync_graph`
|
|
100
|
-
|
|
101
|
-
|
|
156
|
+
`sync_graph` defaults to payload v3: graph metadata plus deterministic architecture, health, stack,
|
|
157
|
+
package-dependency and clone-review evidence. Read each section's `state`, `verdict` and completeness counts; `PARTIAL`,
|
|
158
|
+
`NOT_CHECKED` and `ERROR` are unknown/incomplete, never a clean result. Architecture evidence
|
|
159
|
+
contains concrete runtime versus compile-time cycles, declared boundary violations and separated
|
|
160
|
+
runtime/type-only/compile-only module dependencies. Package evidence contains a bounded lockfile
|
|
161
|
+
graph with direct/transitive runtime, dev, optional and peer edges plus explicit resolution counts.
|
|
162
|
+
Duplicate evidence contains stable, source-free clone/divergence candidates; it never sends method
|
|
163
|
+
bodies or snippets. Use `payload_version: 2` only when the user explicitly wants graph-only
|
|
164
|
+
compatibility—Weavatrix never silently downgrades. A graph built before `0.1.4`, or one stale against
|
|
165
|
+
the working tree, must be rebuilt first. Sync remains unavailable until the user selects `hosted`
|
|
166
|
+
(or the exact `hosted` capability) and configures `WEAVATRIX_SYNC_URL`.
|
|
102
167
|
|
|
103
168
|
## Troubleshooting
|
|
104
169
|
|
|
105
|
-
- `Graph unavailable` → `rebuild_graph`;
|
|
170
|
+
- `Graph unavailable` → `rebuild_graph`; normal graph/health calls automatically refresh an existing
|
|
171
|
+
graph and report `none`, `incremental`, or `full`. `open_repo` can select another valid repository path unless
|
|
106
172
|
the registration deliberately omitted `retarget`.
|
|
107
173
|
- `refresh_advisories` is unavailable → with the user's approval, re-register/reconfigure the MCP
|
|
108
|
-
|
|
174
|
+
with the `osv` profile, reconnect it, and then invoke the tool. Do not enable network
|
|
109
175
|
access merely to turn `NOT_CHECKED` into a cosmetic green state.
|
|
176
|
+
- `pull_architecture_contract` / `sync_graph` is unavailable → use the `hosted` profile only after
|
|
177
|
+
the user chooses hosted integration; configure `WEAVATRIX_SYNC_URL` and a bearer token for contract
|
|
178
|
+
pull. Profile selection alone never performs a request.
|
|
110
179
|
- `No coverage report` → run the repo's own tests with coverage (`vitest run --coverage`,
|
|
111
180
|
`jest --coverage`, `pytest --cov --cov-report=json`, `go test -coverprofile=coverage.out`),
|
|
112
181
|
then re-call.
|
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
// Executable target-architecture contract and no-regressions ratchet.
|
|
2
|
+
// The contract contains selectors and budgets only; verification is pure over graph metadata and never
|
|
3
|
+
// needs source bodies. Fingerprints deliberately exclude line numbers so edits do not churn baselines.
|
|
4
|
+
import {createHash} from 'node:crypto'
|
|
5
|
+
import {existsSync, readFileSync, writeFileSync, mkdirSync} from 'node:fs'
|
|
6
|
+
import {dirname, join} from 'node:path'
|
|
7
|
+
import {createRepoBoundary} from '../repo-path.js'
|
|
8
|
+
|
|
9
|
+
export const ARCHITECTURE_CONTRACT_V = 1
|
|
10
|
+
export const CONTRACT_PATHS = ['.weavatrix/architecture.json', '.weavatrix-architecture.json']
|
|
11
|
+
|
|
12
|
+
const RELATION_KIND = new Set(['runtime', 'type-only', 'compile-only', 'any'])
|
|
13
|
+
const ACTION = new Set(['allow', 'forbid'])
|
|
14
|
+
const ENFORCEMENT = new Set(['ratchet', 'strict', 'advisory'])
|
|
15
|
+
const safeId = (value, fallback = '') => {
|
|
16
|
+
const text = String(value ?? '').trim()
|
|
17
|
+
return /^[a-z0-9][a-z0-9._:-]{0,127}$/i.test(text) ? text : fallback
|
|
18
|
+
}
|
|
19
|
+
const pathPrefix = (value) => {
|
|
20
|
+
const text = String(value ?? '').replace(/\\/g, '/').replace(/^\.\//, '').replace(/^\/+|\/+$/g, '')
|
|
21
|
+
return text && !text.split('/').some((part) => !part || part === '.' || part === '..') ? text : null
|
|
22
|
+
}
|
|
23
|
+
const stringList = (value, sanitize, cap = 100) => [...new Set((Array.isArray(value) ? value : [])
|
|
24
|
+
.slice(0, cap).map(sanitize).filter(Boolean))]
|
|
25
|
+
const finiteBudget = (value, fallback = null) => {
|
|
26
|
+
const number = Number(value)
|
|
27
|
+
return Number.isFinite(number) && number >= 0 ? number : fallback
|
|
28
|
+
}
|
|
29
|
+
const endpoint = (value) => value && typeof value === 'object' ? String(value.id) : String(value ?? '')
|
|
30
|
+
const isSymbol = (id) => String(id).includes('#')
|
|
31
|
+
const fileOf = (id, byId) => {
|
|
32
|
+
const node = byId.get(String(id))
|
|
33
|
+
if (node?.source_file) return String(node.source_file).replace(/\\/g, '/')
|
|
34
|
+
return String(id).split('#')[0].replace(/\\/g, '/')
|
|
35
|
+
}
|
|
36
|
+
const stable = (value) => Array.isArray(value)
|
|
37
|
+
? `[${value.map(stable).join(',')}]`
|
|
38
|
+
: value && typeof value === 'object'
|
|
39
|
+
? `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stable(value[key])}`).join(',')}}`
|
|
40
|
+
: JSON.stringify(value)
|
|
41
|
+
const hash = (value) => createHash('sha256').update(stable(value)).digest('hex')
|
|
42
|
+
|
|
43
|
+
function sanitizeComponent(value, index) {
|
|
44
|
+
const id = safeId(value?.id, `component-${index + 1}`)
|
|
45
|
+
const paths = stringList(value?.paths ?? [value?.path], pathPrefix, 64)
|
|
46
|
+
if (!paths.length) return null
|
|
47
|
+
return {id, name: String(value?.name || id).slice(0, 128), paths}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function sanitizeRule(value, index) {
|
|
51
|
+
const from = stringList(value?.from, (item) => item === '*' ? '*' : safeId(item), 64)
|
|
52
|
+
const to = stringList(value?.to, (item) => item === '*' ? '*' : safeId(item), 64)
|
|
53
|
+
if (!from.length || !to.length) return null
|
|
54
|
+
return {
|
|
55
|
+
id: safeId(value?.id, `dependency-${index + 1}`),
|
|
56
|
+
action: ACTION.has(value?.action) ? value.action : 'forbid',
|
|
57
|
+
kinds: stringList(value?.kinds ?? ['runtime'], (kind) => RELATION_KIND.has(kind) ? kind : '', 4),
|
|
58
|
+
from,
|
|
59
|
+
to,
|
|
60
|
+
...(value?.reason ? {reason: String(value.reason).slice(0, 300)} : {}),
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function sanitizeException(value) {
|
|
65
|
+
const fingerprint = /^[a-f0-9]{16,64}$/i.test(String(value?.fingerprint || '')) ? String(value.fingerprint) : null
|
|
66
|
+
if (!fingerprint) return null
|
|
67
|
+
const expires = /^\d{4}-\d{2}-\d{2}$/.test(String(value?.expires || '')) ? String(value.expires) : null
|
|
68
|
+
return {fingerprint, reason: String(value?.reason || '').slice(0, 300), ...(expires ? {expires} : {})}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function normalizeArchitectureContract(input) {
|
|
72
|
+
if (!input || typeof input !== 'object' || Array.isArray(input)) throw new Error('architecture contract must be an object')
|
|
73
|
+
const components = (Array.isArray(input.components) ? input.components : [])
|
|
74
|
+
.slice(0, 200).map(sanitizeComponent).filter(Boolean)
|
|
75
|
+
const componentIds = new Set(components.map((item) => item.id))
|
|
76
|
+
const dependencyRules = (Array.isArray(input.dependencyRules) ? input.dependencyRules : [])
|
|
77
|
+
.slice(0, 500).map(sanitizeRule).filter(Boolean)
|
|
78
|
+
.filter((rule) => [...rule.from, ...rule.to].every((id) => id === '*' || componentIds.has(id)))
|
|
79
|
+
const rawBudgets = input.budgets && typeof input.budgets === 'object' ? input.budgets : {}
|
|
80
|
+
const budgets = {
|
|
81
|
+
runtimeCycles: finiteBudget(rawBudgets.runtimeCycles),
|
|
82
|
+
maxFunctionLoc: finiteBudget(rawBudgets.maxFunctionLoc),
|
|
83
|
+
maxFileLoc: finiteBudget(rawBudgets.maxFileLoc),
|
|
84
|
+
maxCyclomatic: finiteBudget(rawBudgets.maxCyclomatic),
|
|
85
|
+
maxParams: finiteBudget(rawBudgets.maxParams),
|
|
86
|
+
maxRuntimeDependenciesPerComponent: finiteBudget(rawBudgets.maxRuntimeDependenciesPerComponent),
|
|
87
|
+
maxModuleFiles: finiteBudget(rawBudgets.maxModuleFiles),
|
|
88
|
+
minModuleCohesion: finiteBudget(rawBudgets.minModuleCohesion),
|
|
89
|
+
maxModuleBoundaryRatio: finiteBudget(rawBudgets.maxModuleBoundaryRatio),
|
|
90
|
+
}
|
|
91
|
+
for (const key of Object.keys(budgets)) if (budgets[key] == null) delete budgets[key]
|
|
92
|
+
const technologies = {
|
|
93
|
+
required: stringList(input.technologies?.required, (item) => safeId(item), 100),
|
|
94
|
+
forbidden: stringList(input.technologies?.forbidden, (item) => safeId(item), 100),
|
|
95
|
+
}
|
|
96
|
+
const baseline = input.ratchet?.baseline && typeof input.ratchet.baseline === 'object'
|
|
97
|
+
? {
|
|
98
|
+
fingerprints: stringList(input.ratchet.baseline.fingerprints, (item) => /^[a-f0-9]{16,64}$/i.test(String(item)) ? String(item) : '', 5_000),
|
|
99
|
+
metrics: Object.fromEntries(Object.entries(input.ratchet.baseline.metrics || {}).slice(0, 500)
|
|
100
|
+
.map(([key, value]) => [safeId(key), finiteBudget(value)]).filter(([key, value]) => key && value != null)),
|
|
101
|
+
}
|
|
102
|
+
: {fingerprints: [], metrics: {}}
|
|
103
|
+
const contract = {
|
|
104
|
+
architectureContractV: ARCHITECTURE_CONTRACT_V,
|
|
105
|
+
name: String(input.name || 'Target architecture').slice(0, 128),
|
|
106
|
+
style: safeId(input.style, 'custom'),
|
|
107
|
+
enforcement: ENFORCEMENT.has(input.enforcement) ? input.enforcement : 'ratchet',
|
|
108
|
+
components,
|
|
109
|
+
dependencyRules,
|
|
110
|
+
budgets,
|
|
111
|
+
technologies,
|
|
112
|
+
exceptions: (Array.isArray(input.exceptions) ? input.exceptions : []).slice(0, 500).map(sanitizeException).filter(Boolean),
|
|
113
|
+
ratchet: {baseline},
|
|
114
|
+
}
|
|
115
|
+
return {...contract, contractHash: hash(contract)}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function loadArchitectureContract(repoRoot, graphPath) {
|
|
119
|
+
const boundary = createRepoBoundary(repoRoot)
|
|
120
|
+
for (const relative of CONTRACT_PATHS) {
|
|
121
|
+
const resolved = boundary.resolve(relative)
|
|
122
|
+
if (!resolved.ok || !existsSync(resolved.path)) continue
|
|
123
|
+
try { return {contract: normalizeArchitectureContract(JSON.parse(readFileSync(resolved.path, 'utf8'))), source: relative} }
|
|
124
|
+
catch (error) { return {contract: null, source: relative, error: error.message} }
|
|
125
|
+
}
|
|
126
|
+
const cached = graphPath ? join(dirname(graphPath), 'architecture.contract.json') : null
|
|
127
|
+
if (cached && existsSync(cached)) {
|
|
128
|
+
try { return {contract: normalizeArchitectureContract(JSON.parse(readFileSync(cached, 'utf8'))), source: 'hosted-cache'} }
|
|
129
|
+
catch (error) { return {contract: null, source: 'hosted-cache', error: error.message} }
|
|
130
|
+
}
|
|
131
|
+
return {contract: null, source: null, error: null}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function writeCachedArchitectureContract(graphPath, input) {
|
|
135
|
+
if (!graphPath) throw new Error('graph path is required for hosted contract cache')
|
|
136
|
+
const contract = normalizeArchitectureContract(input)
|
|
137
|
+
const path = join(dirname(graphPath), 'architecture.contract.json')
|
|
138
|
+
mkdirSync(dirname(path), {recursive: true})
|
|
139
|
+
writeFileSync(path, JSON.stringify(contract, null, 2), 'utf8')
|
|
140
|
+
return {path, contract}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function componentFor(file, components) {
|
|
144
|
+
const normalized = String(file || '').replace(/\\/g, '/')
|
|
145
|
+
let best = null
|
|
146
|
+
for (const component of components) for (const prefix of component.paths) {
|
|
147
|
+
if (normalized !== prefix && !normalized.startsWith(`${prefix}/`)) continue
|
|
148
|
+
if (!best || prefix.length > best.prefix.length) best = {id: component.id, prefix}
|
|
149
|
+
}
|
|
150
|
+
return best?.id || '(unmapped)'
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function relationKind(link) {
|
|
154
|
+
if (link.typeOnly === true) return 'type-only'
|
|
155
|
+
if (link.compileOnly === true) return 'compile-only'
|
|
156
|
+
return 'runtime'
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function runtimeFileGraph(graph) {
|
|
160
|
+
const byId = new Map((graph.nodes || []).map((node) => [String(node.id), node]))
|
|
161
|
+
const files = new Set((graph.nodes || []).filter((node) => !isSymbol(node.id)).map((node) => String(node.id)))
|
|
162
|
+
const adjacency = new Map([...files].map((file) => [file, new Set()]))
|
|
163
|
+
for (const link of graph.links || []) {
|
|
164
|
+
if (relationKind(link) !== 'runtime' || !['imports', 're_exports'].includes(link.relation) || link.barrelProxy === true) continue
|
|
165
|
+
const source = fileOf(endpoint(link.source), byId)
|
|
166
|
+
const target = fileOf(endpoint(link.target), byId)
|
|
167
|
+
if (source && target && source !== target && files.has(source) && files.has(target)) adjacency.get(source)?.add(target)
|
|
168
|
+
}
|
|
169
|
+
return adjacency
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function stronglyConnected(adjacency) {
|
|
173
|
+
let index = 0
|
|
174
|
+
const indexes = new Map(), low = new Map(), stack = [], onStack = new Set(), out = []
|
|
175
|
+
const visit = (node) => {
|
|
176
|
+
indexes.set(node, index); low.set(node, index); index++; stack.push(node); onStack.add(node)
|
|
177
|
+
for (const target of adjacency.get(node) || []) {
|
|
178
|
+
if (!indexes.has(target)) { visit(target); low.set(node, Math.min(low.get(node), low.get(target))) }
|
|
179
|
+
else if (onStack.has(target)) low.set(node, Math.min(low.get(node), indexes.get(target)))
|
|
180
|
+
}
|
|
181
|
+
if (low.get(node) !== indexes.get(node)) return
|
|
182
|
+
const component = []
|
|
183
|
+
while (stack.length) { const value = stack.pop(); onStack.delete(value); component.push(value); if (value === node) break }
|
|
184
|
+
if (component.length > 1 || adjacency.get(node)?.has(node)) out.push(component.sort())
|
|
185
|
+
}
|
|
186
|
+
for (const node of [...adjacency.keys()].sort()) if (!indexes.has(node)) visit(node)
|
|
187
|
+
return out.sort((a, b) => b.length - a.length || a[0].localeCompare(b[0]))
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function violation(ruleId, kind, evidence, current, target) {
|
|
191
|
+
const normalizedEvidence = String(evidence).replace(/:\d+(?=\b|$)/g, '')
|
|
192
|
+
const fingerprint = hash({ruleId, kind, evidence: normalizedEvidence}).slice(0, 32)
|
|
193
|
+
return {fingerprint, ruleId, kind, evidence, ...(current != null ? {current} : {}), ...(target != null ? {target} : {})}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function matchSelector(selector, value) { return selector.includes('*') || selector.includes(value) }
|
|
197
|
+
|
|
198
|
+
export function verifyArchitecture({graph, contract: rawContract, technologies = []}) {
|
|
199
|
+
const contract = rawContract?.contractHash ? rawContract : normalizeArchitectureContract(rawContract)
|
|
200
|
+
const nodes = Array.isArray(graph?.nodes) ? graph.nodes : []
|
|
201
|
+
const links = Array.isArray(graph?.links) ? graph.links : []
|
|
202
|
+
const byId = new Map(nodes.map((node) => [String(node.id), node]))
|
|
203
|
+
const exceptions = new Set(contract.exceptions
|
|
204
|
+
.filter((item) => !item.expires || item.expires >= new Date().toISOString().slice(0, 10))
|
|
205
|
+
.map((item) => item.fingerprint))
|
|
206
|
+
const violations = []
|
|
207
|
+
const componentEdges = new Map()
|
|
208
|
+
for (const link of links) {
|
|
209
|
+
if (!['imports', 're_exports', 'calls', 'references'].includes(link.relation) || link.barrelProxy === true) continue
|
|
210
|
+
const fromFile = fileOf(endpoint(link.source), byId), toFile = fileOf(endpoint(link.target), byId)
|
|
211
|
+
if (!fromFile || !toFile || fromFile === toFile) continue
|
|
212
|
+
const from = componentFor(fromFile, contract.components), to = componentFor(toFile, contract.components)
|
|
213
|
+
if (from === to) continue
|
|
214
|
+
const kind = relationKind(link)
|
|
215
|
+
const key = `${from}\0${to}\0${kind}`
|
|
216
|
+
const record = componentEdges.get(key) || {from, to, kind, count: 0, samples: []}
|
|
217
|
+
record.count++
|
|
218
|
+
if (record.samples.length < 5) record.samples.push(`${fromFile} -> ${toFile}`)
|
|
219
|
+
componentEdges.set(key, record)
|
|
220
|
+
}
|
|
221
|
+
for (const edge of componentEdges.values()) {
|
|
222
|
+
const applicable = contract.dependencyRules.filter((rule) =>
|
|
223
|
+
rule.kinds.some((kind) => kind === 'any' || kind === edge.kind) && matchSelector(rule.from, edge.from))
|
|
224
|
+
for (const rule of applicable) {
|
|
225
|
+
if (rule.action === 'forbid' && matchSelector(rule.to, edge.to)) {
|
|
226
|
+
violations.push(violation(rule.id, 'dependency', `${edge.from} -> ${edge.to} (${edge.kind})`, edge.count, 0))
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
const allowRules = applicable.filter((rule) => rule.action === 'allow')
|
|
230
|
+
if (allowRules.length && !allowRules.some((rule) => matchSelector(rule.to, edge.to))) {
|
|
231
|
+
const ruleId = allowRules.map((rule) => rule.id).sort().join('+')
|
|
232
|
+
violations.push(violation(ruleId, 'dependency', `${edge.from} -> ${edge.to} (${edge.kind}; outside allow-list)`, edge.count, 0))
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
const runtimeCycles = stronglyConnected(runtimeFileGraph(graph))
|
|
236
|
+
if (contract.budgets.runtimeCycles != null && runtimeCycles.length > contract.budgets.runtimeCycles) {
|
|
237
|
+
violations.push(violation('budget.runtimeCycles', 'budget', `runtime cycles: ${runtimeCycles.length}`, runtimeCycles.length, contract.budgets.runtimeCycles))
|
|
238
|
+
}
|
|
239
|
+
const componentRuntimeTargets = new Map()
|
|
240
|
+
for (const edge of componentEdges.values()) if (edge.kind === 'runtime') {
|
|
241
|
+
const targets = componentRuntimeTargets.get(edge.from) || new Set(); targets.add(edge.to); componentRuntimeTargets.set(edge.from, targets)
|
|
242
|
+
}
|
|
243
|
+
if (contract.budgets.maxRuntimeDependenciesPerComponent != null) for (const [component, targets] of componentRuntimeTargets) {
|
|
244
|
+
if (targets.size > contract.budgets.maxRuntimeDependenciesPerComponent) violations.push(violation(
|
|
245
|
+
'budget.maxRuntimeDependenciesPerComponent', 'budget', `${component} runtime dependencies`, targets.size,
|
|
246
|
+
contract.budgets.maxRuntimeDependenciesPerComponent,
|
|
247
|
+
))
|
|
248
|
+
}
|
|
249
|
+
const componentStats = new Map(contract.components.map((component) => [component.id, {
|
|
250
|
+
files: new Set(), internalPairs: new Set(), boundaryPairs: new Set(),
|
|
251
|
+
}]))
|
|
252
|
+
for (const node of nodes.filter((item) => !isSymbol(item.id))) {
|
|
253
|
+
const file = String(node.source_file || node.id).replace(/\\/g, '/')
|
|
254
|
+
const component = componentFor(file, contract.components)
|
|
255
|
+
componentStats.get(component)?.files.add(file)
|
|
256
|
+
}
|
|
257
|
+
const runtimePairs = new Set()
|
|
258
|
+
for (const link of links) {
|
|
259
|
+
if (relationKind(link) !== 'runtime' || !['imports', 're_exports', 'calls', 'references'].includes(link.relation) || link.barrelProxy === true) continue
|
|
260
|
+
const source = fileOf(endpoint(link.source), byId), target = fileOf(endpoint(link.target), byId)
|
|
261
|
+
if (!source || !target || source === target) continue
|
|
262
|
+
const pair = `${source}\0${target}`
|
|
263
|
+
if (runtimePairs.has(pair)) continue
|
|
264
|
+
runtimePairs.add(pair)
|
|
265
|
+
const from = componentFor(source, contract.components), to = componentFor(target, contract.components)
|
|
266
|
+
if (from === to) componentStats.get(from)?.internalPairs.add(pair)
|
|
267
|
+
else {
|
|
268
|
+
componentStats.get(from)?.boundaryPairs.add(pair)
|
|
269
|
+
componentStats.get(to)?.boundaryPairs.add(pair)
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
for (const [component, stats] of componentStats) {
|
|
273
|
+
const internal = stats.internalPairs.size, boundary = stats.boundaryPairs.size
|
|
274
|
+
const cohesion = internal + boundary ? internal / (internal + boundary) : 1
|
|
275
|
+
const boundaryRatio = internal + boundary ? boundary / (internal + boundary) : 0
|
|
276
|
+
if (contract.budgets.maxModuleFiles != null && stats.files.size > contract.budgets.maxModuleFiles) violations.push(violation('budget.maxModuleFiles', 'budget', `${component} files`, stats.files.size, contract.budgets.maxModuleFiles))
|
|
277
|
+
if (contract.budgets.minModuleCohesion != null && cohesion < contract.budgets.minModuleCohesion) violations.push(violation('budget.minModuleCohesion', 'budget', `${component} cohesion`, Number(cohesion.toFixed(4)), contract.budgets.minModuleCohesion))
|
|
278
|
+
if (contract.budgets.maxModuleBoundaryRatio != null && boundaryRatio > contract.budgets.maxModuleBoundaryRatio) violations.push(violation('budget.maxModuleBoundaryRatio', 'budget', `${component} boundary ratio`, Number(boundaryRatio.toFixed(4)), contract.budgets.maxModuleBoundaryRatio))
|
|
279
|
+
}
|
|
280
|
+
for (const node of nodes) {
|
|
281
|
+
const loc = finiteBudget(node?.complexity?.loc)
|
|
282
|
+
const cyclomatic = finiteBudget(node?.complexity?.cyclomatic)
|
|
283
|
+
const params = finiteBudget(node?.complexity?.params)
|
|
284
|
+
const evidence = `${node.source_file || fileOf(node.id, byId)}#${node.label || node.id}`
|
|
285
|
+
if (contract.budgets.maxFunctionLoc != null && loc != null && loc > contract.budgets.maxFunctionLoc) violations.push(violation('budget.maxFunctionLoc', 'budget', evidence, loc, contract.budgets.maxFunctionLoc))
|
|
286
|
+
if (contract.budgets.maxCyclomatic != null && cyclomatic != null && cyclomatic > contract.budgets.maxCyclomatic) violations.push(violation('budget.maxCyclomatic', 'budget', evidence, cyclomatic, contract.budgets.maxCyclomatic))
|
|
287
|
+
if (contract.budgets.maxParams != null && params != null && params > contract.budgets.maxParams) violations.push(violation('budget.maxParams', 'budget', evidence, params, contract.budgets.maxParams))
|
|
288
|
+
}
|
|
289
|
+
if (contract.budgets.maxFileLoc != null) for (const node of nodes.filter((item) => !isSymbol(item.id))) {
|
|
290
|
+
const file = String(node.source_file || node.id)
|
|
291
|
+
const maxEnd = nodes.filter((item) => item.source_file === file && isSymbol(item.id))
|
|
292
|
+
.reduce((max, item) => Math.max(max, Number(String(item.source_end || '').replace(/^L/, '')) || 0), 0)
|
|
293
|
+
if (maxEnd > contract.budgets.maxFileLoc) violations.push(violation('budget.maxFileLoc', 'budget', file, maxEnd, contract.budgets.maxFileLoc))
|
|
294
|
+
}
|
|
295
|
+
const techSet = new Set(technologies.map((item) => safeId(item)).filter(Boolean))
|
|
296
|
+
for (const required of contract.technologies.required) if (!techSet.has(required)) violations.push(violation('technology.required', 'technology', `missing ${required}`))
|
|
297
|
+
for (const forbidden of contract.technologies.forbidden) if (techSet.has(forbidden)) violations.push(violation('technology.forbidden', 'technology', `forbidden ${forbidden}`))
|
|
298
|
+
|
|
299
|
+
const active = violations.filter((item) => !exceptions.has(item.fingerprint))
|
|
300
|
+
const baseline = new Set(contract.ratchet.baseline.fingerprints)
|
|
301
|
+
const current = new Set(active.map((item) => item.fingerprint))
|
|
302
|
+
const fresh = active.filter((item) => !baseline.has(item.fingerprint))
|
|
303
|
+
const existing = active.filter((item) => baseline.has(item.fingerprint))
|
|
304
|
+
const fixed = [...baseline].filter((fingerprint) => !current.has(fingerprint))
|
|
305
|
+
const status = contract.enforcement === 'advisory'
|
|
306
|
+
? 'ADVISORY'
|
|
307
|
+
: contract.enforcement === 'strict'
|
|
308
|
+
? (active.length ? 'FAIL' : 'PASS')
|
|
309
|
+
: (fresh.length ? 'FAIL' : 'PASS')
|
|
310
|
+
const metrics = {
|
|
311
|
+
runtimeCycles: runtimeCycles.length,
|
|
312
|
+
violations: active.length,
|
|
313
|
+
componentDependencies: componentEdges.size,
|
|
314
|
+
mappedComponents: contract.components.length,
|
|
315
|
+
componentFitness: Object.fromEntries([...componentStats].map(([component, stats]) => {
|
|
316
|
+
const internal = stats.internalPairs.size, boundary = stats.boundaryPairs.size
|
|
317
|
+
return [component, {files: stats.files.size, cohesion: internal + boundary ? Number((internal / (internal + boundary)).toFixed(4)) : 1, boundaryRatio: internal + boundary ? Number((boundary / (internal + boundary)).toFixed(4)) : 0}]
|
|
318
|
+
})),
|
|
319
|
+
}
|
|
320
|
+
const result = {
|
|
321
|
+
architectureVerificationV: 1,
|
|
322
|
+
contractHash: contract.contractHash,
|
|
323
|
+
status,
|
|
324
|
+
enforcement: contract.enforcement,
|
|
325
|
+
metrics,
|
|
326
|
+
new: fresh,
|
|
327
|
+
existing,
|
|
328
|
+
fixed,
|
|
329
|
+
excepted: violations.filter((item) => exceptions.has(item.fingerprint)),
|
|
330
|
+
componentEdges: [...componentEdges.values()].sort((a, b) => a.from.localeCompare(b.from) || a.to.localeCompare(b.to) || a.kind.localeCompare(b.kind)),
|
|
331
|
+
runtimeCycles: runtimeCycles.slice(0, 100),
|
|
332
|
+
}
|
|
333
|
+
return {...result, verificationHash: hash(result)}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
export function contractForChange(contract, files = []) {
|
|
337
|
+
const normalized = rawArray(files).map(pathPrefix).filter(Boolean)
|
|
338
|
+
const components = [...new Set(normalized.map((file) => componentFor(file, contract.components)))]
|
|
339
|
+
const rules = contract.dependencyRules.filter((rule) => components.some((component) => matchSelector(rule.from, component) || matchSelector(rule.to, component)))
|
|
340
|
+
return {files: normalized, components, rules, budgets: contract.budgets, technologies: contract.technologies}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function rawArray(value) { return Array.isArray(value) ? value.slice(0, 200) : [] }
|