@manukyalo/scopelock 2.0.0 → 2.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 CHANGED
@@ -8,53 +8,32 @@ npm install -g @manukyalo/scopelock
8
8
 
9
9
  `scopelock` solves a specific, well-documented problem: AI coding agents frequently exhibit scope creep — modifying files outside the intended change — and lack persistent project memory across sessions.
10
10
 
11
- ---
11
+ `scopelock` acts as a physical guardrail (via a pre-commit hook) and an architectural memory bank (via the context block) to keep agents strictly confined to their authorized scope.
12
12
 
13
- ## Companion Skill
13
+ ## Features
14
14
 
15
- `skills/scope-enforcement/SKILL.md` is a structured 3-checkpoint workflow for AI agents that enforces scopelock boundaries across every phase of a session. Load it into: **Antigravity**, **Claude Code**, **Gemini CLI**, **Cursor**, **Kiro**.
16
-
17
- ---
18
-
19
- ## The Problem
20
-
21
- You ask an AI agent to fix the login button. It also refactors your auth middleware, updates 3 unrelated components, and breaks a working API route. You had no pre-commit guardrail to stop it.
22
-
23
- `scopelock` enforces scope at two levels:
24
-
25
- | Protection | What it stops |
26
- |------------|--------------|
27
- | **File-level** | Agent modifies any file marked `locked` |
28
- | **Function-level** | Agent modifies lines inside a locked function body, even if the surrounding file is `active` |
15
+ - **File-level locks**: Run `scopelock lock src/auth.ts` to make the file read-only for agents.
16
+ - **Function-level locks**: Don't want to lock the whole file? Lock specific AST functions so agents can only edit adjacent code.
17
+ - **Dependency Lockdown**: Zero-trust dependency management. Automatically locks `package.json` and other dependency manifests on init to prevent silent dependency drift.
18
+ - **Secret Sentinel**: A hard-blocking pre-commit scanner that physically prevents agents from committing AWS keys, Stripe tokens, or `.env` leaks.
19
+ - **Zero dependencies**: Written in pure Node.js. Install it anywhere without bloating your `node_modules`.
29
20
 
30
21
  ---
31
22
 
32
23
  ## Commands
33
24
 
34
- ### `scopelock init`
35
- Scan the repo and generate `.scopelock.json`. Automatically ignores `node_modules`, `.git`, `.next`, `dist`, `build`, `out`, `coverage`, and other build artifacts.
36
-
37
25
  ```bash
38
- scopelock init
26
+ scopelock init Scan repo and generate .scopelock.json
27
+ scopelock lock <file>[:<func>] [reason] Lock a file or a specific function
28
+ scopelock unlock <file>[:<func>] <reason> Unlock (reason is mandatory)
29
+ scopelock allow-secret <file> <reason> Bypass Secret Sentinel for a specific file
30
+ scopelock context [task] Generate AI context block for a task
31
+ scopelock check Check git diff for scope violations and secret leaks
32
+ scopelock status Show manifest summary
39
33
  ```
40
34
 
41
- ### `scopelock status`
42
- Print a human-readable summary of the manifest.
43
-
44
- ```bash
45
- scopelock status
46
-
47
- # 📋 scopelock status
48
- #
49
- # 🔒 locked — 3 file(s), 2 function(s)
50
- # ✏️ active — 1 file(s)
51
- # ⬜ unscoped — 98 file(s)
52
- #
53
- # Locked files:
54
- # src/lib/supabase.ts
55
- # src/middleware.ts
56
- # └── middleware() [locked]
57
- ```
35
+ ### `scopelock init`
36
+ Scan the repo and generate `.scopelock.json`. Automatically ignores `node_modules`, `.git`, `.next`, `dist`, `build`, `out`, `coverage`, and other build artifacts.
58
37
 
59
38
  ### `scopelock lock <file>[:<function>] [reason]`
60
39
  Lock a whole file or a specific named function.
@@ -74,8 +53,15 @@ Unlock a file or function. Reason is mandatory and logged.
74
53
  scopelock unlock src/auth/token.ts:validateToken "fixing JWT expiry edge case"
75
54
  ```
76
55
 
56
+ ### `scopelock allow-secret <file> <reason>`
57
+ Bypass the Secret Sentinel hard-block for a specific file (e.g., when intentionally committing a mock test key).
58
+
59
+ ```bash
60
+ scopelock allow-secret test/run.js "this is a mock stripe key for testing"
61
+ ```
62
+
77
63
  ### `scopelock check`
78
- Two-tier scope violation check against `git diff HEAD`. Exits non-zero on violations.
64
+ Two-tier scope violation check against `git diff HEAD`. Exits non-zero on violations or secret leaks. Wire this up as a `pre-commit` hook.
79
65
 
80
66
  ```bash
81
67
  scopelock check
@@ -85,57 +71,27 @@ scopelock check
85
71
  Output a token-efficient AI context block with all locks clearly flagged.
86
72
 
87
73
  ```bash
88
- scopelock context "Fix the broken checkout flow" | clip
74
+ scopelock context "Update the login page"
89
75
  ```
90
-
91
- ---
92
-
93
- ## Pre-commit Hook
94
-
95
- ```bash
96
- echo '#!/bin/sh
97
- scopelock check' > .git/hooks/pre-commit
98
- chmod +x .git/hooks/pre-commit
99
76
  ```
77
+ [SCOPE CONTEXT]
78
+ Task: Update the login page
100
79
 
101
- Once wired, no agent or developer can commit a scope violation.
102
-
103
- ---
104
-
105
- ## File Statuses
106
-
107
- | Status | Meaning |
108
- |--------|---------|
109
- | `unscoped` | Not yet classified |
110
- | `locked` | Stable — do not modify without an explicit `scopelock unlock` |
111
- | `active` | In scope for the current task |
112
-
113
- ---
114
-
115
- ## Language Support for Function-level Locking
80
+ Status:
81
+ 🔒 locked — 1 file(s)
82
+ ✏️ active — 0 file(s)
83
+ ⬜ unscoped — 14 file(s)
116
84
 
117
- | Language | Extensions |
118
- |----------|-----------|
119
- | JavaScript | `.js`, `.jsx`, `.mjs`, `.cjs` |
120
- | TypeScript | `.ts`, `.tsx` |
121
- | Python | `.py` |
85
+ Locked files:
86
+ src/lib/supabase.ts
87
+ ```
122
88
 
123
- All other file types fall back to file-level locking automatically.
89
+ ## Agent Skill
124
90
 
125
- ---
91
+ `scopelock` includes a native Agent Skill.
92
+ If you use an agent framework (like Antigravity or Cline) that supports Markdown skills, point it to `skills/scope-enforcement/SKILL.md` to automatically teach the agent how to use `scopelock` safely.
126
93
 
127
- ## Commit `.scopelock.json`
94
+ ## Data Model
95
+ All state is stored in `.scopelock.json` at the root of your repo.
128
96
 
129
97
  The manifest is project state, not a personal config. Commit it so your whole team — and all their AI agents — share the same scope boundaries.
130
-
131
- ---
132
-
133
- ## Zero Dependencies
134
-
135
- Built entirely on Node.js built-ins (`fs`, `path`, `child_process`). No runtime dependencies.
136
-
137
- ---
138
-
139
- ## License
140
-
141
- MIT
package/bin/scopelock.js CHANGED
@@ -13,6 +13,7 @@
13
13
  * scopelock status Print manifest summary
14
14
  */
15
15
 
16
+ const { execSync } = require('child_process');
16
17
  const manifest = require('../src/manifest');
17
18
  const context = require('../src/context');
18
19
  const git = require('../src/git');
@@ -54,13 +55,82 @@ switch (command) {
54
55
  }
55
56
 
56
57
  case 'check':
57
- git.check();
58
+ git.check(args);
58
59
  break;
59
60
 
60
61
  case 'status':
61
62
  manifest.status();
62
63
  break;
63
64
 
65
+ case 'allow-secret': {
66
+ const target = args[0];
67
+ const reason = args.slice(1).join(' ');
68
+ if (!target || !reason) {
69
+ console.error('Usage: scopelock allow-secret <file> <reason>');
70
+ process.exit(1);
71
+ }
72
+ manifest.allowSecret(target, reason);
73
+ break;
74
+ }
75
+
76
+ case 'snapshot': {
77
+ try {
78
+ const ts = new Date().toISOString().replace(/[:.]/g, '-');
79
+ const manifestObj = manifest.getManifest();
80
+
81
+ // Exclude .scopelock.json from stash — the manifest must survive the snapshot.
82
+ // On Windows, stash@{0} needs to be quoted.
83
+ const out = execSync(
84
+ `git stash push --include-untracked -m "scopelock-snapshot-${ts}" -- . ":(exclude).scopelock.json"`,
85
+ { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }
86
+ );
87
+
88
+ if (out.includes('No local changes to save')) {
89
+ manifestObj.lastSnapshot = 'clean';
90
+ } else {
91
+ // Re-apply so the working tree is fully restored; stash stays as our savepoint.
92
+ execSync(`git stash apply "stash@{0}"`, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] });
93
+ manifestObj.lastSnapshot = 'dirty';
94
+ }
95
+
96
+ manifest.saveManifest(manifestObj);
97
+ console.log(`✅ Snapshot created. Run 'scopelock revert' to obliterate agent changes and restore this state.`);
98
+ } catch (e) {
99
+ console.error('Failed to create snapshot.', e.stderr || e.message);
100
+ process.exit(1);
101
+ }
102
+ break;
103
+ }
104
+
105
+ case 'revert': {
106
+ try {
107
+ const manifestObj = manifest.getManifest();
108
+ if (!manifestObj.lastSnapshot) {
109
+ console.error('❌ No snapshot found. Run `scopelock snapshot` first.');
110
+ process.exit(1);
111
+ }
112
+ console.log(`Obliterating agent mess...`);
113
+ execSync(`git reset --hard HEAD`, { stdio: ['pipe', 'pipe', 'pipe'] });
114
+ execSync(`git clean -fd`, { stdio: ['pipe', 'pipe', 'pipe'] });
115
+
116
+ if (manifestObj.lastSnapshot === 'dirty') {
117
+ const stashes = execSync('git stash list', { encoding: 'utf8' });
118
+ const match = stashes.match(/stash@\{(\d+)\}: .*?scopelock-snapshot/);
119
+ if (match) {
120
+ execSync(`git stash pop "stash@{${match[1]}}"`, { stdio: ['pipe', 'pipe', 'pipe'] });
121
+ }
122
+ }
123
+
124
+ manifestObj.lastSnapshot = null;
125
+ manifest.saveManifest(manifestObj);
126
+ console.log(`✅ Rollback complete. The repository has been restored.`);
127
+ } catch (e) {
128
+ console.error('Failed to revert.', e.stderr || e.message);
129
+ process.exit(1);
130
+ }
131
+ break;
132
+ }
133
+
64
134
  default:
65
135
  console.log(`
66
136
  scopelock — Anti-hallucination scope locking for AI coding agents.
@@ -69,10 +139,14 @@ Usage:
69
139
  scopelock init Scan repo and generate .scopelock.json
70
140
  scopelock lock <file>[:<func>] [reason] Lock a file or a specific function
71
141
  scopelock unlock <file>[:<func>] <reason> Unlock (reason is mandatory)
142
+ scopelock allow-secret <file> <reason> Bypass Secret Sentinel for a specific file
72
143
  scopelock context [task] Generate AI context block for a task
73
- scopelock check Check git diff for scope violations
144
+ scopelock snapshot Auto-snapshot repo state before an agent session
145
+ scopelock revert Rollback to the last snapshot
146
+ scopelock check [--require-tests] Check git diff for violations and test coverage
74
147
  scopelock status Show manifest summary
75
148
 
149
+
76
150
  Examples:
77
151
  scopelock lock src/auth.ts
78
152
  scopelock lock src/auth.ts:validateToken "stable — do not touch"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@manukyalo/scopelock",
3
- "version": "2.0.0",
3
+ "version": "2.2.0",
4
4
  "description": "Anti-hallucination scope locking for AI coding agents.",
5
5
  "main": "bin/scopelock.js",
6
6
  "bin": {
@@ -0,0 +1,21 @@
1
+ ---
2
+ name: test-coverage-gate
3
+ description: "Godmode Skill: Forces the AI agent to write or update test files whenever it modifies application logic. Run 'scopelock check --require-tests' before committing to enforce this rule."
4
+ ---
5
+
6
+ ## Overview
7
+
8
+ This skill teaches the agent how to navigate the `scopelock` Test Coverage Gate.
9
+ When `--require-tests` is active, the CLI will hard-block any commit that modifies a source file (`.js`, `.ts`, `.py`, etc.) if a corresponding test file (`.test.ts`, `.spec.js`, `test/`) is not also modified in the same diff.
10
+
11
+ ## Agent Protocol
12
+
13
+ 1. **Before writing code**: If you are about to modify application logic, understand that your changes will be rejected unless you also provide test coverage.
14
+ 2. **Write the code**: Implement the requested feature or fix.
15
+ 3. **Write the test**: You *must* update the corresponding test file or create a new one. The file path must contain `.test.`, `.spec.`, or be inside a `test/` or `__tests__/` directory.
16
+ 4. **Validation**: Run `scopelock check --require-tests` to mathematically verify your diff will pass the coverage gate.
17
+ 5. **Ship**: Only after the test gate is passed are you allowed to commit or mark the task as complete.
18
+
19
+ ## Why this exists
20
+
21
+ Agents often write code and immediately declare a task "done" without proving it works. This physical gate prevents the agent from bypassing the verification phase. No tests, no merge.
package/src/diff.js CHANGED
@@ -17,7 +17,7 @@ const HUNK_HEADER_RE = /^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@/;
17
17
 
18
18
  /**
19
19
  * @param {string} filePath
20
- * @returns {Set<number>} 1-indexed line numbers that changed in the new file.
20
+ * @returns {Map<number, string>} 1-indexed line numbers -> line content that changed in the new file.
21
21
  */
22
22
  function getChangedLines(filePath) {
23
23
  let diffOutput;
@@ -31,9 +31,9 @@ function getChangedLines(filePath) {
31
31
  return new Set();
32
32
  }
33
33
 
34
- if (!diffOutput.trim()) return new Set();
34
+ if (!diffOutput.trim()) return new Map();
35
35
 
36
- const changedLines = new Set();
36
+ const changedLines = new Map();
37
37
  const lines = diffOutput.split('\n');
38
38
  let currentLine = 0;
39
39
 
@@ -51,7 +51,8 @@ function getChangedLines(filePath) {
51
51
 
52
52
  if (line.startsWith('+')) {
53
53
  // Added/changed line in the new file
54
- changedLines.add(currentLine);
54
+ // Strip the leading '+' before saving the content
55
+ changedLines.set(currentLine, line.substring(1));
55
56
  currentLine++;
56
57
  } else if (line.startsWith('-')) {
57
58
  // Deleted line — does NOT advance the new-file line counter
package/src/git.js CHANGED
@@ -19,8 +19,10 @@ const { execSync } = require('child_process');
19
19
  const { getManifest } = require('./manifest');
20
20
  const { getChangedLines } = require('./diff');
21
21
  const { extractFunctions } = require('./parser');
22
+ const { detectSecret } = require('./secrets');
22
23
 
23
- function check() {
24
+ function check(args = []) {
25
+ const requireTests = args.includes('--require-tests');
24
26
  const manifest = getManifest();
25
27
  let diffOutput;
26
28
 
@@ -46,11 +48,50 @@ function check() {
46
48
  }
47
49
 
48
50
  const violations = [];
51
+
52
+ // ── Tier -1: Test Coverage Gate ───────────────────────────────────────────
53
+ if (requireTests) {
54
+ const hasSourceChanges = changedFiles.some(f =>
55
+ !f.includes('.test.') && !f.includes('.spec.') && !f.includes('/test/') && !f.includes('/__tests__/') &&
56
+ (f.endsWith('.js') || f.endsWith('.ts') || f.endsWith('.py') || f.endsWith('.go') || f.endsWith('.rs'))
57
+ );
58
+ const hasTestChanges = changedFiles.some(f =>
59
+ f.includes('.test.') || f.includes('.spec.') || f.includes('/test/') || f.includes('/__tests__/')
60
+ );
61
+ if (hasSourceChanges && !hasTestChanges) {
62
+ violations.push({
63
+ type: 'test-gate',
64
+ file: 'N/A',
65
+ message: 'TEST GATE VIOLATION: Source logic was modified, but no tests were added or updated. You must write tests to pass `--require-tests`.'
66
+ });
67
+ }
68
+ }
49
69
 
50
70
  for (const file of changedFiles) {
51
71
  const normalizedFile = file.replace(/\\/g, '/');
52
72
  const entry = manifest.files[normalizedFile];
53
73
 
74
+ const changedLines = getChangedLines(normalizedFile);
75
+
76
+ // ── Tier 0: Secret Sentinel ─────────────────────────────────────────────
77
+ if (changedLines.size > 0) {
78
+ // Check if this file has explicitly allowed secrets
79
+ const hasOverride = manifest.allowedSecrets && manifest.allowedSecrets[normalizedFile];
80
+ if (!hasOverride) {
81
+ for (const [lineNum, content] of changedLines.entries()) {
82
+ const secretType = detectSecret(content);
83
+ if (secretType) {
84
+ violations.push({
85
+ type: 'secret',
86
+ file: normalizedFile,
87
+ message: `SECRET LEAK [${secretType}] detected in '${normalizedFile}' on line ${lineNum}.`,
88
+ });
89
+ break; // One secret violation per file is enough
90
+ }
91
+ }
92
+ }
93
+ }
94
+
54
95
  // ── Tier 1: File-level lock ─────────────────────────────────────────────
55
96
  if (entry && entry.status === 'locked') {
56
97
  violations.push({
@@ -70,10 +111,6 @@ function check() {
70
111
 
71
112
  if (lockedFunctions.length === 0) continue;
72
113
 
73
- // Get the line numbers that changed in this specific file
74
- const changedLines = getChangedLines(normalizedFile);
75
- if (changedLines.size === 0) continue;
76
-
77
114
  // Re-extract function boundaries from the current on-disk file
78
115
  const currentFunctions = extractFunctions(normalizedFile);
79
116
 
@@ -91,7 +128,7 @@ function check() {
91
128
  }
92
129
 
93
130
  // Check if any changed line falls within the function's boundaries
94
- for (const line of changedLines) {
131
+ for (const line of changedLines.keys()) {
95
132
  if (line >= fn.startLine && line <= fn.endLine) {
96
133
  violations.push({
97
134
  type: 'function',
package/src/manifest.js CHANGED
@@ -71,18 +71,29 @@ function saveManifest(data) {
71
71
  fs.writeFileSync(MANIFEST_FILE, JSON.stringify(data, null, 2));
72
72
  }
73
73
 
74
+ const DEPENDENCY_MANIFESTS = new Set([
75
+ 'package.json', 'package-lock.json', 'yarn.lock', 'pnpm-lock.yaml',
76
+ 'requirements.txt', 'Pipfile', 'Pipfile.lock', 'poetry.lock',
77
+ 'Cargo.toml', 'Cargo.lock', 'go.mod', 'go.sum'
78
+ ]);
79
+
74
80
  // Ensure a file entry exists in the manifest, creating it if needed.
75
81
  function ensureFileEntry(manifest, relativePath) {
76
82
  if (!manifest.files[relativePath]) {
77
- manifest.files[relativePath] = { status: 'unscoped', functions: {}, history: [] };
83
+ // Auto-lock dependency manifests
84
+ const isDep = DEPENDENCY_MANIFESTS.has(path.basename(relativePath));
85
+ manifest.files[relativePath] = {
86
+ status: isDep ? 'locked' : 'unscoped',
87
+ functions: {},
88
+ history: isDep ? [{ timestamp: new Date().toISOString(), action: 'locked', reason: 'auto-locked dependency manifest' }] : []
89
+ };
78
90
  }
79
91
  if (!manifest.files[relativePath].functions) {
80
92
  manifest.files[relativePath].functions = {};
81
93
  }
82
94
  }
83
95
 
84
- // ─── Commands ─────────────────────────────────────────────────────────────────
85
-
96
+ // Ensure entry exists for 'init' function
86
97
  function init() {
87
98
  if (fs.existsSync(MANIFEST_FILE)) {
88
99
  console.log(`.scopelock.json already exists. Use 'scopelock status' to view it.`);
@@ -90,13 +101,13 @@ function init() {
90
101
  }
91
102
 
92
103
  const files = walkDir('.');
93
- const manifest = { version: VERSION, files: {} };
104
+ const manifest = { version: VERSION, files: {}, allowedSecrets: {} };
94
105
  let count = 0;
95
106
 
96
107
  for (const f of files) {
97
108
  const relativePath = path.relative('.', f).replace(/\\/g, '/');
98
109
  if (relativePath === MANIFEST_FILE) continue;
99
- manifest.files[relativePath] = { status: 'unscoped', functions: {}, history: [] };
110
+ ensureFileEntry(manifest, relativePath);
100
111
  count++;
101
112
  }
102
113
 
@@ -256,4 +267,31 @@ function status() {
256
267
  console.log('');
257
268
  }
258
269
 
259
- module.exports = { init, lock, unlock, status, getManifest, saveManifest };
270
+ /**
271
+ * Allow a secret to be committed in a specific file.
272
+ *
273
+ * @param {string} file "<file>"
274
+ * @param {string} reason Mandatory reason string.
275
+ */
276
+ function allowSecret(file, reason) {
277
+ const relativePath = file.replace(/\\/g, '/');
278
+ const manifest = getManifest();
279
+
280
+ if (!manifest.allowedSecrets) {
281
+ manifest.allowedSecrets = {};
282
+ }
283
+
284
+ if (!manifest.allowedSecrets[relativePath]) {
285
+ manifest.allowedSecrets[relativePath] = [];
286
+ }
287
+
288
+ manifest.allowedSecrets[relativePath].push({
289
+ timestamp: new Date().toISOString(),
290
+ reason
291
+ });
292
+
293
+ saveManifest(manifest);
294
+ console.log(`⚠️ Secret Sentinel bypassed for ${relativePath}. Reason: ${reason}`);
295
+ }
296
+
297
+ module.exports = { init, lock, unlock, allowSecret, status, getManifest, saveManifest };
package/src/secrets.js ADDED
@@ -0,0 +1,31 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * src/secrets.js
5
+ *
6
+ * Scans strings for high-risk secrets.
7
+ * Returns an array of detected secret types.
8
+ */
9
+
10
+ const SECRET_PATTERNS = {
11
+ 'AWS Access Key': /(?:A3T[A-Z0-9]|AKIA|AGPA|AIDA|AROA|AIPA|ANPA|ANVA|ASIA)[A-Z0-9]{16}/,
12
+ 'Stripe Secret Key': /sk_(?:live|test)_[0-9a-zA-Z]{24}/,
13
+ 'GitHub Token': /gh[pousr]_[A-Za-z0-9_]{36,}/,
14
+ 'Slack Token': /xox[baprs]-[0-9]{12}-[0-9]{12}-[a-zA-Z0-9]{24}/,
15
+ 'Generic API Key / Secret': /(?:api[_-]?key|secret|token|password)[\s]*[=:]\s*["'][a-zA-Z0-9_\-]{16,}["']/i
16
+ };
17
+
18
+ /**
19
+ * @param {string} lineContent
20
+ * @returns {string|null} The name of the secret type detected, or null.
21
+ */
22
+ function detectSecret(lineContent) {
23
+ for (const [name, regex] of Object.entries(SECRET_PATTERNS)) {
24
+ if (regex.test(lineContent)) {
25
+ return name;
26
+ }
27
+ }
28
+ return null;
29
+ }
30
+
31
+ module.exports = { detectSecret };
package/test/run.js CHANGED
@@ -59,6 +59,7 @@ function workInProgress() {
59
59
  `.trimStart());
60
60
 
61
61
  fs.writeFileSync('readme.txt', 'Initial readme content.\n');
62
+ fs.writeFileSync('package.json', '{"name":"test"}\n'); // Dummy package manifest
62
63
  run('git add .');
63
64
  run('git commit -m "initial"');
64
65
 
@@ -70,29 +71,43 @@ assert(fs.existsSync('.scopelock.json'), '.scopelock.json was created');
70
71
  const manifest = JSON.parse(fs.readFileSync('.scopelock.json', 'utf8'));
71
72
  assert(manifest.version === 2, 'Manifest is V2 schema');
72
73
  assert(manifest.files['app.js'] !== undefined, 'app.js is tracked');
73
- assert(manifest.files['readme.txt'] !== undefined, 'readme.txt is tracked');
74
74
 
75
- console.log('\n--- Test 2: scopelock status ---');
75
+ console.log('\n--- Test 2: Dependency auto-lock (Dependency Lockdown) ---');
76
+ assert(manifest.files['package.json'].status === 'locked', 'package.json is automatically locked on init');
77
+
78
+ console.log('\n--- Test 3: scopelock status ---');
76
79
  const statusOut = run(`${CLI} status`);
77
80
  assert(statusOut.includes('unscoped'), 'status shows unscoped files');
78
81
 
79
- console.log('\n--- Test 3: File-level lock ---');
82
+ console.log('\n--- Test 4: File-level lock ---');
80
83
  run(`${CLI} lock readme.txt "stable documentation"`);
81
84
  const m2 = JSON.parse(fs.readFileSync('.scopelock.json', 'utf8'));
82
85
  assert(m2.files['readme.txt'].status === 'locked', 'readme.txt is locked');
83
86
 
84
- console.log('\n--- Test 4: File-level violation detection ---');
87
+ console.log('\n--- Test 5: File-level violation detection ---');
85
88
  fs.appendFileSync('readme.txt', 'AI hallucinated this line.\n');
86
89
  const violation1 = run(`${CLI} check`, true);
87
90
  assert(violation1.includes('VIOLATION'), 'check detects locked file modification');
88
91
 
89
- console.log('\n--- Test 5: File-level unlock clears violation ---');
92
+ console.log('\n--- Test 6: File-level unlock clears violation ---');
90
93
  run(`${CLI} unlock readme.txt "intentional update to docs"`);
91
94
  const check1 = run(`${CLI} check`);
92
95
  assert(check1.includes('passed'), 'check passes after unlock');
93
96
  run('git restore readme.txt'); // clean up
94
97
 
95
- console.log('\n--- Test 6: Function-level lock ---');
98
+ console.log('\n--- Test 7: Secret Sentinel detection ---');
99
+ fs.appendFileSync('readme.txt', 'const stripe_key = "sk_test_12345abcdeABCDE12345abcd";\n');
100
+ const secretViolation = run(`${CLI} check`, true);
101
+ assert(secretViolation.includes('SECRET LEAK'), 'check detects leaked stripe key');
102
+ assert(secretViolation.includes('Stripe Secret Key'), 'check identifies secret type');
103
+
104
+ console.log('\n--- Test 8: Secret Sentinel bypass (allow-secret) ---');
105
+ run(`${CLI} allow-secret readme.txt "it is a mock key for tests"`);
106
+ const secretCheck = run(`${CLI} check`);
107
+ assert(secretCheck.includes('passed'), 'allow-secret successfully bypasses secret sentinel');
108
+ run('git restore readme.txt'); // clean up
109
+
110
+ console.log('\n--- Test 9: Function-level lock ---');
96
111
  run(`${CLI} lock app.js:stableFunc "tested, production-ready"`);
97
112
  const m3 = JSON.parse(fs.readFileSync('.scopelock.json', 'utf8'));
98
113
  assert(
@@ -100,7 +115,7 @@ assert(
100
115
  'stableFunc is function-locked'
101
116
  );
102
117
 
103
- console.log('\n--- Test 7: Change OUTSIDE locked function — no violation ---');
118
+ console.log('\n--- Test 10: Change OUTSIDE locked function — no violation ---');
104
119
  // Modify workInProgress (line 6-8), stableFunc is locked (lines 1-3)
105
120
  fs.writeFileSync('app.js', `
106
121
  function stableFunc() {
@@ -114,7 +129,7 @@ function workInProgress() {
114
129
  const check2 = run(`${CLI} check`);
115
130
  assert(check2.includes('passed'), 'change outside locked function does not trigger violation');
116
131
 
117
- console.log('\n--- Test 8: Change INSIDE locked function — violation ---');
132
+ console.log('\n--- Test 11: Change INSIDE locked function — violation ---');
118
133
  fs.writeFileSync('app.js', `
119
134
  function stableFunc() {
120
135
  return 'I have been hallucinated';
@@ -128,19 +143,43 @@ const violation2 = run(`${CLI} check`, true);
128
143
  assert(violation2.includes('VIOLATION'), 'change inside locked function triggers violation');
129
144
  assert(violation2.includes('stableFunc'), 'violation names the locked function');
130
145
 
131
- console.log('\n--- Test 9: Function unlock clears function-level violation ---');
146
+ console.log('\n--- Test 12: Function unlock clears function-level violation ---');
132
147
  run(`${CLI} unlock app.js:stableFunc "need to update return value for new API"`);
133
148
  const check3 = run(`${CLI} check`);
134
149
  assert(check3.includes('passed'), 'check passes after function unlock');
135
150
 
136
- console.log('\n--- Test 10: Lock unknown function fails gracefully ---');
151
+ console.log('\n--- Test 13: Lock unknown function fails gracefully ---');
137
152
  const badLock = run(`${CLI} lock app.js:doesNotExist "testing"`, true);
138
153
  assert(badLock.includes('not found'), 'locking unknown function fails with clear message');
139
154
 
140
- console.log('\n--- Test 11: scopelock context output ---');
155
+ console.log('\n--- Test 14: scopelock context output ---');
141
156
  const ctx = run(`${CLI} context "update the WIP function"`);
142
157
  assert(ctx.includes('SCOPE CONTEXT'), 'context output contains header');
143
158
 
159
+ console.log('\n--- Test 15: Test Coverage Gate (Missing Tests) ---');
160
+ fs.writeFileSync('feature.js', 'console.log("new logic");\n');
161
+ run('git add feature.js');
162
+ const testGateOut = run(`${CLI} check --require-tests`, true);
163
+ assert(testGateOut.includes('TEST GATE VIOLATION'), 'check catches missing tests when flag is used');
164
+
165
+ console.log('\n--- Test 16: Test Coverage Gate (Tests Provided) ---');
166
+ fs.writeFileSync('feature.test.js', 'console.log("test for logic");\n');
167
+ run('git add feature.test.js');
168
+ const testGatePass = run(`${CLI} check --require-tests`);
169
+ assert(testGatePass.includes('passed'), 'check passes when test files accompany source files');
170
+
171
+ console.log('\n--- Test 17: Rollback Snapshot Creation ---');
172
+ run(`${CLI} snapshot`);
173
+ const m4 = JSON.parse(fs.readFileSync('.scopelock.json', 'utf8'));
174
+ assert(m4.lastSnapshot === 'clean' || m4.lastSnapshot === 'dirty', 'snapshot state is tracked in manifest');
175
+
176
+ console.log('\n--- Test 18: Rollback Revert ---');
177
+ fs.writeFileSync('rogue.js', 'I am a rogue agent destroying things');
178
+ run(`${CLI} revert`);
179
+ assert(!fs.existsSync('rogue.js'), 'revert destroys untracked rogue files');
180
+ const m5 = JSON.parse(fs.readFileSync('.scopelock.json', 'utf8'));
181
+ assert(m5.lastSnapshot === null, 'revert clears the snapshot marker');
182
+
144
183
  // ─── Done ─────────────────────────────────────────────────────────────────────
145
184
 
146
- console.log('\n✅ All 11 tests passed.\n');
185
+ console.log('\n✅ All 18 tests passed.\n');