sentinelayer-cli 0.39.1 → 0.40.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.
@@ -1,181 +1,36 @@
1
- // Shared helpers for Sofia's (observability) domain tools (#A20).
2
-
3
- import fsp from "node:fs/promises";
4
- import path from "node:path";
5
- import process from "node:process";
6
-
7
- import ignore from "ignore";
8
-
9
- const DEFAULT_IGNORED_DIRS = new Set([
10
- ".git",
11
- "node_modules",
12
- ".venv",
13
- ".next",
14
- "dist",
15
- "build",
16
- "coverage",
17
- ".sentinelayer",
18
- ".sentinel",
19
- ".turbo",
20
- ".idea",
21
- ".vscode",
22
- "__pycache__",
23
- ".cache",
24
- ]);
25
- const MAX_FILE_SIZE_BYTES = 1024 * 1024;
26
- const SEVERITIES = Object.freeze(["P0", "P1", "P2", "P3"]);
27
-
28
- export function toPosix(value) {
29
- return String(value || "").replace(/\\/g, "/");
30
- }
31
-
32
- export function normalizeSeverity(value) {
33
- const normalized = String(value || "").trim().toUpperCase();
34
- if (SEVERITIES.includes(normalized)) {
35
- return normalized;
36
- }
37
- return "P2";
38
- }
39
-
40
- export function createFinding({
41
- severity,
42
- kind,
43
- file,
44
- line = 0,
45
- evidence = "",
46
- rootCause = "",
47
- recommendedFix = "",
48
- confidence = null,
49
- tool = "",
50
- persona = "observability",
51
- } = {}) {
52
- return {
53
- persona,
54
- tool: String(tool || "").trim(),
55
- kind: String(kind || "").trim() || "observability",
56
- severity: normalizeSeverity(severity),
57
- file: toPosix(file || ""),
58
- line: Number.isFinite(Number(line)) ? Math.max(0, Math.floor(Number(line))) : 0,
59
- evidence: String(evidence || "").trim().slice(0, 400),
60
- rootCause: String(rootCause || "").trim(),
61
- recommendedFix: String(recommendedFix || "").trim(),
62
- confidence:
63
- confidence === null || confidence === undefined
64
- ? null
65
- : Math.max(0, Math.min(1, Number(confidence) || 0)),
66
- };
67
- }
68
-
69
- async function readIgnorePatterns(filePath) {
70
- try {
71
- const raw = await fsp.readFile(filePath, "utf-8");
72
- return String(raw || "")
73
- .split(/\r?\n/)
74
- .map((line) => line.trim())
75
- .filter((line) => line && !line.startsWith("#"));
76
- } catch (err) {
77
- if (err && typeof err === "object" && err.code === "ENOENT") {
78
- return [];
79
- }
80
- throw err;
81
- }
1
+ // Shared helpers for Lin's (observability) domain tools (#A20).
2
+
3
+ import {
4
+ DEFAULT_IGNORED_DIRS,
5
+ MAX_FILE_SIZE_BYTES,
6
+ SEVERITIES,
7
+ createFinding as createDomainFinding,
8
+ findLineMatches,
9
+ getLineContent,
10
+ normalizeSeverity,
11
+ toPosix,
12
+ walkRepoFiles as walkDomainRepoFiles,
13
+ } from "../../shared-tools/domain-base.js";
14
+
15
+ const DEFAULT_PERSONA = "observability";
16
+
17
+ export {
18
+ DEFAULT_IGNORED_DIRS,
19
+ MAX_FILE_SIZE_BYTES,
20
+ SEVERITIES,
21
+ findLineMatches,
22
+ getLineContent,
23
+ normalizeSeverity,
24
+ toPosix,
25
+ };
26
+
27
+ export function createFinding(options = {}) {
28
+ return createDomainFinding(options, {
29
+ defaultPersona: DEFAULT_PERSONA,
30
+ defaultKind: DEFAULT_PERSONA,
31
+ });
32
+ }
33
+
34
+ export async function* walkRepoFiles(options = {}) {
35
+ yield* walkDomainRepoFiles(options);
82
36
  }
83
-
84
- async function createIgnoreMatcher(rootPath) {
85
- const matcher = ignore();
86
- const gitignore = await readIgnorePatterns(path.join(rootPath, ".gitignore"));
87
- const sentinel = await readIgnorePatterns(
88
- path.join(rootPath, ".sentinelayerignore")
89
- );
90
- matcher.add([...gitignore, ...sentinel]);
91
- return (relativePath, isDirectory) => {
92
- const normalized = toPosix(relativePath);
93
- if (!normalized) {
94
- return false;
95
- }
96
- const candidate = isDirectory ? `${normalized}/` : normalized;
97
- return matcher.ignores(candidate);
98
- };
99
- }
100
-
101
- export async function* walkRepoFiles({
102
- rootPath = process.cwd(),
103
- extensions = new Set(),
104
- maxFileSize = MAX_FILE_SIZE_BYTES,
105
- } = {}) {
106
- const resolvedRoot = path.resolve(rootPath);
107
- const ignoreMatcher = await createIgnoreMatcher(resolvedRoot);
108
- const wantedExtensions =
109
- extensions instanceof Set
110
- ? extensions
111
- : new Set(Array.isArray(extensions) ? extensions : []);
112
- const stack = [resolvedRoot];
113
- while (stack.length > 0) {
114
- const current = stack.pop();
115
- let entries = [];
116
- try {
117
- entries = await fsp.readdir(current, { withFileTypes: true });
118
- } catch {
119
- continue;
120
- }
121
- for (const entry of entries) {
122
- const fullPath = path.join(current, entry.name);
123
- const relativePath = toPosix(path.relative(resolvedRoot, fullPath));
124
- if (entry.isDirectory()) {
125
- if (!relativePath || DEFAULT_IGNORED_DIRS.has(entry.name)) {
126
- continue;
127
- }
128
- if (ignoreMatcher(relativePath, true)) {
129
- continue;
130
- }
131
- stack.push(fullPath);
132
- continue;
133
- }
134
- if (!entry.isFile()) {
135
- continue;
136
- }
137
- if (ignoreMatcher(relativePath, false)) {
138
- continue;
139
- }
140
- const ext = path.extname(entry.name).toLowerCase();
141
- if (wantedExtensions.size > 0 && !wantedExtensions.has(ext) && !wantedExtensions.has("")) {
142
- continue;
143
- }
144
- let stat = null;
145
- try {
146
- stat = await fsp.stat(fullPath);
147
- } catch {
148
- stat = null;
149
- }
150
- if (!stat || stat.size > maxFileSize) {
151
- continue;
152
- }
153
- yield { fullPath, relativePath };
154
- }
155
- }
156
- }
157
-
158
- export function findLineMatches(content, pattern) {
159
- const text = String(content || "");
160
- if (!pattern) {
161
- return [];
162
- }
163
- const global = new RegExp(
164
- pattern.source,
165
- pattern.flags.includes("g") ? pattern.flags : `${pattern.flags}g`
166
- );
167
- const matches = [];
168
- let match;
169
- while ((match = global.exec(text)) !== null) {
170
- const lineIndex = text.slice(0, match.index).split(/\r?\n/).length;
171
- matches.push({ index: match.index, line: lineIndex, match: match[0] });
172
- }
173
- return matches;
174
- }
175
-
176
- export function getLineContent(content, line) {
177
- const lines = String(content || "").split(/\r?\n/);
178
- return (lines[Math.max(0, (Number(line) || 1) - 1)] || "").trim();
179
- }
180
-
181
- export { DEFAULT_IGNORED_DIRS, MAX_FILE_SIZE_BYTES, SEVERITIES };
@@ -1,208 +1,38 @@
1
1
  // Shared helpers for Arjun's (performance) domain tools (#A16).
2
2
 
3
- import fsp from "node:fs/promises";
4
- import path from "node:path";
5
- import process from "node:process";
6
-
7
- import ignore from "ignore";
8
-
9
- const DEFAULT_IGNORED_DIRS = new Set([
10
- ".git",
11
- "node_modules",
12
- ".venv",
13
- ".next",
14
- "dist",
15
- "build",
16
- "coverage",
17
- ".sentinelayer",
18
- ".sentinel",
19
- ".turbo",
20
- ".idea",
21
- ".vscode",
22
- "__pycache__",
23
- ".cache",
24
- ]);
25
- const MAX_FILE_SIZE_BYTES = 1024 * 1024;
26
- const SEVERITIES = Object.freeze(["P0", "P1", "P2", "P3"]);
27
-
28
- export function toPosix(value) {
29
- return String(value || "").replace(/\\/g, "/");
30
- }
31
-
32
- export function normalizeSeverity(value) {
33
- const normalized = String(value || "").trim().toUpperCase();
34
- if (SEVERITIES.includes(normalized)) {
35
- return normalized;
36
- }
37
- return "P2";
38
- }
39
-
40
- export function createFinding({
41
- severity,
42
- kind,
43
- file,
44
- line = 0,
45
- evidence = "",
46
- rootCause = "",
47
- recommendedFix = "",
48
- confidence = null,
49
- tool = "",
50
- persona = "performance",
51
- } = {}) {
52
- return {
53
- persona,
54
- tool: String(tool || "").trim(),
55
- kind: String(kind || "").trim() || "performance",
56
- severity: normalizeSeverity(severity),
57
- file: toPosix(file || ""),
58
- line: Number.isFinite(Number(line)) ? Math.max(0, Math.floor(Number(line))) : 0,
59
- evidence: String(evidence || "").trim().slice(0, 400),
60
- rootCause: String(rootCause || "").trim(),
61
- recommendedFix: String(recommendedFix || "").trim(),
62
- confidence:
63
- confidence === null || confidence === undefined
64
- ? null
65
- : Math.max(0, Math.min(1, Number(confidence) || 0)),
66
- };
67
- }
68
-
69
- async function readIgnorePatterns(filePath) {
70
- try {
71
- const raw = await fsp.readFile(filePath, "utf-8");
72
- return String(raw || "")
73
- .split(/\r?\n/)
74
- .map((line) => line.trim())
75
- .filter((line) => line && !line.startsWith("#"));
76
- } catch (err) {
77
- if (err && typeof err === "object" && err.code === "ENOENT") {
78
- return [];
79
- }
80
- throw err;
81
- }
82
- }
83
-
84
- async function createIgnoreMatcher(rootPath) {
85
- const matcher = ignore();
86
- const gitignore = await readIgnorePatterns(path.join(rootPath, ".gitignore"));
87
- const sentinel = await readIgnorePatterns(
88
- path.join(rootPath, ".sentinelayerignore")
89
- );
90
- matcher.add([...gitignore, ...sentinel]);
91
- return (relativePath, isDirectory) => {
92
- const normalized = toPosix(relativePath);
93
- if (!normalized) {
94
- return false;
95
- }
96
- const candidate = isDirectory ? `${normalized}/` : normalized;
97
- return matcher.ignores(candidate);
98
- };
3
+ import {
4
+ DEFAULT_IGNORED_DIRS,
5
+ MAX_FILE_SIZE_BYTES,
6
+ SEVERITIES,
7
+ createFinding as createDomainFinding,
8
+ findLineMatches,
9
+ getLineContent,
10
+ iterateFiles,
11
+ normalizeSeverity,
12
+ toPosix,
13
+ walkRepoFiles as walkDomainRepoFiles,
14
+ } from "../../shared-tools/domain-base.js";
15
+
16
+ const DEFAULT_PERSONA = "performance";
17
+
18
+ export {
19
+ DEFAULT_IGNORED_DIRS,
20
+ MAX_FILE_SIZE_BYTES,
21
+ SEVERITIES,
22
+ findLineMatches,
23
+ getLineContent,
24
+ iterateFiles,
25
+ normalizeSeverity,
26
+ toPosix,
27
+ };
28
+
29
+ export function createFinding(options = {}) {
30
+ return createDomainFinding(options, {
31
+ defaultPersona: DEFAULT_PERSONA,
32
+ defaultKind: DEFAULT_PERSONA,
33
+ });
34
+ }
35
+
36
+ export async function* walkRepoFiles(options = {}) {
37
+ yield* walkDomainRepoFiles(options, { entryMetadata: "sizeBytes" });
99
38
  }
100
-
101
- export async function* walkRepoFiles({
102
- rootPath = process.cwd(),
103
- extensions = new Set(),
104
- maxFileSize = MAX_FILE_SIZE_BYTES,
105
- } = {}) {
106
- const resolvedRoot = path.resolve(rootPath);
107
- const ignoreMatcher = await createIgnoreMatcher(resolvedRoot);
108
- const wantedExtensions =
109
- extensions instanceof Set
110
- ? extensions
111
- : new Set(Array.isArray(extensions) ? extensions : []);
112
- const stack = [resolvedRoot];
113
- while (stack.length > 0) {
114
- const current = stack.pop();
115
- let entries = [];
116
- try {
117
- entries = await fsp.readdir(current, { withFileTypes: true });
118
- } catch {
119
- continue;
120
- }
121
- for (const entry of entries) {
122
- const fullPath = path.join(current, entry.name);
123
- const relativePath = toPosix(path.relative(resolvedRoot, fullPath));
124
- if (entry.isDirectory()) {
125
- if (!relativePath || DEFAULT_IGNORED_DIRS.has(entry.name)) {
126
- continue;
127
- }
128
- if (ignoreMatcher(relativePath, true)) {
129
- continue;
130
- }
131
- stack.push(fullPath);
132
- continue;
133
- }
134
- if (!entry.isFile()) {
135
- continue;
136
- }
137
- if (ignoreMatcher(relativePath, false)) {
138
- continue;
139
- }
140
- const ext = path.extname(entry.name).toLowerCase();
141
- if (wantedExtensions.size > 0 && !wantedExtensions.has(ext) && !wantedExtensions.has("")) {
142
- continue;
143
- }
144
- let stat = null;
145
- try {
146
- stat = await fsp.stat(fullPath);
147
- } catch {
148
- stat = null;
149
- }
150
- if (!stat || stat.size > maxFileSize) {
151
- continue;
152
- }
153
- yield { fullPath, relativePath, sizeBytes: stat.size };
154
- }
155
- }
156
- }
157
-
158
- export function findLineMatches(content, pattern) {
159
- const text = String(content || "");
160
- if (!pattern) {
161
- return [];
162
- }
163
- const global = new RegExp(
164
- pattern.source,
165
- pattern.flags.includes("g") ? pattern.flags : `${pattern.flags}g`
166
- );
167
- const matches = [];
168
- let match;
169
- while ((match = global.exec(text)) !== null) {
170
- const lineIndex = text.slice(0, match.index).split(/\r?\n/).length;
171
- matches.push({ index: match.index, line: lineIndex, match: match[0] });
172
- }
173
- return matches;
174
- }
175
-
176
- export function getLineContent(content, line) {
177
- const lines = String(content || "").split(/\r?\n/);
178
- return (lines[Math.max(0, (Number(line) || 1) - 1)] || "").trim();
179
- }
180
-
181
- export async function* iterateFiles(resolvedRoot, files, extensions) {
182
- if (Array.isArray(files) && files.length > 0) {
183
- for (const file of files) {
184
- const trimmed = String(file || "").trim();
185
- if (!trimmed) {
186
- continue;
187
- }
188
- const fullPath = path.isAbsolute(trimmed)
189
- ? trimmed
190
- : path.join(resolvedRoot, trimmed);
191
- const relativePath = path
192
- .relative(resolvedRoot, fullPath)
193
- .replace(/\\/g, "/");
194
- let stat = null;
195
- try {
196
- stat = await fsp.stat(fullPath);
197
- } catch {
198
- stat = null;
199
- }
200
- yield { fullPath, relativePath, sizeBytes: stat?.size || 0 };
201
- }
202
- return;
203
- }
204
-
205
- yield* walkRepoFiles({ rootPath: resolvedRoot, extensions });
206
- }
207
-
208
- export { DEFAULT_IGNORED_DIRS, MAX_FILE_SIZE_BYTES, SEVERITIES };
@@ -1,181 +1,36 @@
1
- // Shared helpers for Omar's (release) domain tools (#A19).
2
-
3
- import fsp from "node:fs/promises";
4
- import path from "node:path";
5
- import process from "node:process";
6
-
7
- import ignore from "ignore";
8
-
9
- const DEFAULT_IGNORED_DIRS = new Set([
10
- ".git",
11
- "node_modules",
12
- ".venv",
13
- ".next",
14
- "dist",
15
- "build",
16
- "coverage",
17
- ".sentinelayer",
18
- ".sentinel",
19
- ".turbo",
20
- ".idea",
21
- ".vscode",
22
- "__pycache__",
23
- ".cache",
24
- ]);
25
- const MAX_FILE_SIZE_BYTES = 1024 * 1024;
26
- const SEVERITIES = Object.freeze(["P0", "P1", "P2", "P3"]);
27
-
28
- export function toPosix(value) {
29
- return String(value || "").replace(/\\/g, "/");
30
- }
31
-
32
- export function normalizeSeverity(value) {
33
- const normalized = String(value || "").trim().toUpperCase();
34
- if (SEVERITIES.includes(normalized)) {
35
- return normalized;
36
- }
37
- return "P2";
38
- }
39
-
40
- export function createFinding({
41
- severity,
42
- kind,
43
- file,
44
- line = 0,
45
- evidence = "",
46
- rootCause = "",
47
- recommendedFix = "",
48
- confidence = null,
49
- tool = "",
50
- persona = "release",
51
- } = {}) {
52
- return {
53
- persona,
54
- tool: String(tool || "").trim(),
55
- kind: String(kind || "").trim() || "release",
56
- severity: normalizeSeverity(severity),
57
- file: toPosix(file || ""),
58
- line: Number.isFinite(Number(line)) ? Math.max(0, Math.floor(Number(line))) : 0,
59
- evidence: String(evidence || "").trim().slice(0, 400),
60
- rootCause: String(rootCause || "").trim(),
61
- recommendedFix: String(recommendedFix || "").trim(),
62
- confidence:
63
- confidence === null || confidence === undefined
64
- ? null
65
- : Math.max(0, Math.min(1, Number(confidence) || 0)),
66
- };
67
- }
68
-
69
- async function readIgnorePatterns(filePath) {
70
- try {
71
- const raw = await fsp.readFile(filePath, "utf-8");
72
- return String(raw || "")
73
- .split(/\r?\n/)
74
- .map((line) => line.trim())
75
- .filter((line) => line && !line.startsWith("#"));
76
- } catch (err) {
77
- if (err && typeof err === "object" && err.code === "ENOENT") {
78
- return [];
79
- }
80
- throw err;
81
- }
1
+ // Shared helpers for Vera's (release) domain tools (#A21).
2
+
3
+ import {
4
+ DEFAULT_IGNORED_DIRS,
5
+ MAX_FILE_SIZE_BYTES,
6
+ SEVERITIES,
7
+ createFinding as createDomainFinding,
8
+ findLineMatches,
9
+ getLineContent,
10
+ normalizeSeverity,
11
+ toPosix,
12
+ walkRepoFiles as walkDomainRepoFiles,
13
+ } from "../../shared-tools/domain-base.js";
14
+
15
+ const DEFAULT_PERSONA = "release";
16
+
17
+ export {
18
+ DEFAULT_IGNORED_DIRS,
19
+ MAX_FILE_SIZE_BYTES,
20
+ SEVERITIES,
21
+ findLineMatches,
22
+ getLineContent,
23
+ normalizeSeverity,
24
+ toPosix,
25
+ };
26
+
27
+ export function createFinding(options = {}) {
28
+ return createDomainFinding(options, {
29
+ defaultPersona: DEFAULT_PERSONA,
30
+ defaultKind: DEFAULT_PERSONA,
31
+ });
32
+ }
33
+
34
+ export async function* walkRepoFiles(options = {}) {
35
+ yield* walkDomainRepoFiles(options, { entryMetadata: "stat" });
82
36
  }
83
-
84
- async function createIgnoreMatcher(rootPath) {
85
- const matcher = ignore();
86
- const gitignore = await readIgnorePatterns(path.join(rootPath, ".gitignore"));
87
- const sentinel = await readIgnorePatterns(
88
- path.join(rootPath, ".sentinelayerignore")
89
- );
90
- matcher.add([...gitignore, ...sentinel]);
91
- return (relativePath, isDirectory) => {
92
- const normalized = toPosix(relativePath);
93
- if (!normalized) {
94
- return false;
95
- }
96
- const candidate = isDirectory ? `${normalized}/` : normalized;
97
- return matcher.ignores(candidate);
98
- };
99
- }
100
-
101
- export async function* walkRepoFiles({
102
- rootPath = process.cwd(),
103
- extensions = new Set(),
104
- maxFileSize = MAX_FILE_SIZE_BYTES,
105
- } = {}) {
106
- const resolvedRoot = path.resolve(rootPath);
107
- const ignoreMatcher = await createIgnoreMatcher(resolvedRoot);
108
- const wantedExtensions =
109
- extensions instanceof Set
110
- ? extensions
111
- : new Set(Array.isArray(extensions) ? extensions : []);
112
- const stack = [resolvedRoot];
113
- while (stack.length > 0) {
114
- const current = stack.pop();
115
- let entries = [];
116
- try {
117
- entries = await fsp.readdir(current, { withFileTypes: true });
118
- } catch {
119
- continue;
120
- }
121
- for (const entry of entries) {
122
- const fullPath = path.join(current, entry.name);
123
- const relativePath = toPosix(path.relative(resolvedRoot, fullPath));
124
- if (entry.isDirectory()) {
125
- if (!relativePath || DEFAULT_IGNORED_DIRS.has(entry.name)) {
126
- continue;
127
- }
128
- if (ignoreMatcher(relativePath, true)) {
129
- continue;
130
- }
131
- stack.push(fullPath);
132
- continue;
133
- }
134
- if (!entry.isFile()) {
135
- continue;
136
- }
137
- if (ignoreMatcher(relativePath, false)) {
138
- continue;
139
- }
140
- const ext = path.extname(entry.name).toLowerCase();
141
- if (wantedExtensions.size > 0 && !wantedExtensions.has(ext) && !wantedExtensions.has("")) {
142
- continue;
143
- }
144
- let stat = null;
145
- try {
146
- stat = await fsp.stat(fullPath);
147
- } catch {
148
- stat = null;
149
- }
150
- if (!stat || stat.size > maxFileSize) {
151
- continue;
152
- }
153
- yield { fullPath, relativePath, stat };
154
- }
155
- }
156
- }
157
-
158
- export function findLineMatches(content, pattern) {
159
- const text = String(content || "");
160
- if (!pattern) {
161
- return [];
162
- }
163
- const global = new RegExp(
164
- pattern.source,
165
- pattern.flags.includes("g") ? pattern.flags : `${pattern.flags}g`
166
- );
167
- const matches = [];
168
- let match;
169
- while ((match = global.exec(text)) !== null) {
170
- const lineIndex = text.slice(0, match.index).split(/\r?\n/).length;
171
- matches.push({ index: match.index, line: lineIndex, match: match[0] });
172
- }
173
- return matches;
174
- }
175
-
176
- export function getLineContent(content, line) {
177
- const lines = String(content || "").split(/\r?\n/);
178
- return (lines[Math.max(0, (Number(line) || 1) - 1)] || "").trim();
179
- }
180
-
181
- export { DEFAULT_IGNORED_DIRS, MAX_FILE_SIZE_BYTES, SEVERITIES };