@hadden-industries/markdown-quality 1.0.1 → 1.0.3
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/assets/.editorconfig +4 -0
- package/docs/consumer-guide.md +26 -4
- package/docs/support-and-release.md +22 -13
- package/package.json +3 -3
- package/src/analysis.js +2 -2
- package/src/document-analysis.js +61 -23
- package/src/document-memo.js +97 -0
- package/src/document-worker.js +151 -24
- package/src/formatting.js +54 -9
- package/src/literal-layout.js +45 -11
- package/src/native-checks.js +247 -0
- package/src/native-staging.js +218 -0
- package/src/prose-diagnostics.js +119 -0
- package/src/quality.js +173 -39
- package/src/whitespace.js +57 -0
package/src/literal-layout.js
CHANGED
|
@@ -4,10 +4,12 @@ import * as prettier from "prettier";
|
|
|
4
4
|
import { parse } from "./analysis.js";
|
|
5
5
|
import { fail } from "./contracts.js";
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
/** Return split-array code-body row indices, optionally collecting empty indented rows. */
|
|
8
|
+
export function codeBodyRows(
|
|
9
|
+
text,
|
|
10
|
+
tree = parse(text),
|
|
11
|
+
emptyIndentedRows = null,
|
|
12
|
+
) {
|
|
11
13
|
const rows = text.split(/(\r\n|\r|\n)/u);
|
|
12
14
|
const selected = new Set();
|
|
13
15
|
function visit(node, quoteDepth = 0) {
|
|
@@ -45,20 +47,52 @@ export async function formatLayout(text, options) {
|
|
|
45
47
|
const endLine =
|
|
46
48
|
node.position.end.line -
|
|
47
49
|
(closed || node.position.end.column === 1 ? 1 : 0);
|
|
50
|
+
const valueRows =
|
|
51
|
+
!opening && emptyIndentedRows ? node.value.split(/\r\n|\r|\n/u) : null;
|
|
48
52
|
for (
|
|
49
53
|
let line = node.position.start.line - (opening ? 0 : 1);
|
|
50
54
|
line < endLine;
|
|
51
55
|
line++
|
|
52
|
-
)
|
|
53
|
-
if (
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
selected.add(line * 2);
|
|
56
|
+
) {
|
|
57
|
+
if (rows[line * 2] !== undefined) selected.add(line * 2);
|
|
58
|
+
if (valueRows?.[line - node.position.start.line + 1] === "")
|
|
59
|
+
emptyIndentedRows.add(line * 2);
|
|
60
|
+
}
|
|
58
61
|
}
|
|
59
62
|
for (const child of node.children ?? []) visit(child, quoteDepth);
|
|
60
63
|
}
|
|
61
|
-
visit(
|
|
64
|
+
visit(tree);
|
|
65
|
+
return selected;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// embeddedLanguageFormatting: off still allows Prettier to trim code-line
|
|
69
|
+
// whitespace. Keep those lines nonempty until layout finishes, then remove the
|
|
70
|
+
// absent marker before native analysis or the unchanged semantic guard runs.
|
|
71
|
+
export async function formatLayout(text, options, memo) {
|
|
72
|
+
return memo
|
|
73
|
+
? memo.layout(text, options, () =>
|
|
74
|
+
protectLiteralLayout(text, options, memo),
|
|
75
|
+
)
|
|
76
|
+
: protectLiteralLayout(text, options);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function protectLiteralLayout(text, options, memo) {
|
|
80
|
+
const rows = text.split(/(\r\n|\r|\n)/u);
|
|
81
|
+
const emptyIndentedRows = new Set();
|
|
82
|
+
const codeRows = codeBodyRows(
|
|
83
|
+
text,
|
|
84
|
+
memo ? memo.parse(text) : parse(text),
|
|
85
|
+
emptyIndentedRows,
|
|
86
|
+
);
|
|
87
|
+
const selected = new Set(
|
|
88
|
+
[...codeRows].filter(
|
|
89
|
+
// An empty indented-code value needs no protection. A marker without its
|
|
90
|
+
// missing indentation would become prose and split the code block.
|
|
91
|
+
(line) =>
|
|
92
|
+
!emptyIndentedRows.has(line) &&
|
|
93
|
+
(rows[line] === "" || /\s$/u.test(rows[line])),
|
|
94
|
+
),
|
|
95
|
+
);
|
|
62
96
|
if (!selected.size) return prettier.format(text, options);
|
|
63
97
|
let marker;
|
|
64
98
|
for (let code = 0xe000; code < 0xe100; code++) {
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
2
|
+
import { runNative } from "./native-tool.js";
|
|
3
|
+
import { spawnSync } from "node:child_process";
|
|
4
|
+
import { lstatSync, readFileSync } from "node:fs";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { decode, fail, limits } from "./contracts.js";
|
|
8
|
+
import {
|
|
9
|
+
removeStagedFile,
|
|
10
|
+
nativeCheckConfig,
|
|
11
|
+
verifyNativeStaging,
|
|
12
|
+
writeStagedFile,
|
|
13
|
+
} from "./native-staging.js";
|
|
14
|
+
|
|
15
|
+
/** Bind an admitted check to its exact bytes, executable identity and owned policy. */
|
|
16
|
+
export function bindNativeCheck(tool, input, report) {
|
|
17
|
+
return {
|
|
18
|
+
input,
|
|
19
|
+
toolSha256: tool.sha256,
|
|
20
|
+
configuration: nativeCheckConfig,
|
|
21
|
+
report,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
export function matchesNativeCheck(tool, input, check) {
|
|
25
|
+
return (
|
|
26
|
+
check?.input === input &&
|
|
27
|
+
check.toolSha256 === tool.sha256 &&
|
|
28
|
+
check.configuration === nativeCheckConfig &&
|
|
29
|
+
typeof check.report?.wouldReformat === "boolean" &&
|
|
30
|
+
Array.isArray(check.report.diagnostics)
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
function reportBound(text, staging) {
|
|
34
|
+
return (
|
|
35
|
+
3200 * text.split("\n").length +
|
|
36
|
+
512 +
|
|
37
|
+
(staging
|
|
38
|
+
? Buffer.byteLength(JSON.stringify(join(staging.path, "input-31.md")))
|
|
39
|
+
: 0)
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
/** Avoid speculative checking where the qualified file protocol cannot apply. */
|
|
43
|
+
export function nativeFileCheckEligible(text, staging) {
|
|
44
|
+
return (
|
|
45
|
+
Boolean(staging) &&
|
|
46
|
+
Buffer.byteLength(text) <= limits.fileBytes &&
|
|
47
|
+
reportBound(text, staging) + 2 <= limits.nativeOutputBytes
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Check bounded exact snapshots; partition before invocation, never retry failed groups. */
|
|
52
|
+
export function runNativeChecks(tool, texts, staging, observer) {
|
|
53
|
+
const results = [];
|
|
54
|
+
let pending = [],
|
|
55
|
+
bound = 2,
|
|
56
|
+
bytes = 0;
|
|
57
|
+
function observe(indices, operation) {
|
|
58
|
+
observer?.start(indices);
|
|
59
|
+
const started = performance.now();
|
|
60
|
+
try {
|
|
61
|
+
return operation();
|
|
62
|
+
} finally {
|
|
63
|
+
observer?.finish(indices, performance.now() - started);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
function flush() {
|
|
67
|
+
if (!pending.length) return;
|
|
68
|
+
const group = pending;
|
|
69
|
+
pending = [];
|
|
70
|
+
bound = 2;
|
|
71
|
+
bytes = 0;
|
|
72
|
+
results.push(
|
|
73
|
+
...observe(
|
|
74
|
+
group.map((item) => item.index),
|
|
75
|
+
() =>
|
|
76
|
+
checkFiles(
|
|
77
|
+
tool,
|
|
78
|
+
group.map((item) => item.text),
|
|
79
|
+
staging,
|
|
80
|
+
),
|
|
81
|
+
),
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
for (const [index, text] of texts.entries()) {
|
|
85
|
+
const inputBytes = Buffer.byteLength(text);
|
|
86
|
+
if (inputBytes > limits.fileBytes)
|
|
87
|
+
fail("DOCUMENT_LIMIT", "Native input exceeds the size limit.");
|
|
88
|
+
// Pinned check.rs emits at most two retained findings per source line and
|
|
89
|
+
// at most 203 excerpt characters. Include worst JSON escaping, pretty-print
|
|
90
|
+
// overhead and the complete staged pathname; keep large cases on stdin.
|
|
91
|
+
const outputBound = reportBound(text, staging);
|
|
92
|
+
if (!staging || outputBound + 2 > limits.nativeOutputBytes) {
|
|
93
|
+
flush();
|
|
94
|
+
results.push(
|
|
95
|
+
observe([index], () => ({
|
|
96
|
+
diagnostics: runNative(tool, text, true),
|
|
97
|
+
wouldReformat: null,
|
|
98
|
+
})),
|
|
99
|
+
);
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
if (
|
|
103
|
+
pending.length === 32 ||
|
|
104
|
+
bytes + inputBytes > 4_194_304 ||
|
|
105
|
+
bound + outputBound > limits.nativeOutputBytes
|
|
106
|
+
)
|
|
107
|
+
flush();
|
|
108
|
+
pending.push({ index, text });
|
|
109
|
+
bytes += inputBytes;
|
|
110
|
+
bound += outputBound;
|
|
111
|
+
}
|
|
112
|
+
flush();
|
|
113
|
+
return results;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function checkFiles(tool, texts, staging) {
|
|
117
|
+
verifyNativeStaging(staging);
|
|
118
|
+
const files = [];
|
|
119
|
+
try {
|
|
120
|
+
for (const [index, text] of texts.entries())
|
|
121
|
+
files.push(writeStagedFile(staging, `input-${index}.md`, text));
|
|
122
|
+
verifyNativeStaging(staging);
|
|
123
|
+
for (const [index, file] of files.entries()) {
|
|
124
|
+
const stat = lstatSync(file.path, { bigint: true });
|
|
125
|
+
if (
|
|
126
|
+
!stat.isFile() ||
|
|
127
|
+
stat.isSymbolicLink() ||
|
|
128
|
+
stat.nlink !== 1n ||
|
|
129
|
+
Object.keys(file.identity).some(
|
|
130
|
+
(key) => stat[key] !== file.identity[key],
|
|
131
|
+
) ||
|
|
132
|
+
stat.size !== BigInt(Buffer.byteLength(texts[index])) ||
|
|
133
|
+
readFileSync(file.path, "utf8") !== texts[index]
|
|
134
|
+
)
|
|
135
|
+
fail("NATIVE_STAGING", "Private native input changed before checking.");
|
|
136
|
+
}
|
|
137
|
+
const result = spawnSync(
|
|
138
|
+
tool.executable,
|
|
139
|
+
[
|
|
140
|
+
"--native",
|
|
141
|
+
"--config",
|
|
142
|
+
join(staging.path, "snapper-check.toml"),
|
|
143
|
+
"--format",
|
|
144
|
+
"markdown",
|
|
145
|
+
"--max-width",
|
|
146
|
+
"0",
|
|
147
|
+
"--color",
|
|
148
|
+
"never",
|
|
149
|
+
"--check",
|
|
150
|
+
"--output-format",
|
|
151
|
+
"json",
|
|
152
|
+
...files.map((file) => file.path),
|
|
153
|
+
],
|
|
154
|
+
{
|
|
155
|
+
cwd: fileURLToPath(new URL("../assets/", import.meta.url)),
|
|
156
|
+
env: {
|
|
157
|
+
...Object.fromEntries(
|
|
158
|
+
["SystemRoot", "WINDIR", "TEMP", "TMP", "LANG", "LC_ALL"]
|
|
159
|
+
.filter((key) => process.env[key])
|
|
160
|
+
.map((key) => [key, process.env[key]]),
|
|
161
|
+
),
|
|
162
|
+
RAYON_NUM_THREADS: "1",
|
|
163
|
+
},
|
|
164
|
+
shell: false,
|
|
165
|
+
timeout: limits.nativeMs,
|
|
166
|
+
maxBuffer: limits.nativeOutputBytes,
|
|
167
|
+
windowsHide: true,
|
|
168
|
+
},
|
|
169
|
+
);
|
|
170
|
+
return readReports(result, files, texts);
|
|
171
|
+
} finally {
|
|
172
|
+
for (const file of files) removeStagedFile(staging, file);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function readReports(result, files, texts) {
|
|
177
|
+
if (result.error || result.signal || ![0, 1].includes(result.status))
|
|
178
|
+
fail("NATIVE_FAILURE", "Native check failed or exceeded resource limits.");
|
|
179
|
+
let reports;
|
|
180
|
+
try {
|
|
181
|
+
reports = JSON.parse(decode(result.stdout));
|
|
182
|
+
} catch {
|
|
183
|
+
fail("NATIVE_REPORT", "Malformed native JSON.");
|
|
184
|
+
}
|
|
185
|
+
if (!Array.isArray(reports) || reports.length > files.length)
|
|
186
|
+
fail("NATIVE_REPORT", "Invalid native group report.");
|
|
187
|
+
// Source/binary fixtures qualify omitted clean records: every eligible owned
|
|
188
|
+
// file is processed before JSON is emitted, and all I/O failures exit fatally.
|
|
189
|
+
const checked = texts.map(() => ({ diagnostics: [], wouldReformat: false }));
|
|
190
|
+
const paths = new Map(files.map((file, index) => [file.path, index]));
|
|
191
|
+
const seen = new Set();
|
|
192
|
+
for (const report of reports) {
|
|
193
|
+
if (
|
|
194
|
+
!report ||
|
|
195
|
+
typeof report !== "object" ||
|
|
196
|
+
Array.isArray(report) ||
|
|
197
|
+
Object.keys(report).length !== 5 ||
|
|
198
|
+
!paths.has(report.file) ||
|
|
199
|
+
seen.has(report.file)
|
|
200
|
+
)
|
|
201
|
+
fail("NATIVE_REPORT", "Invalid native group attribution.");
|
|
202
|
+
const index = paths.get(report.file),
|
|
203
|
+
text = texts[index];
|
|
204
|
+
const sourceLines =
|
|
205
|
+
text === "" ? 0 : text.split("\n").length - Number(text.endsWith("\n"));
|
|
206
|
+
if (
|
|
207
|
+
report.original_lines !== sourceLines ||
|
|
208
|
+
!Number.isSafeInteger(report.formatted_lines) ||
|
|
209
|
+
report.formatted_lines < 0 ||
|
|
210
|
+
report.formatted_lines > limits.nativeOutputBytes ||
|
|
211
|
+
typeof report.would_reformat !== "boolean" ||
|
|
212
|
+
!Array.isArray(report.diagnostics) ||
|
|
213
|
+
(!report.would_reformat && !report.diagnostics.length)
|
|
214
|
+
)
|
|
215
|
+
fail("NATIVE_REPORT", "Invalid native group coverage.");
|
|
216
|
+
seen.add(report.file);
|
|
217
|
+
const findings = new Set();
|
|
218
|
+
for (const diagnostic of report.diagnostics) {
|
|
219
|
+
if (
|
|
220
|
+
!diagnostic ||
|
|
221
|
+
Object.keys(diagnostic).length !== 3 ||
|
|
222
|
+
!["fused", "wrap", "long"].includes(diagnostic.kind) ||
|
|
223
|
+
!Number.isSafeInteger(diagnostic.line) ||
|
|
224
|
+
diagnostic.line < 1 ||
|
|
225
|
+
diagnostic.line > sourceLines ||
|
|
226
|
+
typeof diagnostic.excerpt !== "string" ||
|
|
227
|
+
[...diagnostic.excerpt].length > 203 ||
|
|
228
|
+
findings.has(`${diagnostic.kind}:${diagnostic.line}`)
|
|
229
|
+
)
|
|
230
|
+
fail("NATIVE_REPORT", "Invalid or unknown native diagnostic.");
|
|
231
|
+
findings.add(`${diagnostic.kind}:${diagnostic.line}`);
|
|
232
|
+
if (diagnostic.kind !== "long")
|
|
233
|
+
checked[index].diagnostics.push({
|
|
234
|
+
source: "snapper",
|
|
235
|
+
rule: diagnostic.kind,
|
|
236
|
+
line: diagnostic.line,
|
|
237
|
+
column: 1,
|
|
238
|
+
message: `Sentence layout: ${diagnostic.kind}.`,
|
|
239
|
+
severity: "error",
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
checked[index].wouldReformat = report.would_reformat;
|
|
243
|
+
}
|
|
244
|
+
if (result.status !== Number(checked.some((item) => item.wouldReformat)))
|
|
245
|
+
fail("NATIVE_REPORT", "Inconsistent native failure report.");
|
|
246
|
+
return checked;
|
|
247
|
+
}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
2
|
+
import {
|
|
3
|
+
chmodSync,
|
|
4
|
+
closeSync,
|
|
5
|
+
fstatSync,
|
|
6
|
+
lstatSync,
|
|
7
|
+
mkdtempSync,
|
|
8
|
+
openSync,
|
|
9
|
+
readFileSync,
|
|
10
|
+
readdirSync,
|
|
11
|
+
realpathSync,
|
|
12
|
+
rmdirSync,
|
|
13
|
+
unlinkSync,
|
|
14
|
+
writeFileSync,
|
|
15
|
+
} from "node:fs";
|
|
16
|
+
import { tmpdir } from "node:os";
|
|
17
|
+
import { dirname, isAbsolute, join, relative, sep } from "node:path";
|
|
18
|
+
import { spawnSync } from "node:child_process";
|
|
19
|
+
import { fileURLToPath } from "node:url";
|
|
20
|
+
import { fail, limits } from "./contracts.js";
|
|
21
|
+
|
|
22
|
+
export const nativeCheckConfig =
|
|
23
|
+
'format = "markdown"\nmax_width = 0\nclause_breaks = false\nlong_threshold = 2097153\n';
|
|
24
|
+
const editorConfig = "root = true\n\n[*]\nmax_line_length = off\n";
|
|
25
|
+
const configFiles = new Map([
|
|
26
|
+
["snapper-check.toml", nativeCheckConfig],
|
|
27
|
+
[".editorconfig", editorConfig],
|
|
28
|
+
]);
|
|
29
|
+
const inputName = /^input-(?:[0-9]|[12][0-9]|3[01])\.md$/u;
|
|
30
|
+
|
|
31
|
+
function identity(stat) {
|
|
32
|
+
return { dev: stat.dev, ino: stat.ino, birthtimeNs: stat.birthtimeNs };
|
|
33
|
+
}
|
|
34
|
+
function same(stat, expected) {
|
|
35
|
+
return Object.keys(expected).every((key) => stat[key] === expected[key]);
|
|
36
|
+
}
|
|
37
|
+
function contains(root, path) {
|
|
38
|
+
const part = relative(root, path);
|
|
39
|
+
return (
|
|
40
|
+
part === "" ||
|
|
41
|
+
(!isAbsolute(part) && part !== ".." && !part.startsWith(".." + sep))
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Establish a private external capability, or preserve the stdin path if unavailable. */
|
|
46
|
+
export function createNativeStaging(consumerRoot) {
|
|
47
|
+
let token;
|
|
48
|
+
try {
|
|
49
|
+
const parent = realpathSync(tmpdir());
|
|
50
|
+
// Never create even an empty staging directory inside the consumer checkout.
|
|
51
|
+
if (
|
|
52
|
+
contains(consumerRoot, parent) ||
|
|
53
|
+
(process.platform === "win32" && parent.startsWith("\\\\"))
|
|
54
|
+
)
|
|
55
|
+
return null;
|
|
56
|
+
const parentStat = lstatSync(parent, { bigint: true });
|
|
57
|
+
if (
|
|
58
|
+
process.platform !== "win32" &&
|
|
59
|
+
parentStat.mode & 0o022n &&
|
|
60
|
+
!(parentStat.mode & 0o1000n)
|
|
61
|
+
)
|
|
62
|
+
return null;
|
|
63
|
+
const path = mkdtempSync(join(parent, "markdown-quality-native-"));
|
|
64
|
+
token = { path, identity: identity(lstatSync(path, { bigint: true })) };
|
|
65
|
+
if (process.platform === "win32") restrictWindowsAccess(path);
|
|
66
|
+
else chmodSync(path, 0o700);
|
|
67
|
+
verifyNativeStaging(token, false);
|
|
68
|
+
for (const [name, text] of configFiles) writeStagedFile(token, name, text);
|
|
69
|
+
verifyNativeStaging(token);
|
|
70
|
+
let closed = false;
|
|
71
|
+
return {
|
|
72
|
+
token,
|
|
73
|
+
close() {
|
|
74
|
+
if (closed) return;
|
|
75
|
+
closed = true;
|
|
76
|
+
closeNativeStaging(token);
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
} catch {
|
|
80
|
+
// No private document payload is written during capability establishment.
|
|
81
|
+
if (token) closeNativeStaging(token);
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function restrictWindowsAccess(path) {
|
|
87
|
+
const deadline = performance.now() + 1000;
|
|
88
|
+
const systemRoot = process.env.SystemRoot ?? process.env.WINDIR;
|
|
89
|
+
if (!systemRoot || !isAbsolute(systemRoot))
|
|
90
|
+
throw Error("No system directory.");
|
|
91
|
+
const system = join(systemRoot, "System32");
|
|
92
|
+
const options = {
|
|
93
|
+
cwd: fileURLToPath(new URL("../assets/", import.meta.url)),
|
|
94
|
+
env: { SystemRoot: systemRoot, WINDIR: systemRoot },
|
|
95
|
+
shell: false,
|
|
96
|
+
windowsHide: true,
|
|
97
|
+
maxBuffer: 65536,
|
|
98
|
+
};
|
|
99
|
+
function run(command, args) {
|
|
100
|
+
const remaining = Math.floor(deadline - performance.now());
|
|
101
|
+
if (remaining <= 0) throw Error("Private access deadline exceeded.");
|
|
102
|
+
return spawnSync(command, args, { ...options, timeout: remaining });
|
|
103
|
+
}
|
|
104
|
+
const user = run(join(system, "whoami.exe"), ["/user", "/fo", "csv", "/nh"]);
|
|
105
|
+
if (user.error || user.signal || user.status !== 0)
|
|
106
|
+
throw Error("Identity unavailable.");
|
|
107
|
+
const sid = /"(S-1-(?:\d+-)*\d+)"\s*$/u.exec(
|
|
108
|
+
user.stdout.toString("utf8").trim(),
|
|
109
|
+
)?.[1];
|
|
110
|
+
if (!sid) throw Error("Identity unavailable.");
|
|
111
|
+
// Reset an empty owned directory first, then remove inherited entries and grant
|
|
112
|
+
// only the current SID. No consumer ACL or existing directory is modified.
|
|
113
|
+
for (const args of [
|
|
114
|
+
[path, "/reset"],
|
|
115
|
+
[path, "/inheritance:r", "/grant:r", `*${sid}:(OI)(CI)F`],
|
|
116
|
+
]) {
|
|
117
|
+
const result = run(join(system, "icacls.exe"), args);
|
|
118
|
+
if (result.error || result.signal || result.status !== 0)
|
|
119
|
+
throw Error("Private access unavailable.");
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Validate the originally created directory and immutable configuration boundary. */
|
|
124
|
+
export function verifyNativeStaging(token, checkConfiguration = true) {
|
|
125
|
+
try {
|
|
126
|
+
const stat = lstatSync(token.path, { bigint: true });
|
|
127
|
+
if (
|
|
128
|
+
!stat.isDirectory() ||
|
|
129
|
+
stat.isSymbolicLink() ||
|
|
130
|
+
!same(stat, token.identity) ||
|
|
131
|
+
realpathSync(token.path) !== token.path
|
|
132
|
+
)
|
|
133
|
+
throw Error("Changed staging root.");
|
|
134
|
+
if (
|
|
135
|
+
process.platform !== "win32" &&
|
|
136
|
+
((stat.mode & 0o077n) !== 0n || stat.uid !== BigInt(process.getuid()))
|
|
137
|
+
)
|
|
138
|
+
throw Error("Staging access is not private.");
|
|
139
|
+
if (checkConfiguration)
|
|
140
|
+
for (const [name, text] of configFiles) {
|
|
141
|
+
const path = join(token.path, name);
|
|
142
|
+
const file = lstatSync(path, { bigint: true });
|
|
143
|
+
if (
|
|
144
|
+
!file.isFile() ||
|
|
145
|
+
file.isSymbolicLink() ||
|
|
146
|
+
file.nlink !== 1n ||
|
|
147
|
+
file.size !== BigInt(Buffer.byteLength(text)) ||
|
|
148
|
+
readFileSync(path, "utf8") !== text
|
|
149
|
+
)
|
|
150
|
+
throw Error("Changed staging configuration.");
|
|
151
|
+
}
|
|
152
|
+
} catch {
|
|
153
|
+
fail("NATIVE_STAGING", "Private native staging is unavailable or changed.");
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** Write one package-selected exclusive file; its caller owns the returned identity. */
|
|
158
|
+
export function writeStagedFile(token, name, text) {
|
|
159
|
+
verifyNativeStaging(token, false);
|
|
160
|
+
if (!inputName.test(name) && !configFiles.has(name))
|
|
161
|
+
fail("NATIVE_STAGING", "Invalid native staging input.");
|
|
162
|
+
const path = join(token.path, name);
|
|
163
|
+
let descriptor;
|
|
164
|
+
try {
|
|
165
|
+
descriptor = openSync(path, "wx", 0o600);
|
|
166
|
+
writeFileSync(descriptor, text, "utf8");
|
|
167
|
+
return {
|
|
168
|
+
path,
|
|
169
|
+
identity: identity(fstatSync(descriptor, { bigint: true })),
|
|
170
|
+
};
|
|
171
|
+
} catch {
|
|
172
|
+
fail("NATIVE_STAGING", "Cannot prepare private native input.");
|
|
173
|
+
} finally {
|
|
174
|
+
if (descriptor !== undefined) closeSync(descriptor);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** Remove only regular single-link files in the still-owned flat directory. */
|
|
179
|
+
export function removeStagedFile(token, file) {
|
|
180
|
+
try {
|
|
181
|
+
verifyNativeStaging(token, false);
|
|
182
|
+
if (dirname(file.path) !== token.path) throw Error("Outside staging.");
|
|
183
|
+
const stat = lstatSync(file.path, { bigint: true });
|
|
184
|
+
if (
|
|
185
|
+
!stat.isFile() ||
|
|
186
|
+
stat.isSymbolicLink() ||
|
|
187
|
+
stat.nlink !== 1n ||
|
|
188
|
+
!same(stat, file.identity)
|
|
189
|
+
)
|
|
190
|
+
throw Error("Changed staging input.");
|
|
191
|
+
unlinkSync(file.path);
|
|
192
|
+
} catch {
|
|
193
|
+
fail("NATIVE_CLEANUP", "Private native staging cleanup is incomplete.");
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function closeNativeStaging(token) {
|
|
198
|
+
try {
|
|
199
|
+
verifyNativeStaging(token, false);
|
|
200
|
+
const names = readdirSync(token.path);
|
|
201
|
+
if (
|
|
202
|
+
names.length > 34 ||
|
|
203
|
+
names.some((name) => !inputName.test(name) && !configFiles.has(name))
|
|
204
|
+
)
|
|
205
|
+
throw Error("Unexpected staging contents.");
|
|
206
|
+
for (const name of names) {
|
|
207
|
+
const path = join(token.path, name);
|
|
208
|
+
removeStagedFile(token, {
|
|
209
|
+
path,
|
|
210
|
+
identity: identity(lstatSync(path, { bigint: true })),
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
verifyNativeStaging(token, false);
|
|
214
|
+
rmdirSync(token.path);
|
|
215
|
+
} catch {
|
|
216
|
+
fail("NATIVE_CLEANUP", "Private native staging cleanup is incomplete.");
|
|
217
|
+
}
|
|
218
|
+
}
|
package/src/prose-diagnostics.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
// 58a306013d3701f59dfe34341e96fac5a011e3ed. Copyright 2026 Hadden Industries Ltd.
|
|
5
5
|
// Original MIT terms retained in LICENSES/MIT-universal-ontology.txt.
|
|
6
6
|
import { runNative } from "./native-tool.js";
|
|
7
|
+
import { runNativeChecks } from "./native-checks.js";
|
|
7
8
|
// Requalified against 0.11.9. Owner approval: this chat, 2026-10-05.
|
|
8
9
|
// Mirrors OwlAPI's narrow container boundary and continuation recheck approach.
|
|
9
10
|
function listItem(line = "") {
|
|
@@ -56,3 +57,121 @@ export function checkProse(tool, text) {
|
|
|
56
57
|
return runNative(tool, prose.join("\n") + "\n", true).length !== 0;
|
|
57
58
|
});
|
|
58
59
|
}
|
|
60
|
+
|
|
61
|
+
/** Recheck complete independent list items in bounded groups, retaining finding order. */
|
|
62
|
+
export function checkProseGroup(
|
|
63
|
+
tool,
|
|
64
|
+
texts,
|
|
65
|
+
staging,
|
|
66
|
+
observer,
|
|
67
|
+
admittedChecks,
|
|
68
|
+
) {
|
|
69
|
+
let checkMilliseconds = 0;
|
|
70
|
+
function check(inputs) {
|
|
71
|
+
const owners = inputs.map((input) => input.owner);
|
|
72
|
+
const uniqueOwners = [...new Set(owners)];
|
|
73
|
+
observer?.start(uniqueOwners);
|
|
74
|
+
const started = performance.now();
|
|
75
|
+
let nativeMilliseconds = 0;
|
|
76
|
+
try {
|
|
77
|
+
return runNativeChecks(
|
|
78
|
+
tool,
|
|
79
|
+
inputs.map((input) => input.text),
|
|
80
|
+
staging,
|
|
81
|
+
observer && {
|
|
82
|
+
start: (indices) =>
|
|
83
|
+
observer.start([...new Set(indices.map((index) => owners[index]))]),
|
|
84
|
+
finish: (indices, elapsed) => {
|
|
85
|
+
nativeMilliseconds += elapsed;
|
|
86
|
+
observer.finish(
|
|
87
|
+
[...new Set(indices.map((index) => owners[index]))],
|
|
88
|
+
elapsed,
|
|
89
|
+
);
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
);
|
|
93
|
+
} finally {
|
|
94
|
+
const elapsed = performance.now() - started;
|
|
95
|
+
checkMilliseconds += elapsed;
|
|
96
|
+
observer?.finish(uniqueOwners, Math.max(0, elapsed - nativeMilliseconds));
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
const checked = new Array(texts.length);
|
|
100
|
+
const unchecked = [];
|
|
101
|
+
for (const [owner, text] of texts.entries()) {
|
|
102
|
+
const admitted = admittedChecks?.[owner];
|
|
103
|
+
if (admitted) checked[owner] = admitted;
|
|
104
|
+
else unchecked.push({ text, owner });
|
|
105
|
+
}
|
|
106
|
+
if (unchecked.length) {
|
|
107
|
+
const results = check(unchecked);
|
|
108
|
+
for (const [index, input] of unchecked.entries())
|
|
109
|
+
checked[input.owner] = results[index];
|
|
110
|
+
}
|
|
111
|
+
const keep = checked.map((entry) => entry.diagnostics.map(() => true));
|
|
112
|
+
let pending = [],
|
|
113
|
+
pendingBytes = 0;
|
|
114
|
+
function flush() {
|
|
115
|
+
if (!pending.length) return;
|
|
116
|
+
const items = pending;
|
|
117
|
+
pending = [];
|
|
118
|
+
pendingBytes = 0;
|
|
119
|
+
const results = check(items);
|
|
120
|
+
for (const [index, result] of results.entries())
|
|
121
|
+
keep[items[index].owner][items[index].finding] =
|
|
122
|
+
result.diagnostics.length !== 0;
|
|
123
|
+
}
|
|
124
|
+
for (const [owner, text] of texts.entries()) {
|
|
125
|
+
observer?.start([owner]);
|
|
126
|
+
const started = performance.now(),
|
|
127
|
+
previousChecks = checkMilliseconds;
|
|
128
|
+
try {
|
|
129
|
+
const lines = text.split(/\r?\n/u);
|
|
130
|
+
for (const [finding, diagnostic] of checked[
|
|
131
|
+
owner
|
|
132
|
+
].diagnostics.entries()) {
|
|
133
|
+
const index = diagnostic.line - 1,
|
|
134
|
+
item = listItem(lines[index]);
|
|
135
|
+
if (!item) continue;
|
|
136
|
+
const previous = listItem(lines[index - 1]);
|
|
137
|
+
const quotedNumber =
|
|
138
|
+
diagnostic.rule === "fused" && item.quotes > 0 && item.ordered;
|
|
139
|
+
const adjacent =
|
|
140
|
+
diagnostic.rule === "wrap" &&
|
|
141
|
+
previous &&
|
|
142
|
+
previous.prefix === item.prefix &&
|
|
143
|
+
previous.indent === item.indent &&
|
|
144
|
+
previous.ordered === item.ordered;
|
|
145
|
+
if (!quotedNumber && !adjacent) continue;
|
|
146
|
+
const prose = [item.prose];
|
|
147
|
+
for (let next = index + 1; next < lines.length; next++) {
|
|
148
|
+
const line = lines[next];
|
|
149
|
+
if (!line.startsWith(item.prefix) || listItem(line)) break;
|
|
150
|
+
const content = line.slice(item.prefix.length),
|
|
151
|
+
indent = item.contentOffset - item.prefix.length;
|
|
152
|
+
if (!content.startsWith(" ".repeat(indent))) break;
|
|
153
|
+
prose.push(content.slice(indent));
|
|
154
|
+
}
|
|
155
|
+
const snippet = prose.join("\n") + "\n",
|
|
156
|
+
bytes = Buffer.byteLength(snippet);
|
|
157
|
+
if (pending.length === 32 || pendingBytes + bytes > 4_194_304) flush();
|
|
158
|
+
pending.push({ text: snippet, owner, finding });
|
|
159
|
+
pendingBytes += bytes;
|
|
160
|
+
}
|
|
161
|
+
} finally {
|
|
162
|
+
// Native groups already charge their participants. Attribute owned snippet
|
|
163
|
+
// extraction separately, including time between successive rechecks.
|
|
164
|
+
observer?.finish(
|
|
165
|
+
[owner],
|
|
166
|
+
Math.max(
|
|
167
|
+
0,
|
|
168
|
+
performance.now() - started - (checkMilliseconds - previousChecks),
|
|
169
|
+
),
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
flush();
|
|
174
|
+
return checked.map((entry, owner) =>
|
|
175
|
+
entry.diagnostics.filter((_, index) => keep[owner][index]),
|
|
176
|
+
);
|
|
177
|
+
}
|