@manukyalo/scopelock 2.0.0 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +39 -83
- package/bin/scopelock.js +14 -1
- package/package.json +1 -1
- package/src/diff.js +5 -4
- package/src/git.js +23 -5
- package/src/manifest.js +44 -6
- package/src/secrets.js +31 -0
- package/test/run.js +27 -12
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
|
-
##
|
|
13
|
+
## Features
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
|
42
|
-
|
|
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 "
|
|
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
|
-
|
|
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
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
| TypeScript | `.ts`, `.tsx` |
|
|
121
|
-
| Python | `.py` |
|
|
85
|
+
Locked files:
|
|
86
|
+
src/lib/supabase.ts
|
|
87
|
+
```
|
|
122
88
|
|
|
123
|
-
|
|
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
|
-
##
|
|
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
|
@@ -61,6 +61,17 @@ switch (command) {
|
|
|
61
61
|
manifest.status();
|
|
62
62
|
break;
|
|
63
63
|
|
|
64
|
+
case 'allow-secret': {
|
|
65
|
+
const target = args[0];
|
|
66
|
+
const reason = args.slice(1).join(' ');
|
|
67
|
+
if (!target || !reason) {
|
|
68
|
+
console.error('Usage: scopelock allow-secret <file> <reason>');
|
|
69
|
+
process.exit(1);
|
|
70
|
+
}
|
|
71
|
+
manifest.allowSecret(target, reason);
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
|
|
64
75
|
default:
|
|
65
76
|
console.log(`
|
|
66
77
|
scopelock — Anti-hallucination scope locking for AI coding agents.
|
|
@@ -69,10 +80,12 @@ Usage:
|
|
|
69
80
|
scopelock init Scan repo and generate .scopelock.json
|
|
70
81
|
scopelock lock <file>[:<func>] [reason] Lock a file or a specific function
|
|
71
82
|
scopelock unlock <file>[:<func>] <reason> Unlock (reason is mandatory)
|
|
83
|
+
scopelock allow-secret <file> <reason> Bypass Secret Sentinel for a specific file
|
|
72
84
|
scopelock context [task] Generate AI context block for a task
|
|
73
|
-
scopelock check Check git diff for scope violations
|
|
85
|
+
scopelock check Check git diff for scope violations and secret leaks
|
|
74
86
|
scopelock status Show manifest summary
|
|
75
87
|
|
|
88
|
+
|
|
76
89
|
Examples:
|
|
77
90
|
scopelock lock src/auth.ts
|
|
78
91
|
scopelock lock src/auth.ts:validateToken "stable — do not touch"
|
package/package.json
CHANGED
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 {
|
|
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
|
|
34
|
+
if (!diffOutput.trim()) return new Map();
|
|
35
35
|
|
|
36
|
-
const changedLines = new
|
|
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
|
-
|
|
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,6 +19,7 @@ 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
24
|
function check() {
|
|
24
25
|
const manifest = getManifest();
|
|
@@ -51,6 +52,27 @@ function check() {
|
|
|
51
52
|
const normalizedFile = file.replace(/\\/g, '/');
|
|
52
53
|
const entry = manifest.files[normalizedFile];
|
|
53
54
|
|
|
55
|
+
const changedLines = getChangedLines(normalizedFile);
|
|
56
|
+
|
|
57
|
+
// ── Tier 0: Secret Sentinel ─────────────────────────────────────────────
|
|
58
|
+
if (changedLines.size > 0) {
|
|
59
|
+
// Check if this file has explicitly allowed secrets
|
|
60
|
+
const hasOverride = manifest.allowedSecrets && manifest.allowedSecrets[normalizedFile];
|
|
61
|
+
if (!hasOverride) {
|
|
62
|
+
for (const [lineNum, content] of changedLines.entries()) {
|
|
63
|
+
const secretType = detectSecret(content);
|
|
64
|
+
if (secretType) {
|
|
65
|
+
violations.push({
|
|
66
|
+
type: 'secret',
|
|
67
|
+
file: normalizedFile,
|
|
68
|
+
message: `SECRET LEAK [${secretType}] detected in '${normalizedFile}' on line ${lineNum}.`,
|
|
69
|
+
});
|
|
70
|
+
break; // One secret violation per file is enough
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
54
76
|
// ── Tier 1: File-level lock ─────────────────────────────────────────────
|
|
55
77
|
if (entry && entry.status === 'locked') {
|
|
56
78
|
violations.push({
|
|
@@ -70,10 +92,6 @@ function check() {
|
|
|
70
92
|
|
|
71
93
|
if (lockedFunctions.length === 0) continue;
|
|
72
94
|
|
|
73
|
-
// Get the line numbers that changed in this specific file
|
|
74
|
-
const changedLines = getChangedLines(normalizedFile);
|
|
75
|
-
if (changedLines.size === 0) continue;
|
|
76
|
-
|
|
77
95
|
// Re-extract function boundaries from the current on-disk file
|
|
78
96
|
const currentFunctions = extractFunctions(normalizedFile);
|
|
79
97
|
|
|
@@ -91,7 +109,7 @@ function check() {
|
|
|
91
109
|
}
|
|
92
110
|
|
|
93
111
|
// Check if any changed line falls within the function's boundaries
|
|
94
|
-
for (const line of changedLines) {
|
|
112
|
+
for (const line of changedLines.keys()) {
|
|
95
113
|
if (line >= fn.startLine && line <= fn.endLine) {
|
|
96
114
|
violations.push({
|
|
97
115
|
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
|
-
|
|
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
|
-
//
|
|
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
|
|
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
|
-
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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,19 @@ 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
|
|
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
|
|
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
|
|
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
|
|
|
144
159
|
// ─── Done ─────────────────────────────────────────────────────────────────────
|
|
145
160
|
|
|
146
|
-
console.log('\n✅ All
|
|
161
|
+
console.log('\n✅ All 14 tests passed.\n');
|