@mnemonik/shared 7.119.1 → 7.120.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.
@@ -0,0 +1,149 @@
1
+ /**
2
+ * Judge a stored or pushed path the way the daemon judged it, from the rules
3
+ * the daemon uploaded (`projects.scanner_ignore_patterns`).
4
+ *
5
+ * The daemon walks the disk with every rule file in front of it. The server
6
+ * never sees the disk; it has the root .gitignore / .mnemonikignore lines (the
7
+ * privacy list) plus the tagged nested-ignore and .gitattributes lines the
8
+ * daemon uploads beside them (`indexClassifier.ts`, "Rule upload"). This
9
+ * module rebuilds the same precedence from those lines so the /scan/push
10
+ * backstop, file_push, the dropped-path marker bookkeeping and retirement all
11
+ * reach the daemon's answer:
12
+ *
13
+ * hard built-in directories, secret files, the root ignore patterns
14
+ * (makeIgnoreMatcher - unchanged, never overridable), and a nested
15
+ * ignore file that ignores the path;
16
+ * soft classifyForIndex: `!pattern` and .gitattributes overrides, then the
17
+ * soft defaults - path rules always, content rules when content is
18
+ * supplied.
19
+ */
20
+
21
+ import ignore, { type Ignore } from 'ignore';
22
+ import { AUTHORITY_FILE_MATCHERS, makeIgnoreMatcher } from './codeScanner.js';
23
+ import {
24
+ classifyForIndex,
25
+ decodeIndexRules,
26
+ parseGitattributes,
27
+ softDefaultForContent,
28
+ type GitattributesLayer,
29
+ type IgnoreFileVerdict,
30
+ type IndexExclusionReason,
31
+ } from './indexClassifier.js';
32
+
33
+ export type IndexJudgement =
34
+ | {
35
+ excluded: false;
36
+ /**
37
+ * Why the soft defaults did not apply: a project rule re-included the
38
+ * path, or it is an authority file (`AUTHORITY_FILE_MATCHERS`) - collected
39
+ * whole for doc-truth, never chunked, and never judged by the defaults on
40
+ * the daemon either. Content rules do not run for any of them.
41
+ */
42
+ override?: 'ignore_file' | 'gitattributes' | 'authority';
43
+ }
44
+ | { excluded: true; kind: 'hard'; rule: string }
45
+ | { excluded: true; kind: 'soft'; reason: IndexExclusionReason; rule: string };
46
+
47
+ export interface IndexJudge {
48
+ /** Hard rules and the path-only soft defaults, with the project's overrides. */
49
+ judgePath(relPath: string): IndexJudgement;
50
+ /**
51
+ * `judgePath`, then the content rules when `content` (decoded, scrubbed
52
+ * text) is supplied and no project rule re-included the path.
53
+ */
54
+ judge(relPath: string, content?: string): IndexJudgement;
55
+ }
56
+
57
+ interface IgnoreLayer {
58
+ baseRel: string;
59
+ ig: Ignore;
60
+ }
61
+
62
+ function compile(lines: readonly string[]): Ignore | null {
63
+ const cleaned = lines.filter((line) => typeof line === 'string' && line.trim().length > 0);
64
+ if (cleaned.length === 0) return null;
65
+ try {
66
+ return ignore().add([...cleaned]);
67
+ } catch {
68
+ return null;
69
+ }
70
+ }
71
+
72
+ /** Deepest layer with an opinion wins, as CodeScanner's walk resolves it. */
73
+ function verdictFor(path: string, layers: readonly IgnoreLayer[]): IgnoreFileVerdict {
74
+ for (let i = layers.length - 1; i >= 0; i--) {
75
+ const layer = layers[i];
76
+ if (!layer) continue;
77
+ let sub: string;
78
+ if (layer.baseRel === '') sub = path;
79
+ else if (path.startsWith(`${layer.baseRel}/`)) sub = path.slice(layer.baseRel.length + 1);
80
+ else continue;
81
+ try {
82
+ const res = layer.ig.test(sub);
83
+ if (res.ignored) return 'ignored';
84
+ if (res.unignored) return 'unignored';
85
+ } catch {
86
+ // `ignore` throws on paths it considers invalid; such a path has no verdict.
87
+ }
88
+ }
89
+ return 'unmatched';
90
+ }
91
+
92
+ const depth = (baseRel: string): number => (baseRel === '' ? 0 : baseRel.split('/').length);
93
+
94
+ export function makeIndexJudge(storedPatterns: readonly string[] = []): IndexJudge {
95
+ const rules = decodeIndexRules(storedPatterns);
96
+ // The privacy net is exactly what it was: root patterns, built-in dirs, secrets.
97
+ const isHardIgnored = makeIgnoreMatcher(rules.rootIgnore);
98
+
99
+ const layers: IgnoreLayer[] = [];
100
+ const root = compile(rules.rootIgnore);
101
+ if (root) layers.push({ baseRel: '', ig: root });
102
+ const nested = [...rules.nestedIgnore.entries()].sort(([a], [b]) => depth(a) - depth(b));
103
+ for (const [baseRel, lines] of nested) {
104
+ const ig = compile(lines);
105
+ if (ig) layers.push({ baseRel, ig });
106
+ }
107
+
108
+ const attributes: GitattributesLayer[] = [...rules.attributes.entries()].map(
109
+ ([baseRel, lines]) => ({ baseRel, rules: parseGitattributes(lines.join('\n')) })
110
+ );
111
+
112
+ const judgePath = (relPath: string): IndexJudgement => {
113
+ const path = relPath.replace(/\\/g, '/');
114
+ if (!path) return { excluded: false };
115
+ if (isHardIgnored(path)) return { excluded: true, kind: 'hard', rule: 'ignore rule' };
116
+ const verdict = verdictFor(path, layers);
117
+ if (verdict === 'ignored') {
118
+ return { excluded: true, kind: 'hard', rule: 'nested ignore file' };
119
+ }
120
+ if (AUTHORITY_FILE_MATCHERS.some((matches) => matches(path))) {
121
+ return { excluded: false, override: 'authority' };
122
+ }
123
+ const classification = classifyForIndex(path, {
124
+ ignoreFileVerdict: verdict,
125
+ attributes,
126
+ });
127
+ if (!classification.include) {
128
+ return {
129
+ excluded: true,
130
+ kind: 'soft',
131
+ reason: classification.reason,
132
+ rule: classification.rule,
133
+ };
134
+ }
135
+ return classification.override
136
+ ? { excluded: false, override: classification.override }
137
+ : { excluded: false };
138
+ };
139
+
140
+ return {
141
+ judgePath,
142
+ judge(relPath: string, content?: string): IndexJudgement {
143
+ const byPath = judgePath(relPath);
144
+ if (byPath.excluded || byPath.override || content === undefined) return byPath;
145
+ const byContent = softDefaultForContent(relPath, content);
146
+ return byContent ? { excluded: true, kind: 'soft', ...byContent } : byPath;
147
+ },
148
+ };
149
+ }