@holmes-lab/holmes-kit 0.1.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.
Files changed (107) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/LICENSE +21 -0
  3. package/README.md +102 -0
  4. package/bin/holmes-hook-antigravity.js +31 -0
  5. package/bin/holmes-kit.js +23 -0
  6. package/bin/holmes-mcp.js +34 -0
  7. package/bin/holmes-stop-antigravity.js +29 -0
  8. package/dist/.build-id +1 -0
  9. package/dist/holmes/cli/agents.js +168 -0
  10. package/dist/holmes/cli/doctor.js +625 -0
  11. package/dist/holmes/cli/gitignore-merge.js +84 -0
  12. package/dist/holmes/cli/governed-precondition.js +157 -0
  13. package/dist/holmes/cli/index.js +384 -0
  14. package/dist/holmes/cli/init.js +462 -0
  15. package/dist/holmes/cli/playbook-skills.js +711 -0
  16. package/dist/holmes/cli/roles-readme.js +134 -0
  17. package/dist/holmes/cli/settings-merge.js +122 -0
  18. package/dist/holmes/config/config.js +70 -0
  19. package/dist/holmes/context/bundler.js +114 -0
  20. package/dist/holmes/context/render.js +29 -0
  21. package/dist/holmes/context/tiers.js +110 -0
  22. package/dist/holmes/context/tokens.js +8 -0
  23. package/dist/holmes/cpg/cpg-scanner.js +213 -0
  24. package/dist/holmes/cpg/hash-cache.js +86 -0
  25. package/dist/holmes/cpg/language-parser-walk.js +917 -0
  26. package/dist/holmes/cpg/language-parser-worker.js +81 -0
  27. package/dist/holmes/cpg/language-parser.js +234 -0
  28. package/dist/holmes/cpg/scan-cache.js +108 -0
  29. package/dist/holmes/cpg/source-path.js +44 -0
  30. package/dist/holmes/cpg/test-files.js +84 -0
  31. package/dist/holmes/governance/constitution-debt.js +73 -0
  32. package/dist/holmes/governance/constitution-report.js +25 -0
  33. package/dist/holmes/governance/constitution.js +129 -0
  34. package/dist/holmes/governance/identity.js +30 -0
  35. package/dist/holmes/governance/ledger-lock.js +165 -0
  36. package/dist/holmes/governance/ledger-store.conformance.js +90 -0
  37. package/dist/holmes/governance/ledger-store.js +106 -0
  38. package/dist/holmes/governance/progress-ledger.js +83 -0
  39. package/dist/holmes/governance/provenance-chain.js +365 -0
  40. package/dist/holmes/governance/provenance-ledger.js +0 -0
  41. package/dist/holmes/governance/provenance-schema.js +47 -0
  42. package/dist/holmes/governance/replica-id.js +106 -0
  43. package/dist/holmes/governance/role-policy.js +137 -0
  44. package/dist/holmes/governance/trust-score.js +43 -0
  45. package/dist/holmes/guardrail/anchors.js +31 -0
  46. package/dist/holmes/guardrail/blind-spots.js +38 -0
  47. package/dist/holmes/guardrail/decision-ledger.js +107 -0
  48. package/dist/holmes/guardrail/executable-artifact.js +129 -0
  49. package/dist/holmes/guardrail/governance-history.js +101 -0
  50. package/dist/holmes/guardrail/phase.js +169 -0
  51. package/dist/holmes/guardrail/risk-classifier.js +450 -0
  52. package/dist/holmes/guardrail/risk-gate.js +160 -0
  53. package/dist/holmes/guardrail/risk-types.js +6 -0
  54. package/dist/holmes/guardrail/tspec-state.js +392 -0
  55. package/dist/holmes/guardrail/write-target.js +224 -0
  56. package/dist/holmes/hooks/adapters/antigravity.js +194 -0
  57. package/dist/holmes/hooks/pre-tool-use.js +1262 -0
  58. package/dist/holmes/hooks/stop.js +416 -0
  59. package/dist/holmes/mcp/basis.js +162 -0
  60. package/dist/holmes/mcp/handlers.js +1831 -0
  61. package/dist/holmes/mcp/server.js +71 -0
  62. package/dist/holmes/mcp/stdio-client.js +165 -0
  63. package/dist/holmes/mcp/supervisor.js +178 -0
  64. package/dist/holmes/mcp/tool-schemas.js +394 -0
  65. package/dist/holmes/mcp/validate-args.js +281 -0
  66. package/dist/holmes/messages/registry.js +50 -0
  67. package/dist/holmes/project/baseline.js +210 -0
  68. package/dist/holmes/project/change-source.js +233 -0
  69. package/dist/holmes/project/ignore.js +145 -0
  70. package/dist/holmes/project/root.js +113 -0
  71. package/dist/holmes/reverse/anchor.js +162 -0
  72. package/dist/holmes/reverse/cluster.js +187 -0
  73. package/dist/holmes/reverse/draft.js +151 -0
  74. package/dist/holmes/reverse/dynamic-wiring.js +47 -0
  75. package/dist/holmes/reverse/scan.js +194 -0
  76. package/dist/holmes/reverse/surface.js +154 -0
  77. package/dist/holmes/reverse/test-map.js +263 -0
  78. package/dist/holmes/review/coverage.js +33 -0
  79. package/dist/holmes/review/findings.js +123 -0
  80. package/dist/holmes/review/package.js +40 -0
  81. package/dist/holmes/review/review-targets.js +92 -0
  82. package/dist/holmes/review/scope.js +57 -0
  83. package/dist/holmes/review/test-evidence.js +77 -0
  84. package/dist/holmes/review/test-runner.js +572 -0
  85. package/dist/holmes/rtm/dataflow-taint.js +262 -0
  86. package/dist/holmes/rtm/gap-analyzer.js +27 -0
  87. package/dist/holmes/rtm/git-changes.js +72 -0
  88. package/dist/holmes/rtm/incremental.js +45 -0
  89. package/dist/holmes/rtm/localize.js +100 -0
  90. package/dist/holmes/rtm/rtm-builder.js +191 -0
  91. package/dist/holmes/rtm/rtm-check.js +89 -0
  92. package/dist/holmes/rtm/rtm-graph.js +232 -0
  93. package/dist/holmes/rtm/taint.js +92 -0
  94. package/dist/holmes/rtm/test-scope.js +336 -0
  95. package/dist/holmes/spec/approval-blockers.js +204 -0
  96. package/dist/holmes/spec/breaking-change.js +89 -0
  97. package/dist/holmes/spec/legacy-format.js +87 -0
  98. package/dist/holmes/spec/spec-digest.js +71 -0
  99. package/dist/holmes/spec/spec-parser.js +106 -0
  100. package/dist/holmes/spec/spec-store.conformance.js +118 -0
  101. package/dist/holmes/spec/spec-store.js +331 -0
  102. package/dist/holmes/spec/spec-types.js +177 -0
  103. package/dist/holmes/spec/validator.js +280 -0
  104. package/package.json +76 -0
  105. package/playbooks/adopt/PLAYBOOK.md +125 -0
  106. package/playbooks/author-slice/PLAYBOOK.md +119 -0
  107. package/playbooks/promote-slice/PLAYBOOK.md +134 -0
@@ -0,0 +1,213 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.CpgScanner = exports.isTestFile = exports.TEST_FILE_PATTERNS = exports.SCANNABLE_EXTENSIONS = exports.SKIP = void 0;
37
+ const fs = __importStar(require("node:fs"));
38
+ const path = __importStar(require("node:path"));
39
+ const language_parser_1 = require("./language-parser");
40
+ const source_path_1 = require("./source-path");
41
+ /**
42
+ * Directories that never contain hand-written source for the project under analysis.
43
+ *
44
+ * The original set (`node_modules`, `dist`, `.git`) was JS-shaped, and that silently made the
45
+ * scanner unusable on other ecosystems: measured on a real brownfield Python target, a scan
46
+ * ingested **20,631 files instead of 315** — 98.5% of it third-party code inside `.venv`. That is
47
+ * not merely slow; the RTM graph fills with library symbols, clustering becomes meaningless, and
48
+ * reverse-engineered specs would describe someone else's code.
49
+ *
50
+ * Entries here must be *unambiguous* vendor or cache directories. Names that are conventional but
51
+ * genuinely ambiguous as source roots — `build`, `out`, `bin`, `obj`, `target`, `lib` — are
52
+ * deliberately EXCLUDED: silently dropping real source is the worse failure, and `isVendorDir`
53
+ * below catches the case that actually matters through evidence rather than by name.
54
+ */
55
+ exports.SKIP = new Set([
56
+ // JS/TS
57
+ 'node_modules', 'dist', '.next', '.nuxt', '.turbo',
58
+ // Python
59
+ '.venv', 'venv', 'site-packages', '__pycache__', '.tox', '.nox',
60
+ '.mypy_cache', '.pytest_cache', '.ruff_cache', '.eggs',
61
+ // Go / Rust / JVM / .NET tool caches
62
+ 'vendor', '.gradle', '.cargo',
63
+ // VCS + generic tool output
64
+ '.git', '.hg', '.svn', 'coverage', '.idea', '.vscode',
65
+ ]);
66
+ /**
67
+ * Evidence-based virtualenv detection. A Python virtualenv is defined by containing `pyvenv.cfg`,
68
+ * whatever the user named the directory (`env`, `.env311`, `myproject-venv`, …), so name matching
69
+ * alone misses them. Checking for the marker file is precise: no real source tree has one at its
70
+ * root. Same idea for a vendored `site-packages` nested under an unconventionally named env.
71
+ */
72
+ function isVendorDir(dir) {
73
+ try {
74
+ return fs.existsSync(path.join(dir, 'pyvenv.cfg'));
75
+ }
76
+ catch {
77
+ return false;
78
+ }
79
+ }
80
+ const IMPL = /@implements\s+(A-SPEC-\d{3,}(?:\.\d+)?)/g;
81
+ // Well-formed governed-but-non-A-SPEC ids used (wrongly) as an @implements
82
+ // anchor. Deliberately requires the full "KIND-NNN" shape (kind + '-' +
83
+ // 3+ digits) so prose/comments like `@implements A-SPEC refs` (no id number)
84
+ // or a bare word never match — only a genuine wrong-kind spec-id does.
85
+ const WRONG_KIND_IMPL = /@implements\s+((?:REQ|H-SPEC|C-SPEC|T-SPEC)-\d{3,}(?:\.\d+)?)/g;
86
+ /** Single source of truth for which file extensions CpgScanner ingests (REQ-124 gate 2). */
87
+ exports.SCANNABLE_EXTENSIONS = ['.ts', '.mts', '.cts', '.tsx', '.jsx', '.js', '.mjs', '.cjs', '.py', '.cs', '.java', '.go', '.rs', '.cpp', '.cc', '.cxx', '.hpp', '.hh', '.h'];
88
+ const SCANNABLE_RE = /\.(ts|mts|cts|tsx|jsx|js|mjs|cjs|py|cs|java|go|rs|cpp|cc|cxx|hpp|hh|h)$/;
89
+ // The test-file table moved to ./test-files, so the PreToolUse phase gate can read the SAME rules
90
+ // without pulling the scanner (and tree-sitter) into a latency-critical path. Re-exported here
91
+ // because this module was its published home and remains the natural place to look for it.
92
+ var test_files_1 = require("./test-files");
93
+ Object.defineProperty(exports, "TEST_FILE_PATTERNS", { enumerable: true, get: function () { return test_files_1.TEST_FILE_PATTERNS; } });
94
+ Object.defineProperty(exports, "isTestFile", { enumerable: true, get: function () { return test_files_1.isTestFile; } });
95
+ const test_files_2 = require("./test-files");
96
+ // .ts/.mts/.cts can contain TS type-assertion syntax (`<T>x`, arrow-generics)
97
+ // that the 'tsx' grammar misreads as JSX, so they must stay on 'typescript'.
98
+ // Everything else in SCANNABLE_EXTENSIONS (.tsx/.jsx/.js/.mjs/.cjs) never uses
99
+ // that syntax, and JSX-in-.js is common (React "escape hatch", older
100
+ // Next.js pages) — the 'tsx' grammar parses plain JS/TS byte-identically to
101
+ // 'typescript' for our walk while also correctly handling JSX, so route the
102
+ // whole JS/JSX family there.
103
+ const TSX_GRAMMAR_RE = /\.(tsx|jsx|js|mjs|cjs)$/;
104
+ // @implements A-SPEC-121.1
105
+ class CpgScanner {
106
+ parser;
107
+ cache;
108
+ opts;
109
+ // Optional N7 scan cache: when supplied, unchanged files (matching content hash) skip the
110
+ // tree-sitter parse entirely and reuse their cached symbols — file-key incremental scanning.
111
+ constructor(parser = new language_parser_1.TreeSitterTsParser(), cache,
112
+ // @implements A-SPEC-140.1 — see ScannedFile.dataflow for why this is opt-in.
113
+ opts = {}) {
114
+ this.parser = parser;
115
+ this.cache = cache;
116
+ this.opts = opts;
117
+ }
118
+ // @implements A-SPEC-131
119
+ // The recorded-skip channel: per-file failures used to vanish in the catch below, which is how
120
+ // handlers.ts (37KB, over the parse cliff) stayed outside the graph for the project's entire
121
+ // history. The report is per-scan (reset on entry), path-sorted, and a second channel — scan()'s
122
+ // signature and every existing consumer are untouched.
123
+ skipped = [];
124
+ /** `{file, reason}` for every file the LAST scan claimed but could not ingest. Empty on a clean tree. */
125
+ scanSkipped() {
126
+ return [...this.skipped].sort((a, b) => (a.file < b.file ? -1 : a.file > b.file ? 1 : 0));
127
+ }
128
+ scan(rootDir, repoRoot = rootDir) {
129
+ this.skipped = [];
130
+ const out = [];
131
+ const walk = (d) => {
132
+ let entries;
133
+ try {
134
+ entries = fs.readdirSync(d, { withFileTypes: true });
135
+ }
136
+ catch {
137
+ return;
138
+ }
139
+ for (const e of entries) {
140
+ if (exports.SKIP.has(e.name))
141
+ continue;
142
+ const p = path.join(d, e.name);
143
+ if (e.isDirectory()) {
144
+ if (isVendorDir(p))
145
+ continue;
146
+ walk(p);
147
+ }
148
+ else if (SCANNABLE_RE.test(e.name) && !(0, test_files_2.isTestFile)(path.relative(rootDir, p).split(path.sep).join('/'))) {
149
+ // Test detection needs the path, not the basename: a Rust integration test is `tests/x.rs`
150
+ // and a JVM one is `src/test/java/X.java` — nothing in the filename says "test". Relative
151
+ // to the SCAN ROOT, so a repository that merely lives under some `/…/test/` directory on
152
+ // disk is not mistaken for one big test tree.
153
+ try {
154
+ const code = fs.readFileSync(p, 'utf8');
155
+ // @implements A-SPEC-121.5
156
+ // @implements A-SPEC-121.6
157
+ // .h is routed to the C++ grammar (not a separate C parser): the
158
+ // C++ grammar parses plain C headers acceptably for our lexical
159
+ // symbol walk, and disambiguating a bare `.h` between C and C++
160
+ // is out of scope for this slice.
161
+ const lang = /\.py$/.test(e.name) ? 'python' : (/\.cs$/.test(e.name) ? 'csharp' : (/\.java$/.test(e.name) ? 'java' : (/\.go$/.test(e.name) ? 'go' : (/\.rs$/.test(e.name) ? 'rust' : (/\.(cpp|cc|cxx|hpp|hh|h)$/.test(e.name) ? 'cpp' : (TSX_GRAMMAR_RE.test(e.name) ? 'tsx' : 'typescript'))))));
162
+ const implementsSpecs = [...code.matchAll(IMPL)].map(m => m[1]);
163
+ const unanchoredImplements = [...code.matchAll(WRONG_KIND_IMPL)].map(m => m[1]);
164
+ const sourcePath = (0, source_path_1.toSourcePath)(repoRoot, path.resolve(p));
165
+ // @implements A-SPEC-140.1
166
+ // The third parse happens only when asked for. `undefined` from the parser (a language
167
+ // with no walk) is carried through as `undefined`, not coerced to an empty fact set.
168
+ const dataflow = () => (this.opts.dataflow ? this.parser.extractDataFlow(code, lang) : undefined);
169
+ // N7 incremental: on a content-hash hit, reuse cached symbols and skip the parse.
170
+ if (this.cache) {
171
+ const { ScanFileCache } = require('./scan-cache');
172
+ const h = ScanFileCache.hashOf(code);
173
+ const hit = this.cache.get(sourcePath, h);
174
+ // @implements A-SPEC-140.1
175
+ // A hit written by a scan that did NOT extract data-flow cannot satisfy one that does:
176
+ // serving it would hand the taint pass zero facts, which is indistinguishable from
177
+ // "no tainted flows". Correctness over hit rate — re-extract and re-store instead.
178
+ // The language check keeps this from re-parsing every Go file on every scan forever.
179
+ const usable = hit && !(this.opts.dataflow && hit.dataflow === undefined && (0, language_parser_1.hasDataFlowWalk)(lang));
180
+ if (usable) {
181
+ out.push({ path: p, ...hit });
182
+ continue;
183
+ }
184
+ const fresh = { sourcePath, symbols: this.parser.extractSymbols(code, lang), edges: this.parser.extractEdges(code, lang), dataflow: dataflow(), implementsSpecs, unanchoredImplements };
185
+ this.cache.put(sourcePath, h, fresh);
186
+ out.push({ path: p, ...fresh });
187
+ continue;
188
+ }
189
+ out.push({ path: p, sourcePath, symbols: this.parser.extractSymbols(code, lang), edges: this.parser.extractEdges(code, lang), dataflow: dataflow(), implementsSpecs, unanchoredImplements });
190
+ }
191
+ catch (err) {
192
+ // A single unreadable/unparseable file must not abort a whole-tree scan — but it must
193
+ // not VANISH either (REQ-131: the old bare skip is how a >32K file stayed outside the
194
+ // graph, anchors and all, for the project's entire history). Infrastructure failures
195
+ // still propagate: under Jest the worker path can throw TreeSitterInfraError
196
+ // (timeout/crash/desync), and a dead worker is not a property of the file.
197
+ if (err instanceof language_parser_1.TreeSitterInfraError)
198
+ throw err;
199
+ // repoRoot-relative, matching sourcePath's path-key convention — a skip entry must be
200
+ // comparable with scan results and change sets even when rootDir is a subtree.
201
+ const rel = path.relative(repoRoot, p).split(path.sep).join('/');
202
+ const reason = (err instanceof Error ? err.message : String(err)).slice(0, 200);
203
+ this.skipped.push({ file: rel, reason });
204
+ }
205
+ }
206
+ }
207
+ };
208
+ walk(rootDir);
209
+ this.cache?.save(); // batched persist + mark-and-sweep prune, once per scan
210
+ return out;
211
+ }
212
+ }
213
+ exports.CpgScanner = CpgScanner;
@@ -0,0 +1,86 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.HashCache = void 0;
37
+ const fs = __importStar(require("node:fs"));
38
+ const path = __importStar(require("node:path"));
39
+ const node_crypto_1 = require("node:crypto");
40
+ // @implements A-SPEC-113.1
41
+ /**
42
+ * Content-hash skip cache used by the reindex path to avoid re-scanning
43
+ * files whose content hasn't changed since the last recorded hash. Keyed by
44
+ * repo-relative path, persisted as a single JSON file under `dir` (callers
45
+ * pass `.ax/cpg_cache`, which is gitignored — cache files are derived, never
46
+ * committed).
47
+ *
48
+ * Deterministic: SHA-256 of the exact content string, no timestamps or other
49
+ * nondeterministic inputs recorded.
50
+ */
51
+ class HashCache {
52
+ dir;
53
+ file;
54
+ hashes;
55
+ constructor(dir) {
56
+ this.dir = dir;
57
+ this.file = path.join(dir, 'hashes.json');
58
+ this.hashes = this.load();
59
+ }
60
+ load() {
61
+ try {
62
+ const raw = fs.readFileSync(this.file, 'utf8');
63
+ return JSON.parse(raw);
64
+ }
65
+ catch {
66
+ // Missing dir/file, or unreadable/corrupt JSON: treat as no prior hash
67
+ // rather than throwing — a cold or damaged cache must never block a
68
+ // reindex, only make it fall back to a full re-scan.
69
+ return {};
70
+ }
71
+ }
72
+ static hashOf(content) {
73
+ return (0, node_crypto_1.createHash)('sha256').update(content, 'utf8').digest('hex');
74
+ }
75
+ /** True iff a hash was previously stored for relPath and it matches content's hash. */
76
+ unchanged(relPath, content) {
77
+ const stored = this.hashes[relPath];
78
+ return stored !== undefined && stored === HashCache.hashOf(content);
79
+ }
80
+ put(relPath, content) {
81
+ this.hashes[relPath] = HashCache.hashOf(content);
82
+ fs.mkdirSync(this.dir, { recursive: true });
83
+ fs.writeFileSync(this.file, JSON.stringify(this.hashes, null, 2), 'utf8');
84
+ }
85
+ }
86
+ exports.HashCache = HashCache;