@veda-hq/pariksha 0.3.1 → 0.3.2
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 +33 -2
- package/dist/authority-citations.js +192 -0
- package/dist/cli.js +40 -4
- package/dist/export-deletions.js +1 -2
- package/dist/git.js +16 -3
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
test-quality ratchets. Part of [Veda](https://github.com/veda-hq/veda).
|
|
5
5
|
|
|
6
6
|
```sh
|
|
7
|
-
npx @veda-hq/pariksha@0.3.
|
|
8
|
-
npx @veda-hq/pariksha@0.3.
|
|
7
|
+
npx @veda-hq/pariksha@0.3.2 <check> [flags]
|
|
8
|
+
npx @veda-hq/pariksha@0.3.2 --version
|
|
9
9
|
```
|
|
10
10
|
|
|
11
11
|
Every check exits 0 when the repository passes, 1 when it refuses something
|
|
@@ -13,6 +13,37 @@ Every check exits 0 when the repository passes, 1 when it refuses something
|
|
|
13
13
|
error. `--root <dir>` (default: the current directory) names the repository.
|
|
14
14
|
Node 24 or later. MIT.
|
|
15
15
|
|
|
16
|
+
## `authority-citations`
|
|
17
|
+
|
|
18
|
+
```sh
|
|
19
|
+
pariksha authority-citations [--mode staged|tree] [--authority <a,b>]
|
|
20
|
+
[--issue-prefix <A,B>] [--adr-dir <dir>] [--root <dir>]
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
A coherent fabrication passes every internal check: an agent that deletes a
|
|
24
|
+
public API and calls the deletion "an owner decision" is consistent with
|
|
25
|
+
itself. So a line that invokes an authority (`owner decision`,
|
|
26
|
+
`founder decided`, `product rule`, `approved by` / `decided by` /
|
|
27
|
+
`authorized by` / `mandated by` / `per` an office, a `--authority` name or an
|
|
28
|
+
`@handle`) must cite, within three lines, something only that authority could
|
|
29
|
+
have produced:
|
|
30
|
+
|
|
31
|
+
- resolved: a commit id naming a commit here, or `ADR-NNNN` /
|
|
32
|
+
`<adr-dir>/NNNN` naming an existing `NNNN-*.md` other than the claiming file
|
|
33
|
+
(default `docs/adr`);
|
|
34
|
+
- shape-checked: an issue id with a `--issue-prefix` (`ABC-123`), a GitHub
|
|
35
|
+
pull request or issue URL, or `PR #123`.
|
|
36
|
+
|
|
37
|
+
`ALLOW-AUTHORITY-PHRASE: <reason>` in the window marks words used without
|
|
38
|
+
asserting authority; the reason is required. TypeScript, JavaScript, SQL,
|
|
39
|
+
Prisma, shell and Markdown files are read, outside `node_modules`, `dist`,
|
|
40
|
+
`build`, `coverage`, `generated`, `.tmp` and `.local`.
|
|
41
|
+
|
|
42
|
+
`staged` (the default, for a pre-commit or pre-merge-commit hook) judges only
|
|
43
|
+
the lines the commit adds; on a merge, only lines new against every parent,
|
|
44
|
+
so text a merged branch already carried is not judged again. `tree` judges
|
|
45
|
+
every tracked file. When git cannot read the change it exits 2.
|
|
46
|
+
|
|
16
47
|
## `boundary`
|
|
17
48
|
|
|
18
49
|
```sh
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
// The authority-citations check: a line that invokes an authority ("owner
|
|
2
|
+
// decision", "approved by the founder", "per the CTO") must cite something only
|
|
3
|
+
// that authority could have produced, outside the file making the claim.
|
|
4
|
+
//
|
|
5
|
+
// A claim asserted inside the file it justifies cannot be checked from inside
|
|
6
|
+
// that file: an agent that deletes a public API and calls it "an owner
|
|
7
|
+
// decision" passes every internal-consistency gate, because a coherent
|
|
8
|
+
// fabrication is consistent. So the check asks for an anchor within three lines
|
|
9
|
+
// of the claim:
|
|
10
|
+
//
|
|
11
|
+
// resolved here: an ADR number whose file exists (not the claiming file),
|
|
12
|
+
// a commit id that names a commit in this repository
|
|
13
|
+
// shape-checked only: an issue id with a configured prefix (ABC-123), a
|
|
14
|
+
// GitHub pull request or issue URL, "PR #123"
|
|
15
|
+
//
|
|
16
|
+
// `ALLOW-AUTHORITY-PHRASE: <reason>` in the same window marks words that are
|
|
17
|
+
// used without asserting authority; the reason is required.
|
|
18
|
+
//
|
|
19
|
+
// staged mode judges only lines the commit adds; on a merge, only lines new
|
|
20
|
+
// against every parent, so text a merged branch already carried is not judged
|
|
21
|
+
// again. tree mode judges every tracked file.
|
|
22
|
+
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
|
|
23
|
+
import { join, posix } from 'node:path';
|
|
24
|
+
import { must, PLAIN_DIFF } from './git.js';
|
|
25
|
+
export const DEFAULTS = { authorities: [], issuePrefixes: [], adrDir: 'docs/adr' };
|
|
26
|
+
const escape = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
27
|
+
/**
|
|
28
|
+
* An authority is a person or an office, never a subsystem: requiring one after
|
|
29
|
+
* "approved by" and "per" keeps "decided by the diff status" out.
|
|
30
|
+
*/
|
|
31
|
+
export const phrasePattern = (authorities) => {
|
|
32
|
+
const names = ['owner', 'founder', 'CTO', 'CEO', 'management', 'leadership', ...authorities.map(escape), '@[A-Za-z0-9._-]+'];
|
|
33
|
+
const who = `(?:the )?(?:${names.join('|')})`;
|
|
34
|
+
return new RegExp(`owner (?:decision|decided)|founder (?:decision|decided)|product rule|(?:approved|decided|authorized|mandated) by ${who}|per ${who}`, 'i');
|
|
35
|
+
};
|
|
36
|
+
const SOURCE_EXT = /\.(?:ts|tsx|js|jsx|mjs|cjs|mts|cts|sql|prisma|sh|md)$/;
|
|
37
|
+
/** Vendored, generated and scratch trees carry no authored claims. */
|
|
38
|
+
const SKIPPED_PATH = /(^|\/)(node_modules|dist|build|coverage|\.tmp|\.local|generated)\//;
|
|
39
|
+
const WINDOW = 3;
|
|
40
|
+
/** The lines within WINDOW of the 1-based line `center`, joined. */
|
|
41
|
+
export const windowOf = (lines, center) => lines.slice(Math.max(0, center - 1 - WINDOW), center + WINDOW).join('\n');
|
|
42
|
+
/** True when the window holds the pragma with a reason, or a citation that resolves. */
|
|
43
|
+
export const hasCitation = (window, claimingFile, opts, git, adrFiles) => {
|
|
44
|
+
if (/ALLOW-AUTHORITY-PHRASE:[^\S\n]*\S/.test(window))
|
|
45
|
+
return true;
|
|
46
|
+
if (opts.issuePrefixes.length > 0 && new RegExp(`\\b(?:${opts.issuePrefixes.map(escape).join('|')})-[0-9]+\\b`).test(window))
|
|
47
|
+
return true;
|
|
48
|
+
if (/https:\/\/github\.com\/[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+\/(?:pull|issues)\/[0-9]+/.test(window))
|
|
49
|
+
return true;
|
|
50
|
+
if (/\bPR #[0-9]+\b/.test(window))
|
|
51
|
+
return true;
|
|
52
|
+
for (const [, n] of window.matchAll(new RegExp(`(?:ADR-|${escape(opts.adrDir)}/)([0-9]{4})`, 'g'))) {
|
|
53
|
+
if (adrFiles.some((f) => f.startsWith(`${opts.adrDir}/${n}-`) && f.endsWith('.md') && f !== claimingFile))
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
for (const [sha] of window.matchAll(/\b[0-9a-f]{7,40}\b/g)) {
|
|
57
|
+
if (git.run(['cat-file', '-e', `${sha}^{commit}`]) !== undefined)
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
return false;
|
|
61
|
+
};
|
|
62
|
+
/** 1-based line numbers a `-U0` diff adds. */
|
|
63
|
+
export const addedLines = (diff) => {
|
|
64
|
+
const out = new Set();
|
|
65
|
+
for (const line of diff.split('\n')) {
|
|
66
|
+
const m = /^@@ [^+]*\+([0-9]+)(?:,([0-9]+))? @@/.exec(line);
|
|
67
|
+
if (!m)
|
|
68
|
+
continue;
|
|
69
|
+
const start = Number(m[1]);
|
|
70
|
+
const count = m[2] === undefined ? 1 : Number(m[2]);
|
|
71
|
+
for (let i = 0; i < count; i++)
|
|
72
|
+
out.add(start + i);
|
|
73
|
+
}
|
|
74
|
+
return out;
|
|
75
|
+
};
|
|
76
|
+
/**
|
|
77
|
+
* The other parents of the merge being committed; none outside a merge.
|
|
78
|
+
* MERGE_HEAD exists once a merge stops (a conflict, --no-commit). A clean
|
|
79
|
+
* merge runs the pre-merge-commit hook before writing MERGE_HEAD, leaving only
|
|
80
|
+
* the GITHEAD_<sha> variables `git merge` exports: one counts only when git is
|
|
81
|
+
* merging (AUTO_MERGE is the index tree) and the index is exactly the clean
|
|
82
|
+
* merge of HEAD and that commit, so a stale or hand-set variable hides nothing.
|
|
83
|
+
* Anything else falls back to the HEAD diff, which judges more, never less.
|
|
84
|
+
*/
|
|
85
|
+
export const mergeParents = (git, env, readFile) => {
|
|
86
|
+
const parents = [];
|
|
87
|
+
const listed = readFile(must(git, ['rev-parse', '--path-format=absolute', '--git-path', 'MERGE_HEAD']).trim());
|
|
88
|
+
for (const candidate of (listed ?? '').split('\n').filter(Boolean)) {
|
|
89
|
+
const sha = git.run(['rev-parse', '-q', '--verify', `${candidate}^{commit}`])?.trim();
|
|
90
|
+
if (sha)
|
|
91
|
+
parents.push(sha);
|
|
92
|
+
}
|
|
93
|
+
// An index with unresolved conflicts has no tree, and nothing can be committed from it.
|
|
94
|
+
const indexTree = git.run(['write-tree'])?.trim();
|
|
95
|
+
if (!indexTree || git.run(['rev-parse', '-q', '--verify', 'AUTO_MERGE^{tree}'])?.trim() !== indexTree)
|
|
96
|
+
return parents;
|
|
97
|
+
for (const name of Object.keys(env)) {
|
|
98
|
+
const sha = /^GITHEAD_([0-9a-f]{40}|[0-9a-f]{64})$/.exec(name)?.[1];
|
|
99
|
+
if (sha === undefined || parents.includes(sha))
|
|
100
|
+
continue;
|
|
101
|
+
if (git.run(['merge-tree', '--write-tree', 'HEAD', sha])?.trim() === indexTree)
|
|
102
|
+
parents.push(sha);
|
|
103
|
+
}
|
|
104
|
+
return parents;
|
|
105
|
+
};
|
|
106
|
+
const nulList = (out) => out.split('\0').filter(Boolean);
|
|
107
|
+
const literal = (file) => `:(literal)${file}`;
|
|
108
|
+
/** The staged change: what the commit being made adds. */
|
|
109
|
+
export const stagedSource = (git, env, readFile) => {
|
|
110
|
+
const parents = mergeParents(git, env, readFile);
|
|
111
|
+
const added = (file, against) => addedLines(must(git, ['diff', ...PLAIN_DIFF, '--cached', '-U0', ...against, '--', literal(file)]));
|
|
112
|
+
return {
|
|
113
|
+
// T: a symlink or submodule replaced by a file brings in its every line.
|
|
114
|
+
files: nulList(must(git, ['diff', ...PLAIN_DIFF, '--cached', '-z', '--name-only', '--diff-filter=ACMT'])),
|
|
115
|
+
// `:0:` names stage 0 explicitly, so a path such as `0:a.md` is read as itself.
|
|
116
|
+
content: (file) => must(git, ['show', `:0:${file}`]),
|
|
117
|
+
judged: (file) => {
|
|
118
|
+
let lines = added(file, []);
|
|
119
|
+
for (const parent of parents) {
|
|
120
|
+
const vsParent = added(file, [parent]);
|
|
121
|
+
lines = new Set([...lines].filter((n) => vsParent.has(n)));
|
|
122
|
+
}
|
|
123
|
+
return lines;
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
};
|
|
127
|
+
/** Every tracked file, as it is in the working tree. */
|
|
128
|
+
export const treeSource = (git, root, readFile) => ({
|
|
129
|
+
files: nulList(must(git, ['ls-files', '-z'])),
|
|
130
|
+
content: (file) => readFile(join(root, file)),
|
|
131
|
+
judged: () => undefined,
|
|
132
|
+
});
|
|
133
|
+
/** Every uncited claim the source holds. */
|
|
134
|
+
export const findUncited = (source, opts, git, adrFiles) => {
|
|
135
|
+
const phrase = phrasePattern(opts.authorities);
|
|
136
|
+
const claims = [];
|
|
137
|
+
for (const file of source.files) {
|
|
138
|
+
if (!SOURCE_EXT.test(file) || SKIPPED_PATH.test(file))
|
|
139
|
+
continue;
|
|
140
|
+
const text = source.content(file);
|
|
141
|
+
if (text === undefined || !phrase.test(text))
|
|
142
|
+
continue;
|
|
143
|
+
const lines = text.split('\n');
|
|
144
|
+
const judged = source.judged(file);
|
|
145
|
+
lines.forEach((line, i) => {
|
|
146
|
+
const n = i + 1;
|
|
147
|
+
const m = phrase.exec(line);
|
|
148
|
+
if (!m || (judged !== undefined && !judged.has(n)))
|
|
149
|
+
return;
|
|
150
|
+
if (hasCitation(windowOf(lines, n), file, opts, git, adrFiles))
|
|
151
|
+
return;
|
|
152
|
+
claims.push({ file, line: n, phrase: m[0], text: line.slice(0, 120) });
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
return claims;
|
|
156
|
+
};
|
|
157
|
+
/**
|
|
158
|
+
* An ADR directory as the claiming files' paths spell it: `./docs//adr/`
|
|
159
|
+
* is `docs/adr`, so an ADR citing itself is still recognised as itself.
|
|
160
|
+
*/
|
|
161
|
+
export const adrDirOf = (dir) => posix.normalize(dir).replace(/\/+$/, '') || '.';
|
|
162
|
+
/** Files (not directories) under the ADR directory, relative to the root. */
|
|
163
|
+
export const listAdrs = (root, adrDir) => {
|
|
164
|
+
const dir = join(root, adrDir);
|
|
165
|
+
return existsSync(dir) ? readdirSync(dir).filter((f) => statSync(join(dir, f)).isFile()).map((f) => `${adrDir}/${f}`) : [];
|
|
166
|
+
};
|
|
167
|
+
/** A file's text, or undefined when it is not there (gone, or now a directory); any other failure throws. */
|
|
168
|
+
export const readOrUndefined = (path) => {
|
|
169
|
+
try {
|
|
170
|
+
return readFileSync(path, 'utf8');
|
|
171
|
+
}
|
|
172
|
+
catch (e) {
|
|
173
|
+
if (['ENOENT', 'ENOTDIR', 'EISDIR'].includes(e.code ?? ''))
|
|
174
|
+
return undefined;
|
|
175
|
+
throw e;
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
export const formatClaims = (claims, opts) => {
|
|
179
|
+
if (claims.length === 0)
|
|
180
|
+
return { ok: true, out: ['pariksha authority-citations: every authority claim cites an anchor'], err: [] };
|
|
181
|
+
const err = [];
|
|
182
|
+
let last = '';
|
|
183
|
+
for (const c of claims) {
|
|
184
|
+
if (c.file !== last)
|
|
185
|
+
err.push(`pariksha authority-citations: uncited authority claim(s) in ${c.file}:`);
|
|
186
|
+
last = c.file;
|
|
187
|
+
err.push(` ${c.line}: [${c.phrase}] ${c.text}`);
|
|
188
|
+
}
|
|
189
|
+
const files = new Set(claims.map((c) => c.file)).size;
|
|
190
|
+
err.push('', 'An authority claim must anchor in something only that authority can produce. Cite one of:', ' a commit id (owner decision, f1a0da2d)', ` an existing ADR ADR-0040, in ${opts.adrDir}/`, ...(opts.issuePrefixes.length > 0 ? [` an issue id ${opts.issuePrefixes[0]}-447`] : []), ' a GitHub pull request or issue PR #269, or its full URL', 'A comment or a document an agent could have written is not a citation. If no such', 'artifact exists, the decision has not been made. Not an authority claim? Add', ' ALLOW-AUTHORITY-PHRASE: <reason>', '', `${claims.length} claim(s) across ${files} file(s).`);
|
|
191
|
+
return { ok: false, out: [], err };
|
|
192
|
+
};
|
package/dist/cli.js
CHANGED
|
@@ -4,13 +4,15 @@ import { execFileSync } from 'node:child_process';
|
|
|
4
4
|
import { existsSync, readFileSync, realpathSync, statSync } from 'node:fs';
|
|
5
5
|
import { join, relative, resolve } from 'node:path';
|
|
6
6
|
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import { adrDirOf, DEFAULTS, findUncited, formatClaims, listAdrs, readOrUndefined, stagedSource, treeSource } from './authority-citations.js';
|
|
7
8
|
import { findDenied, hash, isText, parseDenyList } from './boundary.js';
|
|
8
9
|
import { checkBudgets } from './doc-budgets.js';
|
|
9
10
|
import { checkExportDeletions, formatReport } from './export-deletions.js';
|
|
10
|
-
import { BASE_CANDIDATES, gitAtTop, resolveBase } from './git.js';
|
|
11
|
+
import { BASE_CANDIDATES, gitAt, gitAtTop, resolveBase, topOf } from './git.js';
|
|
11
12
|
export const DENY_FILE = '.veda/deny.sha256';
|
|
12
13
|
const USAGE = [
|
|
13
|
-
'usage: pariksha
|
|
14
|
+
'usage: pariksha authority-citations [--mode staged|tree] [--authority <a,b>] [--issue-prefix <A,B>] [--adr-dir <dir>] [--root <dir>]',
|
|
15
|
+
' pariksha boundary [--root <dir>] [--deny <file>]',
|
|
14
16
|
' pariksha doc-budgets --table <file> [--section <name>] [--root <dir>]',
|
|
15
17
|
' pariksha export-deletions [--base <ref>] [--root <dir>]',
|
|
16
18
|
' pariksha hash <word>',
|
|
@@ -128,12 +130,46 @@ const exportDeletions = (f, io) => {
|
|
|
128
130
|
result.err.forEach(io.err);
|
|
129
131
|
return result.ok ? 0 : 1;
|
|
130
132
|
};
|
|
133
|
+
/** A comma-separated flag value as a list; empty entries dropped. */
|
|
134
|
+
const list = (value) => (value ?? '').split(',').map((s) => s.trim()).filter(Boolean);
|
|
135
|
+
const authorityCitations = (f, io, env) => {
|
|
136
|
+
const mode = f.get('--mode') ?? 'staged';
|
|
137
|
+
if (mode !== 'staged' && mode !== 'tree') {
|
|
138
|
+
io.err(USAGE);
|
|
139
|
+
return 2;
|
|
140
|
+
}
|
|
141
|
+
const dir = resolve(f.get('--root') ?? '.');
|
|
142
|
+
const root = topOf(dir);
|
|
143
|
+
if (root === undefined) {
|
|
144
|
+
io.err(`pariksha authority-citations: ${dir} is not in a git repository`);
|
|
145
|
+
return 2;
|
|
146
|
+
}
|
|
147
|
+
const git = gitAt(root);
|
|
148
|
+
const opts = {
|
|
149
|
+
authorities: list(f.get('--authority')),
|
|
150
|
+
issuePrefixes: list(f.get('--issue-prefix')),
|
|
151
|
+
adrDir: adrDirOf(f.get('--adr-dir') ?? DEFAULTS.adrDir),
|
|
152
|
+
};
|
|
153
|
+
let result;
|
|
154
|
+
try {
|
|
155
|
+
const source = mode === 'tree' ? treeSource(git, root, readOrUndefined) : stagedSource(git, env, readOrUndefined);
|
|
156
|
+
result = formatClaims(findUncited(source, opts, git, listAdrs(root, opts.adrDir)), opts);
|
|
157
|
+
}
|
|
158
|
+
catch (e) {
|
|
159
|
+
io.err(`pariksha authority-citations: ${e.message}; nothing was checked, so this is not a pass`);
|
|
160
|
+
return 2;
|
|
161
|
+
}
|
|
162
|
+
result.out.forEach(io.out);
|
|
163
|
+
result.err.forEach(io.err);
|
|
164
|
+
return result.ok ? 0 : 1;
|
|
165
|
+
};
|
|
131
166
|
const COMMANDS = {
|
|
167
|
+
'authority-citations': { allowed: ['--mode', '--authority', '--issue-prefix', '--adr-dir', '--root'], run: authorityCitations },
|
|
132
168
|
boundary: { allowed: ['--root', '--deny'], run: boundary },
|
|
133
169
|
'doc-budgets': { allowed: ['--table', '--section', '--root'], run: docBudgets },
|
|
134
170
|
'export-deletions': { allowed: ['--base', '--root'], run: exportDeletions },
|
|
135
171
|
};
|
|
136
|
-
export const run = (args, io, version = ownVersion()) => {
|
|
172
|
+
export const run = (args, io, version = ownVersion(), env = process.env) => {
|
|
137
173
|
const [command, ...rest] = args;
|
|
138
174
|
if (command === '--version' && rest.length === 0) {
|
|
139
175
|
io.out(version);
|
|
@@ -149,7 +185,7 @@ export const run = (args, io, version = ownVersion()) => {
|
|
|
149
185
|
io.err(USAGE);
|
|
150
186
|
return 2;
|
|
151
187
|
}
|
|
152
|
-
return spec.run(f, io);
|
|
188
|
+
return spec.run(f, io, env);
|
|
153
189
|
};
|
|
154
190
|
const isMain = () => {
|
|
155
191
|
const entry = process.argv[1];
|
package/dist/export-deletions.js
CHANGED
|
@@ -10,13 +10,12 @@
|
|
|
10
10
|
// Removes-Export: <symbol> — <why> covers one symbol
|
|
11
11
|
// Removes-File: <path> — <why> covers every export of a file that
|
|
12
12
|
// no longer exists at HEAD
|
|
13
|
-
import { must } from './git.js';
|
|
13
|
+
import { must, PLAIN_DIFF } from './git.js';
|
|
14
14
|
export const SOURCE_GLOBS = ['*.ts', '*.tsx', '*.js', '*.jsx', '*.mjs', '*.cjs', '*.mts', '*.cts'];
|
|
15
15
|
/** Build output and vendored code never count as a source change, or as a use. */
|
|
16
16
|
const SKIPPED = ['node_modules', 'dist', 'build', 'coverage', 'generated'];
|
|
17
17
|
const SKIPPED_DIR = new RegExp(`(^|/)(${SKIPPED.join('|')})/`);
|
|
18
18
|
const SKIPPED_PATHSPECS = SKIPPED.map((d) => `:(exclude,glob)**/${d}/**`);
|
|
19
|
-
const PLAIN_DIFF = ['--no-renames', '--no-color', '--no-ext-diff', '--no-textconv'];
|
|
20
19
|
/**
|
|
21
20
|
* A removed named declaration on a `-` diff line. Bare re-export lists
|
|
22
21
|
* (`export { a, b }`) are left out: they shuffle in refactors and name nothing new.
|
package/dist/git.js
CHANGED
|
@@ -5,12 +5,17 @@ import { execFileSync } from 'node:child_process';
|
|
|
5
5
|
* quoted non-ASCII paths would name no file, and a column field would hide a
|
|
6
6
|
* grep hit's text.
|
|
7
7
|
*/
|
|
8
|
-
export const PINNED = ['-c', 'core.quotePath=off', '-c', 'grep.column=false', '-c', 'color.ui=never'];
|
|
8
|
+
export const PINNED = ['-c', 'core.quotePath=off', '-c', 'grep.column=false', '-c', 'color.ui=never', '-c', 'core.precomposeUnicode=false'];
|
|
9
|
+
/** Variables that change how git reads a pathspec, so `:(literal)` would name no file. */
|
|
10
|
+
const PATHSPEC_ENV = ['GIT_LITERAL_PATHSPECS', 'GIT_GLOB_PATHSPECS', 'GIT_NOGLOB_PATHSPECS', 'GIT_ICASE_PATHSPECS'];
|
|
11
|
+
/** The environment git runs in: the caller's, less the pathspec switches. */
|
|
12
|
+
export const gitEnv = (env) => Object.fromEntries(Object.entries(env).filter(([k]) => !PATHSPEC_ENV.includes(k)));
|
|
9
13
|
export const gitAt = (root) => ({
|
|
10
14
|
run: (args) => {
|
|
11
15
|
try {
|
|
12
16
|
return execFileSync('git', [...PINNED, ...args], {
|
|
13
17
|
cwd: root,
|
|
18
|
+
env: gitEnv(process.env),
|
|
14
19
|
encoding: 'utf8',
|
|
15
20
|
maxBuffer: 1 << 28,
|
|
16
21
|
stdio: ['ignore', 'pipe', 'ignore'],
|
|
@@ -28,9 +33,17 @@ export const gitAt = (root) => ({
|
|
|
28
33
|
* there whichever directory it was started in.
|
|
29
34
|
*/
|
|
30
35
|
export const gitAtTop = (dir) => {
|
|
31
|
-
const top =
|
|
32
|
-
return top ? gitAt(top)
|
|
36
|
+
const top = topOf(dir);
|
|
37
|
+
return top === undefined ? undefined : gitAt(top);
|
|
33
38
|
};
|
|
39
|
+
/** The top level of the repository holding `dir`, or undefined outside one. */
|
|
40
|
+
export const topOf = (dir) => gitAt(dir).run(['rev-parse', '--show-toplevel'])?.trim() || undefined;
|
|
41
|
+
/**
|
|
42
|
+
* Diff output a user's config cannot reshape: no colour, external tool,
|
|
43
|
+
* textconv or rename pairing, and lines even for a file git or its
|
|
44
|
+
* attributes class as binary, which would otherwise show no hunk at all.
|
|
45
|
+
*/
|
|
46
|
+
export const PLAIN_DIFF = ['--no-renames', '--no-color', '--no-ext-diff', '--no-textconv', '--text'];
|
|
34
47
|
/** A git call whose failure must stop the check rather than read as "nothing found". */
|
|
35
48
|
export const must = (git, args) => {
|
|
36
49
|
const out = git.run(args);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@veda-hq/pariksha",
|
|
3
|
-
"version": "0.3.
|
|
4
|
-
"description": "Repository checks: doc drift, rule enforcement, test-quality ratchets. Checks: boundary, doc-budgets, export-deletions.",
|
|
3
|
+
"version": "0.3.2",
|
|
4
|
+
"description": "Repository checks: doc drift, rule enforcement, test-quality ratchets. Checks: authority-citations, boundary, doc-budgets, export-deletions.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"repository checks",
|
|
7
7
|
"lint",
|