@veda-hq/pariksha 0.5.13 → 0.5.17

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,193 @@
1
+ // Just enough shell lexing for the commands a doc shows: a `#` comment cut
2
+ // off, and quoted text masked so an operator inside a string splits nothing.
3
+ // Not a parser; each caller says what it reads.
4
+ /**
5
+ * The characters of `code` from `start`, each with its index. Every scanner
6
+ * walks this bounded sequence, skipping indexes below the point a token it
7
+ * consumed ends at, so no scan runs past the end or loops forever. Lazy: a
8
+ * scan that stops early never builds the rest.
9
+ */
10
+ function* charsFrom(code, start) {
11
+ for (let index = start; index < code.length; index += 1)
12
+ yield [index, code.charAt(index)];
13
+ }
14
+ /** A line continued by backslash-newline: an odd run of trailing backslashes. */
15
+ const CONTINUATION = /(?:^|[^\\])(?:\\\\)*\\$/;
16
+ /**
17
+ * Whether `#` at `at` starts a word (a comment), not `$#`, `a#b`, `${#x}` or
18
+ * `{#a,b}`: the line starts there, or a blank, `;` or `(` precedes it.
19
+ */
20
+ const startsWord = (line, at) => /(?:^|[\s;(])$/.test(line.slice(0, at));
21
+ /**
22
+ * Where an unquoted `#` comment starts in `line` (-1 if none). A `$(…)`
23
+ * inside double quotes has its own quotes, so `"$(printf "%s" " #")"` holds
24
+ * no comment.
25
+ */
26
+ const commentAt = (line) => {
27
+ const stack = [];
28
+ let resume = 0;
29
+ for (const [at, char] of charsFrom(line, 0)) {
30
+ if (at < resume)
31
+ continue;
32
+ const top = stack.at(-1);
33
+ if (top === "'") {
34
+ if (char === "'")
35
+ stack.pop();
36
+ }
37
+ else if (char === '\\') {
38
+ resume = at + 2;
39
+ }
40
+ else if (top === '"') {
41
+ if (char === '"')
42
+ stack.pop();
43
+ if (line.startsWith('$(', at)) {
44
+ stack.push(')');
45
+ resume = at + 2;
46
+ }
47
+ }
48
+ else if (char === '#' && startsWord(line, at)) {
49
+ // At top level or inside a quoted `$(…)` body, the rest is a comment.
50
+ return at;
51
+ }
52
+ else if (char === '"' || char === "'") {
53
+ stack.push(char);
54
+ }
55
+ else if (top === ')' && char === '(') {
56
+ stack.push(')');
57
+ }
58
+ else if (top === ')' && char === ')') {
59
+ stack.pop();
60
+ }
61
+ }
62
+ return -1;
63
+ };
64
+ /**
65
+ * `line` without a `#` comment that starts a word outside quotes, trailing
66
+ * blanks trimmed, except an escaped one: `echo ok \ # note` keeps `\ `,
67
+ * which is no continuation.
68
+ */
69
+ export const stripComment = (line) => {
70
+ const at = commentAt(line);
71
+ if (at === -1)
72
+ return line;
73
+ const trimmed = line.slice(0, at).trimEnd();
74
+ return CONTINUATION.test(trimmed) ? `${trimmed} ` : trimmed;
75
+ };
76
+ /** Whether `$(` at `at` opens a command substitution (`$((` is arithmetic). */
77
+ const opensSubstitution = (code, at) => code.startsWith('$(', at) && !code.startsWith('$((', at);
78
+ /**
79
+ * Outside quotes, `\` makes the next character literal. An escaped operator,
80
+ * quote, `$` or backtick is blanked with its backslash; any other stays as
81
+ * written. Returns the index past the escape.
82
+ */
83
+ const maskEscape = (code, at, out) => {
84
+ if (/^\\[;&|()<>`$"']/.test(code.slice(at)))
85
+ out.fill(' ', at, at + 2);
86
+ return at + 2;
87
+ };
88
+ /** Blanks a single-quoted body from `start`; returns the index past its close. */
89
+ const maskSingle = (code, start, out) => {
90
+ const close = code.indexOf("'", start);
91
+ const end = close === -1 ? code.length : close;
92
+ out.fill(' ', start, end);
93
+ return end + 1;
94
+ };
95
+ /** What the substitution skippers return when the body never closes. */
96
+ const NEVER_CLOSES = Number.POSITIVE_INFINITY;
97
+ /** A case pattern just before its `)`: after `in`, `;;` or a line start. */
98
+ const CASE_PATTERN_BEFORE = /(?:^|\bin|;;|\n)\s*[^\s;&|()]+(?:\s*\|\s*[^\s;&|()]+)*\s*$/;
99
+ /** Whether a `)` after `before` ends a pattern of a `case` open in `before`. */
100
+ const closesCasePattern = (before) => {
101
+ const cases = before.match(/\bcase\b/g)?.length ?? 0;
102
+ const ends = before.match(/\besac\b/g)?.length ?? 0;
103
+ return cases > ends && CASE_PATTERN_BEFORE.test(before);
104
+ };
105
+ /** Leaves a backtick substitution's body as code, masking the strings inside it; `open` is the opening backtick. */
106
+ const skipBacktick = (code, open, out) => {
107
+ let resume = open;
108
+ for (const [at, char] of charsFrom(code, open + 1)) {
109
+ if (at < resume)
110
+ continue;
111
+ if (char === '`')
112
+ return at + 1;
113
+ if (char === '\\')
114
+ resume = maskEscape(code, at, out);
115
+ else if (char === "'")
116
+ resume = maskSingle(code, at + 1, out);
117
+ else if (char === '"')
118
+ resume = maskDouble(code, at + 1, out);
119
+ }
120
+ return NEVER_CLOSES;
121
+ };
122
+ /** Leaves a `$( … )` body as code, masking strings inside it; returns the index past `)`. */
123
+ const skipSubstitution = (code, start, out) => {
124
+ let depth = 1;
125
+ let resume = start;
126
+ for (const [at, char] of charsFrom(code, start)) {
127
+ if (at < resume)
128
+ continue;
129
+ if (char === '\\')
130
+ resume = maskEscape(code, at, out);
131
+ else if (char === "'")
132
+ resume = maskSingle(code, at + 1, out);
133
+ else if (char === '"')
134
+ resume = maskDouble(code, at + 1, out);
135
+ else if (char === '(')
136
+ depth += 1;
137
+ else if (char === ')' && !closesCasePattern(code.slice(start, at))) {
138
+ // A case arm's `)` (`$(case $x in a) …; esac)`) closes nothing.
139
+ depth -= 1;
140
+ if (depth === 0)
141
+ return at + 1;
142
+ }
143
+ }
144
+ return NEVER_CLOSES;
145
+ };
146
+ /**
147
+ * Blanks a double-quoted body from `start`, leaving `$(…)` and backtick
148
+ * substitutions as code; returns the index past the closing quote.
149
+ */
150
+ function maskDouble(code, start, out) {
151
+ let resume = start;
152
+ for (const [at, char] of charsFrom(code, start)) {
153
+ if (at < resume)
154
+ continue;
155
+ if (char === '"')
156
+ return at + 1;
157
+ if (char === '\\') {
158
+ out.fill(' ', at, at + 2);
159
+ resume = at + 2;
160
+ }
161
+ else if (opensSubstitution(code, at)) {
162
+ resume = skipSubstitution(code, at + 2, out);
163
+ }
164
+ else if (char === '`') {
165
+ resume = skipBacktick(code, at, out);
166
+ }
167
+ else {
168
+ out[at] = ' ';
169
+ }
170
+ }
171
+ return code.length;
172
+ }
173
+ /**
174
+ * Blanks the contents of quoted strings, keeping every index in place so a
175
+ * position found in the masked text is the same position in `code`:
176
+ * `"a b"` → `" "`. A command substitution inside double quotes runs, so
177
+ * it stays visible; an escaped operator or quote is blanked too.
178
+ */
179
+ export const maskQuoted = (code) => {
180
+ const out = code.split('');
181
+ let resume = 0;
182
+ for (const [index, char] of charsFrom(code, 0)) {
183
+ if (index < resume)
184
+ continue;
185
+ if (char === '\\')
186
+ resume = maskEscape(code, index, out);
187
+ else if (char === "'")
188
+ resume = maskSingle(code, index + 1, out);
189
+ else if (char === '"')
190
+ resume = maskDouble(code, index + 1, out);
191
+ }
192
+ return out.join('');
193
+ };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@veda-hq/pariksha",
3
- "version": "0.5.13",
4
- "description": "Repository checks: doc drift, rule enforcement, test-quality ratchets. Checks: authority-citations, boundary, codec-registry, coverage-by-type, coverage-exemptions, doc-budgets, enforcement-claims, env-gate-switches, export-deletions, fault-injection-registry, required-settings. Also markdownlint rules for Markdown that renders mangled: @veda-hq/pariksha/markdownlint.",
3
+ "version": "0.5.17",
4
+ "description": "Repository checks: doc drift, rule enforcement, test-quality ratchets. Checks: absolute-paths, aggregate-gate-needs, authority-citations, boundary, codec-registry, coverage-by-type, coverage-exemptions, doc-budgets, doc-references, enforcement-claims, env-gate-switches, export-deletions, fault-injection-registry, non-synced-resources, pnpm-settings, required-settings. Also markdownlint rules for Markdown that renders mangled: @veda-hq/pariksha/markdownlint.",
5
5
  "keywords": [
6
6
  "repository checks",
7
7
  "lint",
@@ -36,7 +36,8 @@
36
36
  },
37
37
  "files": [
38
38
  "dist",
39
- "baselines"
39
+ "baselines",
40
+ "NOTICE"
40
41
  ],
41
42
  "dependencies": {
42
43
  "js-yaml": "4.3.2"
@@ -56,6 +57,7 @@
56
57
  "devDependencies": {
57
58
  "@types/node": "24.19.2",
58
59
  "eslint": "9.39.1",
60
+ "mdast-util-from-markdown": "2.0.3",
59
61
  "typescript": "5.9.3"
60
62
  },
61
63
  "scripts": {