@holmes-lab/holmes-kit 0.1.9 → 0.1.10
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 +99 -0
- package/README.md +48 -4
- package/dist/.build-id +1 -1
- package/dist/holmes/cli/test-platform.d.ts +25 -0
- package/dist/holmes/cli/test-platform.js +38 -0
- package/dist/holmes/cpg/cpg-scanner.d.ts +41 -0
- package/dist/holmes/cpg/cpg-scanner.js +53 -1
- package/dist/holmes/cpg/forbidden-edges.d.ts +73 -0
- package/dist/holmes/cpg/forbidden-edges.js +140 -0
- package/dist/holmes/cpg/hash-cache.js +13 -5
- package/dist/holmes/cpg/language-parser-walk.js +70 -4
- package/dist/holmes/cpg/proposed-content.d.ts +51 -0
- package/dist/holmes/cpg/proposed-content.js +72 -0
- package/dist/holmes/cpg/required-calls.d.ts +62 -0
- package/dist/holmes/cpg/required-calls.js +93 -0
- package/dist/holmes/guardrail/cspec-change.d.ts +23 -0
- package/dist/holmes/guardrail/cspec-change.js +70 -0
- package/dist/holmes/guardrail/risk-classifier.js +122 -0
- package/dist/holmes/guardrail/write-target.d.ts +42 -0
- package/dist/holmes/guardrail/write-target.js +69 -18
- package/dist/holmes/hooks/pre-tool-use.js +90 -5
- package/dist/holmes/hooks/stop.d.ts +17 -0
- package/dist/holmes/hooks/stop.js +39 -2
- package/dist/holmes/mcp/handlers.d.ts +41 -0
- package/dist/holmes/mcp/handlers.js +173 -3
- package/dist/holmes/mcp/tool-schemas.js +12 -0
- package/dist/holmes/project/dependencies.d.ts +15 -0
- package/dist/holmes/project/dependencies.js +58 -0
- package/dist/holmes/project/json-state.d.ts +24 -0
- package/dist/holmes/project/json-state.js +30 -0
- package/dist/holmes/reverse/scan.js +8 -1
- package/dist/holmes/review/scope.d.ts +29 -0
- package/dist/holmes/review/scope.js +44 -0
- package/dist/holmes/rtm/test-scope.d.ts +44 -0
- package/dist/holmes/rtm/test-scope.js +92 -2
- package/dist/holmes/server/dashboard.d.ts +22 -0
- package/dist/holmes/server/dashboard.js +114 -24
- package/dist/holmes/spec/approval-blockers.d.ts +21 -5
- package/dist/holmes/spec/approval-blockers.js +49 -6
- package/dist/holmes/spec/legacy-format.d.ts +14 -0
- package/dist/holmes/spec/legacy-format.js +15 -1
- package/dist/holmes/spec/nonfunctional.d.ts +70 -0
- package/dist/holmes/spec/nonfunctional.js +119 -0
- package/dist/holmes/spec/spec-parser.d.ts +25 -0
- package/dist/holmes/spec/spec-parser.js +46 -2
- package/dist/holmes/spec/spec-types.d.ts +4 -1
- package/dist/holmes/spec/spec-types.js +13 -1
- package/dist/holmes/testing/effects.d.ts +54 -0
- package/dist/holmes/testing/effects.js +107 -0
- package/package.json +3 -2
- package/playbooks/promote-slice/PLAYBOOK.md +20 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,105 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
<!-- @implements A-SPEC-209 -->
|
|
8
|
+
## [0.1.10] - 2026-08-23
|
|
9
|
+
|
|
10
|
+
Forty-one commits since 0.1.9. Two of the defects below were found only by installing the packed
|
|
11
|
+
tarball into projects outside this repository — the in-repo suite was green over both the whole time.
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
- **C-SPEC structural constraints are enforced at write time (REQ-224…231)**: `## Forbidden Edges`
|
|
15
|
+
and `## Layer Rules` are parsed and judged against the code property graph, so a banned import or
|
|
16
|
+
an unmet call obligation is refused as it is written rather than found later. A `draft` C-SPEC
|
|
17
|
+
enforces nothing — authority, not spelling, decides. The refusal names the rule, the file and the
|
|
18
|
+
edge, and states what the check does NOT see: control flow, aliased calls, computed specifiers.
|
|
19
|
+
- **Non-Functional obligations are machine-readable (REQ-234/235)**: `- [tag] text` in an H-SPEC's
|
|
20
|
+
`## Non-Functional` declares an obligation; a T-SPEC case opening `- [tag]` answers it. An
|
|
21
|
+
unanswered obligation widens the regression tier. Opt-in: prose obligations are not judged, and the
|
|
22
|
+
tag vocabulary is not a fixed list.
|
|
23
|
+
- **Risk reaches the regression tier (REQ-233)**: a change touching a C-SPEC or an ADR runs full
|
|
24
|
+
regression. Previously the architecture axis of `assessRisk` returned `hard-hitl` while the same
|
|
25
|
+
change received the narrowest possible test run.
|
|
26
|
+
- **`spec_retire` (REQ-208…212)**: the only path to a status nothing could previously reach. The
|
|
27
|
+
2026-04 harness-era generation and the 23 JOB work orders are gone from the store.
|
|
28
|
+
- **Windows Native promoted to Tier 1 (REQ-220)**: path verdicts are read from syntax rather than
|
|
29
|
+
from `process.platform`, so a drive-letter or UNC path is judged the same way everywhere. See
|
|
30
|
+
*Unverified* below.
|
|
31
|
+
- **Release provenance gate (REQ-242)**: `npm publish` now refuses a dirty tree, a red suite, or a
|
|
32
|
+
`dist/` that does not name HEAD, and says which file or value is at fault.
|
|
33
|
+
- **Runtime dependency set is pinned (REQ-236)**: twenty-seven H-SPECs asserted "no new dependency"
|
|
34
|
+
and nothing checked it. All four declaration surfaces are judged, not just `dependencies`.
|
|
35
|
+
|
|
36
|
+
### Changed
|
|
37
|
+
Behaviour an existing adopter will notice:
|
|
38
|
+
- **Approval refuses placeholder prose.** A section that is nothing but a `TODO` prompt now blocks
|
|
39
|
+
approval. Already-approved specs are unaffected — the rule runs at approval time only.
|
|
40
|
+
- **A corrupt guard-state file now blocks instead of passing silently.** If `.ax/ledger/stop-guard.json`
|
|
41
|
+
is damaged, the Stop gate evaluates as normal rather than yielding.
|
|
42
|
+
- **The regression tier widens** for changes touching architecture documents and for unmet
|
|
43
|
+
obligations. Expect longer runs on those changes, and a reason that names the cause.
|
|
44
|
+
- **`reverse_scan` reports fewer tests.** Dependencies' own test files are no longer discovered or
|
|
45
|
+
attributed; on a real 235-file Python codebase this removed 254 of 340.
|
|
46
|
+
- **Install guidance leads with a local install.** `npm install -g` on Windows commonly lands in
|
|
47
|
+
`C:\Program Files\nodejs` and fails with `EPERM`; the README now documents a user-space prefix
|
|
48
|
+
instead of elevation, which would run native install scripts with system privileges.
|
|
49
|
+
|
|
50
|
+
### Fixed
|
|
51
|
+
- **A corrupt state file switched the governance gate off (REQ-237).** Four characters — `null` — in
|
|
52
|
+
a gitignored file made the Stop hook produce no output and exit 0 on a project with eight article
|
|
53
|
+
violations. `try { JSON.parse } catch` sees a parse failure, not a parse that succeeds into the
|
|
54
|
+
wrong shape.
|
|
55
|
+
- **A document that said nothing could be sealed as normative (REQ-240).** An H-SPEC whose every
|
|
56
|
+
prose section read `TODO` received `status: approved` and a digest. The existing check keyed on an
|
|
57
|
+
HTML-comment marker that human editing strips.
|
|
58
|
+
- **A dependency's tests counted as the project's coverage (REQ-241).** Source candidates excluded
|
|
59
|
+
vendor directories; test discovery did not, so three third-party tests were attributed to project
|
|
60
|
+
clusters.
|
|
61
|
+
- **A trailing space switched No-Spec-No-Code off.** `"src/a.ts "` did not match the extension test,
|
|
62
|
+
and an unrecognised action passed.
|
|
63
|
+
- **Shell seal bypass closed.** `sed -i`, `perl -i` and `ex` edited governance files without
|
|
64
|
+
tripping the write gate.
|
|
65
|
+
- **Four ways past a Forbidden Edges ban closed**: dynamic `import()`, `require()`, re-export, and
|
|
66
|
+
`export *`.
|
|
67
|
+
- **The scan walk is linear.** A 2KB deeply-nested file cost 10,558ms on the write path; depth, not
|
|
68
|
+
size, was the driver.
|
|
69
|
+
- **Eleven performance budgets were boilerplate.** Four were measured and all four were false —
|
|
70
|
+
`Standalone execution within <100ms` appeared verbatim on three unrelated subsystems. Numbers now
|
|
71
|
+
cite a measurement or say none was taken.
|
|
72
|
+
- **A timed-out test left the repository broken for every later run**: a jest timeout does not unwind
|
|
73
|
+
an async function, so `dist/.build-id` stayed at the value the test writes.
|
|
74
|
+
|
|
75
|
+
### Unverified
|
|
76
|
+
- **Windows has never been executed.** Tier 1 removed the *unverifiability* of platform verdicts; it
|
|
77
|
+
did not produce verification. Seventeen assertions are still skipped as POSIX-only and
|
|
78
|
+
`package.json` declares no `os`.
|
|
79
|
+
- **There is no CI.** The suite has never run outside one macOS machine.
|
|
80
|
+
- Transitive dependency growth, control flow, aliased calls and computed module specifiers are
|
|
81
|
+
outside what the constraint engine can see — each stated in the refusal that would otherwise read
|
|
82
|
+
as safety.
|
|
83
|
+
|
|
84
|
+
<!-- @implements A-SPEC-209 -->
|
|
85
|
+
## [0.1.9] - 2026-08-21
|
|
86
|
+
|
|
87
|
+
### Added
|
|
88
|
+
- **Quantitative REQ RTM Matrix Grid (REQ-219)**: per-REQ 6-stage counts (H/A/T-SPEC, code files, AST symbols) with an accordion drill-down, replacing the single coverage bar.
|
|
89
|
+
- **REQ-Grouped 6-Stage Pipeline Heatmap (REQ-219)**: trace chains grouped under REQ master blocks with a multi-lens quick filter bar, ending the row repetition of the flat table.
|
|
90
|
+
- **Rich node/edge hover inspector (REQ-219)**: node type, provenance digest, in/out degree and file location on hover across all four graph layouts.
|
|
91
|
+
- **Unmapped Specs tab**: the 155 documents whose `status` falls outside `draft|review|approved|outdated` are now visible and grouped by kind, instead of being invisible while sitting in the headline denominator.
|
|
92
|
+
|
|
93
|
+
### Changed
|
|
94
|
+
- **Coverage is reported on two axes, not one number**: approval (signed off) and implementation (code anchored) are separate percentages, and specs with a legacy status vocabulary are excluded from both rather than silently scored zero. The fused `coveragePct` field is gone.
|
|
95
|
+
- **Release metadata check is self-consistent**: `packaging.test.ts` asserts that `package.json` version is semver AND has a matching CHANGELOG section, instead of pinning a literal version that went stale on every release.
|
|
96
|
+
|
|
97
|
+
### Fixed
|
|
98
|
+
- **Fabricated trace chains removed**: rows for REQs with no implementing file were back-filled with `dashboard.ts#startDashboardServer`, producing 64 of 787 traces that named code they do not own. Missing stages now report `null` and are listed in `missingStages`.
|
|
99
|
+
- **Synthesized spec ids removed**: absent H/A/T-SPECs were invented as `H-SPEC-<n>` and rendered indistinguishably from real ones.
|
|
100
|
+
- **Coverage is measured, not asserted**: `status: COVERED` was hardcoded for every matched file; a chain is COVERED only when all six stages resolve.
|
|
101
|
+
- **Mutation score no longer reported as a constant**: `mutationScore: 100` is replaced by `null` with `mutationScoreMeasured: false` — the endpoint generates mutants statically and has no basis for a score.
|
|
102
|
+
- **Findings attribution is exact**: `specRef.includes(numPart)` let `REQ-021` claim a finding filed against `A-SPEC-0219`.
|
|
103
|
+
- **An unscanned audit is distinguished from a clean one**: with no `.ax/ledger/findings.jsonl`, Lens 2 rendered "0 open findings" over every chain. Rows now carry `findingsScanned`.
|
|
104
|
+
- **Silent truncation removed**: `slice(0, 3)` dropped AST symbols in both the API and the drill-down while the badge showed the full count.
|
|
105
|
+
- **Graph truncation is disclosed**: the canvas draws a bounded subgraph (30 of 3544 nodes by default) and now says so with numbers.
|
|
106
|
+
|
|
8
107
|
## [0.1.8] - 2026-08-20
|
|
9
108
|
|
|
10
109
|
### Added
|
package/README.md
CHANGED
|
@@ -51,11 +51,53 @@ Holmes-Kit prioritizes **CLI-based AI Coding Agents** where OS-level process hoo
|
|
|
51
51
|
|
|
52
52
|
## ⚡ Quickstart (3-Minute Setup)
|
|
53
53
|
|
|
54
|
-
### 1. Install
|
|
54
|
+
### 1. Install
|
|
55
|
+
|
|
56
|
+
Install it into the project you want to govern. This is the path verified end to end — the
|
|
57
|
+
wiring `init` writes uses absolute paths, so a local install works from any directory:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
npm install --save-dev @holmes-lab/holmes-kit
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
<details>
|
|
64
|
+
<summary>Prefer one install for many projects? (<code>npm install -g</code>)</summary>
|
|
65
|
+
|
|
66
|
+
A global install works too, but **do not elevate to get it**. Where npm puts global packages is
|
|
67
|
+
your npm configuration, not ours — and on Windows it often points inside the Node.js installation
|
|
68
|
+
directory:
|
|
69
|
+
|
|
70
|
+
```text
|
|
71
|
+
npm error code EPERM
|
|
72
|
+
npm error path C:\Program Files\nodejs\node_modules\@holmes-lab
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Running the install as Administrator would "fix" that by executing this package's native
|
|
76
|
+
dependency install scripts with system privileges. Point npm at a user-writable prefix instead:
|
|
77
|
+
|
|
78
|
+
```powershell
|
|
79
|
+
npm config set prefix "$env:APPDATA\npm" # Windows
|
|
80
|
+
# then ensure %APPDATA%\npm is on PATH
|
|
81
|
+
```
|
|
55
82
|
```bash
|
|
56
|
-
npm
|
|
83
|
+
npm config set prefix "$HOME/.npm-global" # macOS / Linux
|
|
84
|
+
export PATH="$HOME/.npm-global/bin:$PATH"
|
|
57
85
|
```
|
|
58
|
-
|
|
86
|
+
|
|
87
|
+
A Node version manager (`nvm`, `fnm`, `volta`) does the same thing by keeping the whole
|
|
88
|
+
toolchain under your home directory.
|
|
89
|
+
</details>
|
|
90
|
+
|
|
91
|
+
**Prerequisites** — Node.js `>= 20.0.0`, plus a native binary for two kinds of dependency:
|
|
92
|
+
|
|
93
|
+
| Dependency | How the binary arrives |
|
|
94
|
+
|---|---|
|
|
95
|
+
| 8 tree-sitter grammars | Prebuilt binaries ship inside the packages (`darwin-arm64`, `darwin-x64`, `linux-x64`, `win32-x64`) — nothing is compiled |
|
|
96
|
+
| `better-sqlite3` | Downloaded at install time by `prebuild-install`, **falling back to compiling** if no prebuild matches your platform and Node ABI |
|
|
97
|
+
|
|
98
|
+
Only the second can require a toolchain. If it has to compile, you need a C++ build environment:
|
|
99
|
+
Xcode Command Line Tools on macOS, Visual Studio Build Tools on Windows, or
|
|
100
|
+
`apk add --no-cache python3 make g++` on Alpine.
|
|
59
101
|
|
|
60
102
|
### 2. Initialize in Your Project
|
|
61
103
|
```bash
|
|
@@ -152,9 +194,11 @@ Holmes-Kit embeds native AST & Code Property Graph (D-CPG) analyzers to track ca
|
|
|
152
194
|
| **macOS** | Apple Silicon (arm64) / Intel (x64) | ✅ Tier 1 | macOS 12+ (Full hook enforcement) |
|
|
153
195
|
| **Linux** | x86_64 / arm64 | ✅ Tier 1 | Ubuntu, Debian, Fedora, Arch, RHEL |
|
|
154
196
|
| **Windows (WSL2)** | x86_64 | ✅ Tier 1 | WSL2 Ubuntu/Debian recommended |
|
|
155
|
-
| **Windows Native** | x86_64 | ✅ Tier
|
|
197
|
+
| **Windows Native** | x86_64 | ✅ Tier 1 | Windows 10/11 (Node.js 20+ with C++ build tools). Gate parity verified from CI — see [ADR-015](.ax/decisions/ADR-015_platform-tier-is-decided-by-executed-verification.md) for the graduation criteria and the residual risks (NTFS 8.3 names, reserved device names, 260-char paths) |
|
|
156
198
|
|
|
157
199
|
> **Runtime Requirement**: Node.js `>= 20.0.0` (LTS recommended)
|
|
200
|
+
>
|
|
201
|
+
> **How a tier is decided**: by the verification that actually executes, not by declaration. A platform is Tier 1 only while its gate verdicts are exercised by the suite; if that stops being true it is demoted and the demotion is recorded. See [ADR-015](.ax/decisions/ADR-015_platform-tier-is-decided-by-executed-verification.md).
|
|
158
202
|
|
|
159
203
|
---
|
|
160
204
|
|
package/dist/.build-id
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
a400784-mt568ove
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A test that cannot run here says so, instead of returning early and being counted as a pass.
|
|
3
|
+
*
|
|
4
|
+
* @implements A-SPEC-220.3
|
|
5
|
+
* `if (process.platform === 'win32') return;` inside a test body is an unexecution that reports as
|
|
6
|
+
* green. Five of them in `playbook-skills.test.ts` are why the Windows platform tier was, in
|
|
7
|
+
* substance, "no evidence" rather than "no feature" (ADR-015). Choosing `it.skip` moves the fact
|
|
8
|
+
* into the report where a reader can see it.
|
|
9
|
+
*
|
|
10
|
+
* `reason` is required and must be non-empty: a skip without a stated reason is the same silence in
|
|
11
|
+
* different syntax. There is no minimum length, because a length rule is a promise this cannot keep.
|
|
12
|
+
*
|
|
13
|
+
* An unrecognised platform gets `it`, not `it.skip` — leaning toward running. An assertion that
|
|
14
|
+
* cannot hold there surfaces as a failure, which is information; skipping it is not.
|
|
15
|
+
*/
|
|
16
|
+
export declare function posixOnly(reason: string, platform?: string): typeof it | typeof it.skip;
|
|
17
|
+
/**
|
|
18
|
+
* As `posixOnly`, and additionally skipped when the process is root.
|
|
19
|
+
*
|
|
20
|
+
* @implements A-SPEC-220.3
|
|
21
|
+
* `chmod`-based assertions are vacuous for root, which bypasses the permission bits being tested.
|
|
22
|
+
* That is a property of the RUNNING process, knowable at definition time just like the platform, so
|
|
23
|
+
* it belongs in the same choice rather than in a second early return inside the body.
|
|
24
|
+
*/
|
|
25
|
+
export declare function posixOnlyNonRoot(reason: string, platform?: string, uid?: number | undefined): typeof it | typeof it.skip;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// @implements A-SPEC-220.3
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.posixOnly = posixOnly;
|
|
5
|
+
exports.posixOnlyNonRoot = posixOnlyNonRoot;
|
|
6
|
+
/**
|
|
7
|
+
* A test that cannot run here says so, instead of returning early and being counted as a pass.
|
|
8
|
+
*
|
|
9
|
+
* @implements A-SPEC-220.3
|
|
10
|
+
* `if (process.platform === 'win32') return;` inside a test body is an unexecution that reports as
|
|
11
|
+
* green. Five of them in `playbook-skills.test.ts` are why the Windows platform tier was, in
|
|
12
|
+
* substance, "no evidence" rather than "no feature" (ADR-015). Choosing `it.skip` moves the fact
|
|
13
|
+
* into the report where a reader can see it.
|
|
14
|
+
*
|
|
15
|
+
* `reason` is required and must be non-empty: a skip without a stated reason is the same silence in
|
|
16
|
+
* different syntax. There is no minimum length, because a length rule is a promise this cannot keep.
|
|
17
|
+
*
|
|
18
|
+
* An unrecognised platform gets `it`, not `it.skip` — leaning toward running. An assertion that
|
|
19
|
+
* cannot hold there surfaces as a failure, which is information; skipping it is not.
|
|
20
|
+
*/
|
|
21
|
+
function posixOnly(reason, platform = process.platform) {
|
|
22
|
+
if (typeof reason !== 'string' || reason.trim().length === 0) {
|
|
23
|
+
throw new Error('posixOnly(reason): 사유 없는 스킵은 문법만 바뀐 침묵입니다 — 왜 이 단언이 Windows에서 무의미한지 적으십시오.');
|
|
24
|
+
}
|
|
25
|
+
return platform === 'win32' ? it.skip : it;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* As `posixOnly`, and additionally skipped when the process is root.
|
|
29
|
+
*
|
|
30
|
+
* @implements A-SPEC-220.3
|
|
31
|
+
* `chmod`-based assertions are vacuous for root, which bypasses the permission bits being tested.
|
|
32
|
+
* That is a property of the RUNNING process, knowable at definition time just like the platform, so
|
|
33
|
+
* it belongs in the same choice rather than in a second early return inside the body.
|
|
34
|
+
*/
|
|
35
|
+
function posixOnlyNonRoot(reason, platform = process.platform, uid = typeof process.getuid === 'function' ? process.getuid() : undefined) {
|
|
36
|
+
const base = posixOnly(reason, platform);
|
|
37
|
+
return uid === 0 ? it.skip : base;
|
|
38
|
+
}
|
|
@@ -49,6 +49,27 @@ export interface ScannedFile {
|
|
|
49
49
|
* below catches the case that actually matters through evidence rather than by name.
|
|
50
50
|
*/
|
|
51
51
|
export declare const SKIP: Set<string>;
|
|
52
|
+
/**
|
|
53
|
+
* Does this repo-relative path lie inside a directory that never holds the project's own source?
|
|
54
|
+
*
|
|
55
|
+
* @implements A-SPEC-241
|
|
56
|
+
* The walker above consults `SKIP` while descending, so a path it never produced is never judged.
|
|
57
|
+
* Anything that arrives from ELSEWHERE — `git ls-files`, a caller's list — has had no such filter
|
|
58
|
+
* applied, and that asymmetry shipped: measured 2026-08-23 on a real 235-file Python codebase,
|
|
59
|
+
* `reverse_scan`'s source candidates were correctly 149 while its TEST discovery returned 340 files
|
|
60
|
+
* of which 254 (75%) were dependencies' own tests under `node_modules`. Three of them were attributed
|
|
61
|
+
* to a project cluster, so a reverse-engineered T-SPEC would have cited a third-party package's test
|
|
62
|
+
* as its own coverage — exactly what `SKIP`'s comment above warns about.
|
|
63
|
+
*
|
|
64
|
+
* SEGMENTS, not prefixes or substrings. A prefix test claims `node_modules_backup/`; a substring
|
|
65
|
+
* test claims `src/my_vendor_code/`. The last segment is a FILE name and is not consulted: a file
|
|
66
|
+
* called `dist` is not a build directory.
|
|
67
|
+
*
|
|
68
|
+
* Reading `SKIP` rather than restating it is the point — one definition of what counts as vendor,
|
|
69
|
+
* whichever code asks. Two copies of `isTestFile` once let a Python file be excluded from the graph
|
|
70
|
+
* as a test AND gated as production at the same time.
|
|
71
|
+
*/
|
|
72
|
+
export declare function isUnderSkippedDir(p: unknown): boolean;
|
|
52
73
|
/** Single source of truth for which file extensions CpgScanner ingests (REQ-124 gate 2). */
|
|
53
74
|
export declare const SCANNABLE_EXTENSIONS: readonly [".ts", ".mts", ".cts", ".tsx", ".jsx", ".js", ".mjs", ".cjs", ".py", ".cs", ".java", ".go", ".rs", ".cpp", ".cc", ".cxx", ".hpp", ".hh", ".h"];
|
|
54
75
|
export { TEST_FILE_PATTERNS, isTestFile } from './test-files';
|
|
@@ -65,5 +86,25 @@ export declare class CpgScanner {
|
|
|
65
86
|
file: string;
|
|
66
87
|
reason: string;
|
|
67
88
|
}[];
|
|
89
|
+
/**
|
|
90
|
+
* The production graph: every scannable file that is NOT a test.
|
|
91
|
+
*
|
|
92
|
+
* The exclusion is load-bearing, not incidental — change impact, RTM coverage, clustering and
|
|
93
|
+
* reverse-engineered drafts all read this return, and test symbols entering it corrupts all four.
|
|
94
|
+
* `scanTests` exists precisely so nothing has to widen this one.
|
|
95
|
+
*/
|
|
68
96
|
scan(rootDir: string, repoRoot?: string): ScannedFile[];
|
|
97
|
+
/**
|
|
98
|
+
* The test corpus, for structural-constraint checking only.
|
|
99
|
+
*
|
|
100
|
+
* @implements A-SPEC-229
|
|
101
|
+
* Measured 2026-08-22: `scan()` returned 326 files and none of them a test, while 166 existed on
|
|
102
|
+
* disk — so every C-SPEC rule was reading half the source. One of the three `filterGoverned`
|
|
103
|
+
* bypasses (`tspec-mirror.test.ts`) lived in exactly that half and no rule could see it.
|
|
104
|
+
*
|
|
105
|
+
* A SEPARATE entry point rather than an option on `scan()`: the two corpora must never merge by
|
|
106
|
+
* accident, and a caller that wants both should have to say so.
|
|
107
|
+
*/
|
|
108
|
+
scanTests(rootDir: string, repoRoot?: string): ScannedFile[];
|
|
109
|
+
private walkScan;
|
|
69
110
|
}
|
|
@@ -34,6 +34,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.CpgScanner = exports.isTestFile = exports.TEST_FILE_PATTERNS = exports.SCANNABLE_EXTENSIONS = exports.SKIP = void 0;
|
|
37
|
+
exports.isUnderSkippedDir = isUnderSkippedDir;
|
|
37
38
|
// @implements A-SPEC-208
|
|
38
39
|
const fs = __importStar(require("node:fs"));
|
|
39
40
|
const path = __importStar(require("node:path"));
|
|
@@ -64,6 +65,33 @@ exports.SKIP = new Set([
|
|
|
64
65
|
// VCS + generic tool output
|
|
65
66
|
'.git', '.hg', '.svn', 'coverage', '.idea', '.vscode',
|
|
66
67
|
]);
|
|
68
|
+
/**
|
|
69
|
+
* Does this repo-relative path lie inside a directory that never holds the project's own source?
|
|
70
|
+
*
|
|
71
|
+
* @implements A-SPEC-241
|
|
72
|
+
* The walker above consults `SKIP` while descending, so a path it never produced is never judged.
|
|
73
|
+
* Anything that arrives from ELSEWHERE — `git ls-files`, a caller's list — has had no such filter
|
|
74
|
+
* applied, and that asymmetry shipped: measured 2026-08-23 on a real 235-file Python codebase,
|
|
75
|
+
* `reverse_scan`'s source candidates were correctly 149 while its TEST discovery returned 340 files
|
|
76
|
+
* of which 254 (75%) were dependencies' own tests under `node_modules`. Three of them were attributed
|
|
77
|
+
* to a project cluster, so a reverse-engineered T-SPEC would have cited a third-party package's test
|
|
78
|
+
* as its own coverage — exactly what `SKIP`'s comment above warns about.
|
|
79
|
+
*
|
|
80
|
+
* SEGMENTS, not prefixes or substrings. A prefix test claims `node_modules_backup/`; a substring
|
|
81
|
+
* test claims `src/my_vendor_code/`. The last segment is a FILE name and is not consulted: a file
|
|
82
|
+
* called `dist` is not a build directory.
|
|
83
|
+
*
|
|
84
|
+
* Reading `SKIP` rather than restating it is the point — one definition of what counts as vendor,
|
|
85
|
+
* whichever code asks. Two copies of `isTestFile` once let a Python file be excluded from the graph
|
|
86
|
+
* as a test AND gated as production at the same time.
|
|
87
|
+
*/
|
|
88
|
+
function isUnderSkippedDir(p) {
|
|
89
|
+
if (typeof p !== 'string' || p === '')
|
|
90
|
+
return false;
|
|
91
|
+
const segments = p.split('/');
|
|
92
|
+
segments.pop(); // the file name is not a directory
|
|
93
|
+
return segments.some((seg) => exports.SKIP.has(seg));
|
|
94
|
+
}
|
|
67
95
|
/**
|
|
68
96
|
* Evidence-based virtualenv detection. A Python virtualenv is defined by containing `pyvenv.cfg`,
|
|
69
97
|
* whatever the user named the directory (`env`, `.env311`, `myproject-venv`, …), so name matching
|
|
@@ -126,7 +154,31 @@ class CpgScanner {
|
|
|
126
154
|
scanSkipped() {
|
|
127
155
|
return [...this.skipped].sort((a, b) => (a.file < b.file ? -1 : a.file > b.file ? 1 : 0));
|
|
128
156
|
}
|
|
157
|
+
/**
|
|
158
|
+
* The production graph: every scannable file that is NOT a test.
|
|
159
|
+
*
|
|
160
|
+
* The exclusion is load-bearing, not incidental — change impact, RTM coverage, clustering and
|
|
161
|
+
* reverse-engineered drafts all read this return, and test symbols entering it corrupts all four.
|
|
162
|
+
* `scanTests` exists precisely so nothing has to widen this one.
|
|
163
|
+
*/
|
|
129
164
|
scan(rootDir, repoRoot = rootDir) {
|
|
165
|
+
return this.walkScan(rootDir, repoRoot, (rel) => !(0, test_files_2.isTestFile)(rel));
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* The test corpus, for structural-constraint checking only.
|
|
169
|
+
*
|
|
170
|
+
* @implements A-SPEC-229
|
|
171
|
+
* Measured 2026-08-22: `scan()` returned 326 files and none of them a test, while 166 existed on
|
|
172
|
+
* disk — so every C-SPEC rule was reading half the source. One of the three `filterGoverned`
|
|
173
|
+
* bypasses (`tspec-mirror.test.ts`) lived in exactly that half and no rule could see it.
|
|
174
|
+
*
|
|
175
|
+
* A SEPARATE entry point rather than an option on `scan()`: the two corpora must never merge by
|
|
176
|
+
* accident, and a caller that wants both should have to say so.
|
|
177
|
+
*/
|
|
178
|
+
scanTests(rootDir, repoRoot = rootDir) {
|
|
179
|
+
return this.walkScan(rootDir, repoRoot, (rel) => (0, test_files_2.isTestFile)(rel));
|
|
180
|
+
}
|
|
181
|
+
walkScan(rootDir, repoRoot, include) {
|
|
130
182
|
this.skipped = [];
|
|
131
183
|
const out = [];
|
|
132
184
|
const walk = (d) => {
|
|
@@ -146,7 +198,7 @@ class CpgScanner {
|
|
|
146
198
|
continue;
|
|
147
199
|
walk(p);
|
|
148
200
|
}
|
|
149
|
-
else if (SCANNABLE_RE.test(e.name) &&
|
|
201
|
+
else if (SCANNABLE_RE.test(e.name) && include(path.relative(rootDir, p).split(path.sep).join('/'))) {
|
|
150
202
|
// Test detection needs the path, not the basename: a Rust integration test is `tests/x.rs`
|
|
151
203
|
// and a JVM one is `src/test/java/X.java` — nothing in the filename says "test". Relative
|
|
152
204
|
// to the SCAN ROOT, so a repository that merely lives under some `/…/test/` directory on
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { ScannedFile } from './cpg-scanner';
|
|
2
|
+
/**
|
|
3
|
+
* Which half of the source a rule judges.
|
|
4
|
+
*
|
|
5
|
+
* @implements A-SPEC-229
|
|
6
|
+
* The rule declares this, not the caller. If it were decided by which file list someone passed in,
|
|
7
|
+
* the same rule would mean different things depending on who invoked it — that is configuration,
|
|
8
|
+
* not a rule. The default is `production` so the ten rules already approved in C-SPEC-224 keep
|
|
9
|
+
* their meaning exactly.
|
|
10
|
+
*/
|
|
11
|
+
export type RuleCorpus = 'production' | 'tests';
|
|
12
|
+
/** Split a leading `tests:` marker off a source prefix. */
|
|
13
|
+
export declare function splitCorpus(prefix: string): {
|
|
14
|
+
corpus: RuleCorpus;
|
|
15
|
+
sourcePrefix: string;
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* One prohibition, read from a C-SPEC's `## Forbidden Edges`.
|
|
19
|
+
*
|
|
20
|
+
* @implements A-SPEC-224
|
|
21
|
+
* The two kinds are not a taxonomy choice — they are the two things the D-CPG can actually see.
|
|
22
|
+
* Measured 2026-08-22 on this repository: 23,854 `calls` edges and 1,646 `imports` edges, and
|
|
23
|
+
* nothing else. Property access (`process.platform`) produces no edge at all, so no rule shape
|
|
24
|
+
* expresses it; offering one would recreate the "documented but unenforced" state this slice exists
|
|
25
|
+
* to remove.
|
|
26
|
+
*/
|
|
27
|
+
export interface ForbiddenEdgeRule {
|
|
28
|
+
kind: 'import' | 'call';
|
|
29
|
+
/** Which corpus this rule judges. @implements A-SPEC-229 */
|
|
30
|
+
corpus: RuleCorpus;
|
|
31
|
+
/** Repo-relative prefix of the files the rule judges. */
|
|
32
|
+
sourcePrefix: string;
|
|
33
|
+
/** Module specifier (import) or callee name (call). */
|
|
34
|
+
target: string;
|
|
35
|
+
/** The rule's own text, so a violation can quote what it broke. */
|
|
36
|
+
text: string;
|
|
37
|
+
}
|
|
38
|
+
export interface ForbiddenEdgeViolation {
|
|
39
|
+
rule: string;
|
|
40
|
+
sourcePath: string;
|
|
41
|
+
/** The enclosing definition the edge leaves from — `<module>` for a top-level import. */
|
|
42
|
+
from: string;
|
|
43
|
+
to: string;
|
|
44
|
+
kind: 'import' | 'call';
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Read the rules out of a `## Forbidden Edges` section.
|
|
48
|
+
*
|
|
49
|
+
* @implements A-SPEC-224
|
|
50
|
+
* A malformed list item is RETURNED, never dropped. A typo that silently parses to nothing reads as
|
|
51
|
+
* "the rule passed" — the same shape as a gate that concludes there is nothing to govern, which
|
|
52
|
+
* this repository has now paid for in A-SPEC-191 §17, §29 and again in A-SPEC-222.2.
|
|
53
|
+
*
|
|
54
|
+
* Prose and blank lines are neither: a section is allowed to explain itself, and treating every
|
|
55
|
+
* sentence as a broken rule would make the report useless.
|
|
56
|
+
*/
|
|
57
|
+
export declare function parseForbiddenEdges(section: string): {
|
|
58
|
+
rules: ForbiddenEdgeRule[];
|
|
59
|
+
malformed: string[];
|
|
60
|
+
};
|
|
61
|
+
/**
|
|
62
|
+
* Judge a scan against a set of prohibitions.
|
|
63
|
+
*
|
|
64
|
+
* @implements A-SPEC-224
|
|
65
|
+
* Pure: no filesystem, no `process`. That is what lets the same function be exercised by synthetic
|
|
66
|
+
* fixtures AND by the real corpus — and the real corpus is the one that matters, because a rule
|
|
67
|
+
* checked only against fixtures stays green while the repository drifts.
|
|
68
|
+
*
|
|
69
|
+
* `import` matches by PREFIX so one rule covers a directory and survives an extension-less
|
|
70
|
+
* specifier. `call` matches EXACTLY, because its target is a bare callee name and a prefix there
|
|
71
|
+
* would make `list` catch `listFiles`.
|
|
72
|
+
*/
|
|
73
|
+
export declare function findForbiddenEdgeViolations(files: ScannedFile[], rules: ForbiddenEdgeRule[]): ForbiddenEdgeViolation[];
|
|
@@ -0,0 +1,140 @@
|
|
|
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.splitCorpus = splitCorpus;
|
|
37
|
+
exports.parseForbiddenEdges = parseForbiddenEdges;
|
|
38
|
+
exports.findForbiddenEdgeViolations = findForbiddenEdgeViolations;
|
|
39
|
+
// @implements A-SPEC-224
|
|
40
|
+
const path = __importStar(require("node:path"));
|
|
41
|
+
const test_files_1 = require("./test-files");
|
|
42
|
+
/** Split a leading `tests:` marker off a source prefix. */
|
|
43
|
+
function splitCorpus(prefix) {
|
|
44
|
+
return prefix.startsWith('tests:')
|
|
45
|
+
? { corpus: 'tests', sourcePrefix: prefix.slice('tests:'.length) }
|
|
46
|
+
: { corpus: 'production', sourcePrefix: prefix };
|
|
47
|
+
}
|
|
48
|
+
const RULE = /^(import|call)\s+(\S+)\s+-x->\s+(\S+)$/;
|
|
49
|
+
/**
|
|
50
|
+
* Read the rules out of a `## Forbidden Edges` section.
|
|
51
|
+
*
|
|
52
|
+
* @implements A-SPEC-224
|
|
53
|
+
* A malformed list item is RETURNED, never dropped. A typo that silently parses to nothing reads as
|
|
54
|
+
* "the rule passed" — the same shape as a gate that concludes there is nothing to govern, which
|
|
55
|
+
* this repository has now paid for in A-SPEC-191 §17, §29 and again in A-SPEC-222.2.
|
|
56
|
+
*
|
|
57
|
+
* Prose and blank lines are neither: a section is allowed to explain itself, and treating every
|
|
58
|
+
* sentence as a broken rule would make the report useless.
|
|
59
|
+
*/
|
|
60
|
+
function parseForbiddenEdges(section) {
|
|
61
|
+
const rules = [];
|
|
62
|
+
const malformed = [];
|
|
63
|
+
for (const raw of String(section ?? '').split('\n')) {
|
|
64
|
+
const line = raw.trim();
|
|
65
|
+
if (!line.startsWith('- '))
|
|
66
|
+
continue; // prose, blank, or a non-list line
|
|
67
|
+
// Strip a trailing comment and the backticks a Markdown author naturally reaches for, so the
|
|
68
|
+
// rule reads the same whether or not it was written for a human eye.
|
|
69
|
+
const body = line.slice(2).replace(/\s+#.*$/, '').replace(/`/g, '').trim().replace(/\s+/g, ' ');
|
|
70
|
+
const m = RULE.exec(body);
|
|
71
|
+
if (!m) {
|
|
72
|
+
malformed.push(raw.trim());
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
const { corpus, sourcePrefix } = splitCorpus(m[2]);
|
|
76
|
+
rules.push({ kind: m[1], corpus, sourcePrefix, target: m[3], text: body });
|
|
77
|
+
}
|
|
78
|
+
return { rules, malformed };
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Where an import specifier lands, as a repo-relative path.
|
|
82
|
+
*
|
|
83
|
+
* A relative specifier is resolved against the importing file; anything else (`node:path`, a package
|
|
84
|
+
* name) has nothing to resolve against and is compared as the literal string it is. Mixing the two
|
|
85
|
+
* inside one comparison would make a rule mean different things depending on how the author spelled
|
|
86
|
+
* the target.
|
|
87
|
+
*/
|
|
88
|
+
function importTargetOf(sourcePath, to) {
|
|
89
|
+
if (!to.startsWith('.'))
|
|
90
|
+
return to;
|
|
91
|
+
return path.posix.normalize(path.posix.join(path.posix.dirname(sourcePath), to));
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Judge a scan against a set of prohibitions.
|
|
95
|
+
*
|
|
96
|
+
* @implements A-SPEC-224
|
|
97
|
+
* Pure: no filesystem, no `process`. That is what lets the same function be exercised by synthetic
|
|
98
|
+
* fixtures AND by the real corpus — and the real corpus is the one that matters, because a rule
|
|
99
|
+
* checked only against fixtures stays green while the repository drifts.
|
|
100
|
+
*
|
|
101
|
+
* `import` matches by PREFIX so one rule covers a directory and survives an extension-less
|
|
102
|
+
* specifier. `call` matches EXACTLY, because its target is a bare callee name and a prefix there
|
|
103
|
+
* would make `list` catch `listFiles`.
|
|
104
|
+
*/
|
|
105
|
+
function findForbiddenEdgeViolations(files, rules) {
|
|
106
|
+
const out = [];
|
|
107
|
+
if (!Array.isArray(files) || !Array.isArray(rules) || rules.length === 0)
|
|
108
|
+
return out;
|
|
109
|
+
for (const f of files) {
|
|
110
|
+
const sourcePath = f?.sourcePath;
|
|
111
|
+
if (typeof sourcePath !== 'string')
|
|
112
|
+
continue;
|
|
113
|
+
// @implements A-SPEC-229 — the file's corpus is decided by `isTestFile`, the same table the
|
|
114
|
+
// scanner and the phase gate already share. A third definition would drift from those two, and
|
|
115
|
+
// the cost of that drift is on record: one Python file was excluded from the graph as a test
|
|
116
|
+
// AND gated as production code at the same time.
|
|
117
|
+
const corpus = (0, test_files_1.isTestFile)(sourcePath) ? 'tests' : 'production';
|
|
118
|
+
const applicable = rules.filter((r) => r.corpus === corpus && sourcePath.startsWith(r.sourcePrefix));
|
|
119
|
+
if (applicable.length === 0)
|
|
120
|
+
continue;
|
|
121
|
+
for (const e of f.edges ?? []) {
|
|
122
|
+
for (const r of applicable) {
|
|
123
|
+
if (r.kind === 'import') {
|
|
124
|
+
if (e.rel !== 'imports')
|
|
125
|
+
continue;
|
|
126
|
+
if (!importTargetOf(sourcePath, e.to).startsWith(r.target))
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
if (e.rel !== 'calls')
|
|
131
|
+
continue;
|
|
132
|
+
if (e.to !== r.target)
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
out.push({ rule: r.text, sourcePath, from: e.from, to: e.to, kind: r.kind });
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return out;
|
|
140
|
+
}
|
|
@@ -35,6 +35,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.HashCache = void 0;
|
|
37
37
|
const fs = __importStar(require("node:fs"));
|
|
38
|
+
const json_state_1 = require("../project/json-state");
|
|
38
39
|
const path = __importStar(require("node:path"));
|
|
39
40
|
const node_crypto_1 = require("node:crypto");
|
|
40
41
|
// @implements A-SPEC-113.1
|
|
@@ -58,16 +59,23 @@ class HashCache {
|
|
|
58
59
|
this.hashes = this.load();
|
|
59
60
|
}
|
|
60
61
|
load() {
|
|
62
|
+
let parsed;
|
|
61
63
|
try {
|
|
62
|
-
|
|
63
|
-
return JSON.parse(raw);
|
|
64
|
+
parsed = JSON.parse(fs.readFileSync(this.file, 'utf8'));
|
|
64
65
|
}
|
|
65
66
|
catch {
|
|
66
|
-
// Missing dir/file, or
|
|
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.
|
|
67
|
+
// Missing dir/file, or JSON that does not parse.
|
|
69
68
|
return {};
|
|
70
69
|
}
|
|
70
|
+
// @implements A-SPEC-237
|
|
71
|
+
// A SECOND check, because the one above cannot see the corruption that matters most. `null`,
|
|
72
|
+
// `42`, `"x"`, `true` and `[1,2]` are all valid JSON: the parse SUCCEEDS and the failure
|
|
73
|
+
// lands later, at the first property access, as a TypeError. Measured 2026-08-23 — four of those
|
|
74
|
+
// forms made `unchanged()` and `put()` throw, while this method's own comment promised that a
|
|
75
|
+
// damaged cache "must never block a reindex". The promise lived only in prose, so nobody checked.
|
|
76
|
+
if (!(0, json_state_1.isJsonStateObject)(parsed))
|
|
77
|
+
return {};
|
|
78
|
+
return parsed;
|
|
71
79
|
}
|
|
72
80
|
static hashOf(content) {
|
|
73
81
|
return (0, node_crypto_1.createHash)('sha256').update(content, 'utf8').digest('hex');
|