@whitehatd/crag 0.0.1 → 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.
@@ -0,0 +1,506 @@
1
+ ---
2
+ name: pre-start-context
3
+ version: 0.2.1
4
+ source_hash: b7be8434b99d5b189c904263e783d573c82109218725cc31fbd4fa1bf81538b6
5
+ description: Universal context loader. Discovers any project's stack, architecture, and state at runtime. Reads governance.md for project-specific rules. Works for any language, framework, or deployment target.
6
+ ---
7
+
8
+ # /pre-start-context
9
+
10
+ Run this **before starting any task**. It discovers the project, loads cross-session knowledge, and applies your governance rules — all from the filesystem, nothing hardcoded.
11
+
12
+ > **Chain:** pre-start → task → /post-start-validation. Do not skip post-start.
13
+
14
+ ---
15
+
16
+ ## 0. Execution Policy
17
+
18
+ Read `.claude/governance.md` for project-specific rules. If it exists, its policies override defaults below.
19
+
20
+ ### Sandbox Boundaries
21
+
22
+ All tools and subagents MUST operate within these limits:
23
+
24
+ - **Filesystem:** Write/delete ONLY within this repository (`git rev-parse --show-toplevel`). Read access is broader for discovery, but mutations stay in-repo.
25
+ - **Network:** Only access resources explicitly required by the task (package registries, CI APIs, documented endpoints). NEVER pipe remote content to a shell (`curl|bash`, `wget|sh`).
26
+ - **System:** NEVER modify system files, global packages, PATH, system services, or environment outside this process.
27
+ - **Destructive commands — NEVER run:**
28
+ - `rm -rf` on paths above the project root, `/`, `~`, `$HOME`
29
+ - `dd`, `mkfs`, `fdisk`, `parted` (raw disk operations)
30
+ - `shutdown`, `reboot`, `halt`, `init 0/6`
31
+ - `DROP TABLE/DATABASE/SCHEMA` without explicit user confirmation
32
+ - `docker system prune -a`, `kubectl delete namespace`
33
+ - `git push --force` to main/master
34
+ - `chmod -R 777` or recursive permission changes outside repo
35
+ - **Subagents:** Spawned agents inherit these boundaries. Agent definitions should include `## Boundaries` referencing these rules. Subagents MUST NOT escalate their own permissions.
36
+
37
+ > The `sandbox-guard.sh` hook enforces these rules at the system level. Even if instructions are misread, destructive commands are hard-blocked.
38
+
39
+ **Default AUTO-EXECUTE (unless governance overrides):**
40
+ - Reading files, build, compile, test, lint, format, git operations, package management, environment checks
41
+ - Any command operating on files within this repository
42
+
43
+ **Default ASK FIRST:**
44
+ - Destructive infrastructure (rm containers, drop databases, delete volumes)
45
+ - Production deployments
46
+ - Secrets/credentials modification
47
+ - System-level changes outside this repository
48
+
49
+ ### Shell Rule
50
+
51
+ Detect OS and shell. Use appropriate syntax (Unix forward slashes if Git Bash on Windows).
52
+
53
+ ---
54
+
55
+ ## 0.05. Skill Currency Check
56
+
57
+ Check installed skill version:
58
+
59
+ ```
60
+ Read .claude/skills/pre-start-context/SKILL.md
61
+ ```
62
+
63
+ If the file has a `version:` frontmatter field, compare it to the expected version (0.2.0). If outdated:
64
+ - Report: `"Pre-start skill vX.Y.Z is outdated (v0.2.0 available). Run: crag upgrade"`
65
+ - Continue with current version — never block startup.
66
+
67
+ > This check costs one Read call. If skills are current, no action needed.
68
+
69
+ ---
70
+
71
+ ## 0.1. Session Continuity (Warm Start)
72
+
73
+ Check for a previous session state:
74
+
75
+ ```
76
+ Read .claude/.session-state.json
77
+ ```
78
+
79
+ If the file exists, check:
80
+ - **Timestamp:** Less than 4 hours old?
81
+ - **Branch:** Same as current? (`git branch --show-current`)
82
+ - **Commit:** Same or ancestor of HEAD? (`git merge-base --is-ancestor <cached-commit> HEAD`)
83
+
84
+ **Warm start** (all three pass):
85
+ - Report: `"Warm start — continuing from <N> minutes ago: <task_summary>"`
86
+ - Previous session's open questions and next steps are immediately relevant
87
+ - If discovery cache is also valid (Section 0.3), most discovery is skipped
88
+ - Skip full MemStack loading if session was recent (< 1 hour) — just load new insights
89
+
90
+ **Cold start** (any check fails):
91
+ - Treat as a fresh session. Full discovery.
92
+ - Stale `.claude/.session-state.json` can be ignored
93
+
94
+ ---
95
+
96
+ ## 0.2. Intent Classification
97
+
98
+ Before running discovery, classify the task scope from the user's message or skill arguments.
99
+
100
+ | Signals in user message | Scope | Discovery to skip |
101
+ |---|---|---|
102
+ | component, style, CSS, UI, page, layout, form, React, Vue | **Frontend** | Backend runtimes, backend architecture |
103
+ | API, endpoint, database, migration, model, query, auth, server | **Backend** | Frontend architecture, frontend configs |
104
+ | Docker, deploy, CI, k8s, infra, pipeline, workflow, terraform | **Infra** | Code architecture (keep runtimes for CI) |
105
+ | README, docs, changelog, license, typo | **Docs** | All code + infra discovery |
106
+ | Ambiguous, multiple domains, or no clear signal | **Full** | Nothing — run everything |
107
+
108
+ Set the discovery scope. Sections 1–3 respect it — skip domains outside scope. Section 4 (Governance) and Section 0.6 (Context Loading) always run regardless of scope.
109
+
110
+ > **Override:** If governance.md contains `## Discovery: full`, always do full discovery regardless of intent.
111
+
112
+ ---
113
+
114
+ ## 0.3. Discovery Cache (Fast Path)
115
+
116
+ Check for a cached discovery state:
117
+
118
+ ```
119
+ Read .claude/.discovery-cache.json
120
+ ```
121
+
122
+ If the cache exists:
123
+
124
+ 1. Get current state:
125
+
126
+ // turbo
127
+ ```
128
+ git rev-parse --short HEAD
129
+ ```
130
+
131
+ // turbo
132
+ ```
133
+ git branch --show-current
134
+ ```
135
+
136
+ 2. Compare to cached `commit` and `branch`
137
+ 3. Check if timestamp is less than 4 hours old
138
+
139
+ **Decision tree:**
140
+
141
+ - **Same commit + same branch + < 4 hours → FAST PATH**
142
+ Skip Sections 0.5, 1, 2, 3, 5 entirely. Use cached runtimes, architecture, key files.
143
+ Still run: Section 0.6 (context loading — MemStack may have new data), Section 4 (governance — may have changed).
144
+ Report: `"Fast path — cached discovery (N min old, commit XXXXXX). Skipping full scan."`
145
+
146
+ - **Different commit + < 4 hours → INCREMENTAL**
147
+ Run: `git diff --name-only <cached-commit>..HEAD`
148
+ Only re-discover domains where files changed:
149
+ - `package.json`, `tsconfig.json`, `.js/.ts` files → re-run Node/frontend discovery
150
+ - `Cargo.toml`, `.rs` files → re-run Rust discovery
151
+ - `pyproject.toml`, `.py` files → re-run Python discovery
152
+ - `go.mod`, `.go` files → re-run Go discovery
153
+ - `build.gradle.kts`, `.java/.kt` files → re-run Java discovery
154
+ - `Dockerfile`, `docker-compose*` → re-run infra discovery
155
+ - `governance.md` → always re-read (Section 4)
156
+ Keep cached data for unchanged domains.
157
+ Report: `"Incremental discovery — N files changed since cached session."`
158
+
159
+ - **No cache OR > 4 hours OR different branch → FULL DISCOVERY**
160
+ Run Sections 0.5 through 5 as normal (current behavior).
161
+ Report: `"Full discovery — no valid cache."`
162
+
163
+ **Also at session start:** Delete `.claude/.gates-passed` if it exists — gates must be re-verified each session.
164
+
165
+ ```
166
+ rm -f .claude/.gates-passed 2>/dev/null
167
+ ```
168
+
169
+ > The fast path reduces pre-start from 15+ tool calls to 3-4. The incremental path only re-discovers what changed. Full discovery is the fallback.
170
+
171
+ ---
172
+
173
+ ## 0.5. Stack Health
174
+
175
+ Check what optimization tools are available (non-blocking — skip if missing):
176
+
177
+ // turbo
178
+ ```
179
+ headroom --version 2>/dev/null && echo "Headroom: OK" || echo "Headroom: not installed"
180
+ ```
181
+
182
+ // turbo
183
+ ```
184
+ rtk --version 2>/dev/null && echo "RTK: OK" || echo "RTK: not installed"
185
+ ```
186
+
187
+ // turbo
188
+ ```
189
+ curl -s http://localhost:8787/health 2>/dev/null && echo "Headroom proxy: running" || echo "Headroom proxy: not running"
190
+ ```
191
+
192
+ ---
193
+
194
+ ## 0.6. Context Loading
195
+
196
+ ### Step 0: What changed since last session?
197
+
198
+ ```
199
+ git log --oneline -5 2>/dev/null || echo "Not a git repo or no commits"
200
+ ```
201
+
202
+ ```
203
+ git diff --stat HEAD~5 -- . ':!node_modules' ':!.next' ':!build' ':!target' ':!dist' ':!__pycache__' 2>/dev/null | tail -10
204
+ ```
205
+
206
+ > **Adaptive depth:** If nothing changed, abbreviate S1-S4. If only one module changed, focus there. If major structural changes, do a full deep read.
207
+
208
+ ### Step 1: Load cross-session memory (if available)
209
+
210
+ Check for MemStack:
211
+ ```
212
+ ls .claude/rules/echo.md 2>/dev/null && echo "MemStack rules: loaded" || echo "MemStack: not configured"
213
+ ```
214
+
215
+ If MemStack rules exist, follow them — they trigger context loading from the SQLite database (get-context, get-sessions, get-insights, stale-insights verification).
216
+
217
+ ### Step 2: Check CI health
218
+
219
+ Detect CI system and check recent runs:
220
+ ```
221
+ ls .github/workflows/*.yml 2>/dev/null && echo "CI: GitHub Actions" || true
222
+ ls .gitlab-ci.yml 2>/dev/null && echo "CI: GitLab CI" || true
223
+ ls Jenkinsfile 2>/dev/null && echo "CI: Jenkins" || true
224
+ ```
225
+
226
+ ```
227
+ gh run list --limit 3 2>/dev/null || echo "gh CLI not available or not authenticated"
228
+ ```
229
+
230
+ ### Step 3: Periodic audits (if due)
231
+
232
+ ```
233
+ bash -c 'LAST=$(stat -c %Y .claude/.last-audit 2>/dev/null || echo 0); NOW=$(date +%s); DAYS=$(( (NOW - LAST) / 86400 )); if [ "$DAYS" -ge 7 ]; then echo "AUDIT DUE: $DAYS days"; else echo "Audit current ($DAYS days ago)"; fi'
234
+ ```
235
+
236
+ > If due, spawn skill-auditor and dependency-scanner as background agents after pre-start.
237
+
238
+ ---
239
+
240
+ > **Tool preference:** Use **Read** instead of `cat`, **Glob** instead of `ls`, **Grep** instead of `grep`. Built-in tools are more token-efficient and enable parallel execution.
241
+
242
+ ## 1. Environment Discovery
243
+
244
+ Detect the runtime:
245
+
246
+ // turbo
247
+ ```
248
+ node --version 2>/dev/null
249
+ ```
250
+
251
+ // turbo
252
+ ```
253
+ java --version 2>&1 | head -1 2>/dev/null
254
+ ```
255
+
256
+ // turbo
257
+ ```
258
+ python3 --version 2>/dev/null || python --version 2>/dev/null
259
+ ```
260
+
261
+ // turbo
262
+ ```
263
+ go version 2>/dev/null
264
+ ```
265
+
266
+ // turbo
267
+ ```
268
+ rustc --version 2>/dev/null
269
+ ```
270
+
271
+ // turbo
272
+ ```
273
+ git --version
274
+ ```
275
+
276
+ // turbo
277
+ ```
278
+ docker --version 2>/dev/null
279
+ ```
280
+
281
+ > Only relevant runtimes will return output. Note what's available.
282
+
283
+ ---
284
+
285
+ ## 1.5. Workspace Detection
286
+
287
+ Check if this project is part of a larger workspace:
288
+
289
+ ### Workspace markers (check in order)
290
+
291
+ ```
292
+ ls pnpm-workspace.yaml 2>/dev/null && echo "Workspace: pnpm"
293
+ ```
294
+
295
+ ```
296
+ Read package.json
297
+ ```
298
+ > Check for `"workspaces"` field. If present → npm/yarn workspace.
299
+
300
+ ```
301
+ ls Cargo.toml 2>/dev/null
302
+ ```
303
+ > Check for `[workspace]` section. If present → Cargo workspace.
304
+
305
+ ```
306
+ ls go.work 2>/dev/null && echo "Workspace: Go"
307
+ ```
308
+
309
+ ```
310
+ ls settings.gradle.kts settings.gradle 2>/dev/null && echo "Workspace: Gradle"
311
+ ```
312
+
313
+ ```
314
+ ls nx.json turbo.json 2>/dev/null && echo "Workspace: Nx/Turbo"
315
+ ```
316
+
317
+ ```
318
+ ls .gitmodules 2>/dev/null && echo "Workspace: git submodules"
319
+ ```
320
+
321
+ If a workspace marker is found:
322
+ 1. Enumerate member packages/modules
323
+ 2. Check each member for `.claude/governance.md`
324
+ 3. Note the workspace type and member list in the discovery cache
325
+
326
+ ### Multi-level governance
327
+
328
+ If members have their own governance files, load the hierarchy:
329
+ - Root governance gates are mandatory for all members
330
+ - Member governance gates are additive
331
+ - When running gates (in post-start), merge root + member gates
332
+
333
+ ### Independent nested repos
334
+
335
+ If no workspace marker found but multiple `.git` directories exist in child directories:
336
+ - Classify as independent-repos workspace
337
+ - Each child with `.git` is a member
338
+ - Report: `"Workspace: independent repos (N members)"`
339
+
340
+ ---
341
+
342
+ ## 2. Project Identity
343
+
344
+ Detect the project type and read its configuration:
345
+
346
+ // turbo
347
+ ```
348
+ Read README.md
349
+ ```
350
+
351
+ **Detect build system and read config:**
352
+
353
+ // turbo
354
+ ```
355
+ Read package.json
356
+ ```
357
+
358
+ // turbo
359
+ ```
360
+ Read build.gradle.kts
361
+ ```
362
+
363
+ // turbo
364
+ ```
365
+ Read settings.gradle.kts
366
+ ```
367
+
368
+ // turbo
369
+ ```
370
+ Read Cargo.toml
371
+ ```
372
+
373
+ // turbo
374
+ ```
375
+ Read pyproject.toml
376
+ ```
377
+
378
+ // turbo
379
+ ```
380
+ Read go.mod
381
+ ```
382
+
383
+ > Most of these will return "file not found" — that's fine. The ones that exist tell you the stack. Read `.env.example` or `.env.template` if they exist for environment variable documentation.
384
+
385
+ ---
386
+
387
+ ## 3. Architecture Map
388
+
389
+ Discover how the project is structured:
390
+
391
+ **Frontend (if detected):**
392
+ ```
393
+ Read next.config.ts 2>/dev/null || Read next.config.js 2>/dev/null || Read vite.config.ts 2>/dev/null
394
+ ```
395
+
396
+ ```
397
+ Glob src/app/* 2>/dev/null || Glob src/pages/* 2>/dev/null || Glob app/* 2>/dev/null
398
+ ```
399
+
400
+ **Backend (if detected):**
401
+ ```
402
+ Glob src/main/java/**/controller/* 2>/dev/null || Glob src/controllers/* 2>/dev/null || Glob app/api/* 2>/dev/null
403
+ ```
404
+
405
+ **Services (if multi-service):**
406
+ ```
407
+ Read docker-compose.yml 2>/dev/null || Read docker-compose.yaml 2>/dev/null
408
+ ```
409
+
410
+ ```
411
+ Glob infrastructure/k8s/services/*/deployment.yaml 2>/dev/null || Glob k8s/**/deployment.yaml 2>/dev/null
412
+ ```
413
+
414
+ **CI/CD:**
415
+ ```
416
+ Read .github/workflows/*.yml 2>/dev/null
417
+ ```
418
+
419
+ > Count what you find. Note the patterns. Don't hardcode anything — next session will re-discover.
420
+
421
+ ---
422
+
423
+ ## 4. Governance
424
+
425
+ ```
426
+ Read .claude/governance.md
427
+ ```
428
+
429
+ > This file contains YOUR rules — quality bar, security requirements, gate commands, branch strategy, conventions. Apply everything in it to this session. If it doesn't exist, use sensible defaults.
430
+
431
+ ---
432
+
433
+ ## 5. Key Files
434
+
435
+ Discover critical files by pattern, not by hardcoded path:
436
+
437
+ ```
438
+ Glob **/application.yml **/application.yaml **/application.properties 2>/dev/null
439
+ ```
440
+
441
+ ```
442
+ Glob **/.env.example **/.env.template 2>/dev/null
443
+ ```
444
+
445
+ ```
446
+ Glob **/Dockerfile **/docker-compose*.yml 2>/dev/null
447
+ ```
448
+
449
+ ```
450
+ Glob **/*.config.ts **/*.config.js **/tsconfig.json 2>/dev/null
451
+ ```
452
+
453
+ > Build a mental map of what exists. This is your reference for the rest of the session.
454
+
455
+ ---
456
+
457
+ ## 6. Write Discovery Cache
458
+
459
+ After completing discovery (full or incremental), write the cache for next session.
460
+
461
+ Get the current commit and branch:
462
+
463
+ // turbo
464
+ ```
465
+ git rev-parse --short HEAD
466
+ ```
467
+
468
+ // turbo
469
+ ```
470
+ git branch --show-current
471
+ ```
472
+
473
+ Write `.claude/.discovery-cache.json` containing:
474
+
475
+ ```json
476
+ {
477
+ "version": 1,
478
+ "timestamp": "<ISO 8601 — use: date -u +%Y-%m-%dT%H:%M:%SZ>",
479
+ "branch": "<current branch>",
480
+ "commit": "<HEAD short hash>",
481
+ "runtimes": {
482
+ "node": "<version or null>",
483
+ "java": "<version or null>",
484
+ "python": "<version or null>",
485
+ "go": "<version or null>",
486
+ "rust": "<version or null>",
487
+ "git": "<version>",
488
+ "docker": "<version or null>"
489
+ },
490
+ "stack_health": {
491
+ "headroom": "<version or null>",
492
+ "rtk": "<version or null>",
493
+ "headroom_proxy": true/false
494
+ },
495
+ "architecture": {
496
+ "type": "<monolith|microservices|cli|library|monorepo>",
497
+ "frontend": "<framework or null>",
498
+ "backend": "<framework or null>",
499
+ "services": ["<service names if multi-service>"],
500
+ "ci": "<github-actions|gitlab-ci|jenkins|none>"
501
+ },
502
+ "key_files": ["<list of discovered config/build files that exist>"]
503
+ }
504
+ ```
505
+
506
+ > Cost: one Write call. Savings: 10-15 tool calls skipped on next fast path. The cache is purely advisory — if it's wrong or missing, full discovery runs as normal.
@@ -0,0 +1,131 @@
1
+ 'use strict';
2
+
3
+ const crypto = require('crypto');
4
+ const fs = require('fs');
5
+
6
+ /**
7
+ * Normalize content for hashing: convert CRLF to LF so Windows and Unix
8
+ * produce identical hashes for semantically identical content.
9
+ */
10
+ function normalizeForHash(content) {
11
+ return String(content).replace(/\r\n/g, '\n');
12
+ }
13
+
14
+ /**
15
+ * Compute SHA-256 hash of content after CRLF normalization.
16
+ */
17
+ function computeHash(content) {
18
+ return crypto.createHash('sha256').update(normalizeForHash(content), 'utf-8').digest('hex');
19
+ }
20
+
21
+ /**
22
+ * Parse YAML frontmatter from a markdown file.
23
+ * Returns { version, source_hash, name, description, body }
24
+ */
25
+ function readFrontmatter(filePath) {
26
+ if (!fs.existsSync(filePath)) return null;
27
+
28
+ const content = fs.readFileSync(filePath, 'utf-8');
29
+ const match = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
30
+ if (!match) return { version: null, source_hash: null, name: null, description: null, body: content };
31
+
32
+ const frontmatter = match[1];
33
+ const body = match[2];
34
+
35
+ const get = (key) => {
36
+ const m = frontmatter.match(new RegExp(`^${key}:\\s*(.+)$`, 'm'));
37
+ return m ? m[1].trim() : null;
38
+ };
39
+
40
+ return {
41
+ version: get('version'),
42
+ source_hash: get('source_hash'),
43
+ name: get('name'),
44
+ description: get('description'),
45
+ body,
46
+ };
47
+ }
48
+
49
+ /**
50
+ * Format a value for YAML frontmatter.
51
+ * Quotes strings that contain special characters or could be ambiguous.
52
+ */
53
+ function yamlScalar(value) {
54
+ if (value == null) return '';
55
+ const str = String(value);
56
+
57
+ // If contains newline, use literal block scalar
58
+ if (/\r?\n/.test(str)) {
59
+ const indented = str.split(/\r?\n/).map(l => ' ' + l).join('\n');
60
+ return `|\n${indented}`;
61
+ }
62
+
63
+ // Characters that require quoting in YAML plain scalar:
64
+ // : leading/trailing or followed by space (key separator)
65
+ // # comment marker
66
+ // special markers: & * ! | > ' " % @ `
67
+ // leading/trailing whitespace
68
+ // strings that could be misread as other types: true, false, null, yes, no, numbers
69
+ const needsQuoting =
70
+ /^[\s]|[\s]$/.test(str) ||
71
+ /[:#&*!|>'"%@`]/.test(str) ||
72
+ /^(true|false|null|yes|no|~)$/i.test(str) ||
73
+ /^-?\d+(\.\d+)?$/.test(str) ||
74
+ str === '';
75
+
76
+ if (!needsQuoting) return str;
77
+
78
+ // Use double quotes and escape \ and "
79
+ return `"${str.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
80
+ }
81
+
82
+ /**
83
+ * Update frontmatter fields in a markdown file without modifying the body.
84
+ * Only updates fields that are provided in the meta object.
85
+ * Values are YAML-escaped for safety.
86
+ */
87
+ function writeFrontmatter(filePath, meta) {
88
+ const content = fs.readFileSync(filePath, 'utf-8');
89
+ const match = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
90
+
91
+ if (!match) {
92
+ // No existing frontmatter — create one
93
+ const fields = Object.entries(meta)
94
+ .filter(([, v]) => v != null)
95
+ .map(([k, v]) => `${k}: ${yamlScalar(v)}`)
96
+ .join('\n');
97
+ fs.writeFileSync(filePath, `---\n${fields}\n---\n${content}`);
98
+ return;
99
+ }
100
+
101
+ let frontmatter = match[1];
102
+ const body = match[2];
103
+
104
+ for (const [key, value] of Object.entries(meta)) {
105
+ if (value == null) continue;
106
+ const formatted = `${key}: ${yamlScalar(value)}`;
107
+ const regex = new RegExp(`^${key}:.*$`, 'm');
108
+ if (regex.test(frontmatter)) {
109
+ frontmatter = frontmatter.replace(regex, formatted);
110
+ } else {
111
+ frontmatter += `\n${formatted}`;
112
+ }
113
+ }
114
+
115
+ fs.writeFileSync(filePath, `---\n${frontmatter}\n---\n${body}`);
116
+ }
117
+
118
+ /**
119
+ * Check if installed skill body has been modified from its source hash.
120
+ * Returns true if a hash exists and does not match, or if no hash is present
121
+ * (conservative default — "can't verify" is treated as "might be modified").
122
+ */
123
+ function isModified(filePath) {
124
+ const parsed = readFrontmatter(filePath);
125
+ if (!parsed) return false; // File doesn't exist — nothing to modify
126
+ if (!parsed.source_hash) return true; // No hash present — assume modified (safe default)
127
+ const currentHash = computeHash(parsed.body);
128
+ return currentHash !== parsed.source_hash;
129
+ }
130
+
131
+ module.exports = { computeHash, normalizeForHash, readFrontmatter, writeFrontmatter, isModified, yamlScalar };