@noctcore/harness 0.1.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.
package/dist/cli.js ADDED
@@ -0,0 +1,470 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import { spawnSync as spawnSync2 } from "child_process";
5
+ import { readFileSync as readFileSync2, realpathSync } from "fs";
6
+ import path3 from "path";
7
+ import process from "process";
8
+ import { fileURLToPath } from "url";
9
+
10
+ // src/lint-meta/ctx.ts
11
+ import { spawnSync } from "child_process";
12
+ import { existsSync, globSync, readFileSync } from "fs";
13
+ import path from "path";
14
+ function toPosixRel(rel) {
15
+ return rel.replace(/\\/g, "/");
16
+ }
17
+ function normalizeText(text) {
18
+ return text.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
19
+ }
20
+ var EXEC_MAX_BUFFER = 16 * 1024 * 1024;
21
+ function createNodeCtx(root) {
22
+ return {
23
+ root,
24
+ read(rel) {
25
+ const abs = path.join(root, toPosixRel(rel));
26
+ if (!existsSync(abs)) return null;
27
+ return normalizeText(readFileSync(abs, "utf8"));
28
+ },
29
+ exists(rel) {
30
+ return existsSync(path.join(root, toPosixRel(rel)));
31
+ },
32
+ glob(pattern) {
33
+ return Array.from(globSync(pattern, { cwd: root })).map(toPosixRel);
34
+ },
35
+ exec(cmd) {
36
+ const res = spawnSync(cmd, {
37
+ cwd: root,
38
+ shell: true,
39
+ encoding: "utf8",
40
+ stdio: ["ignore", "pipe", "pipe"],
41
+ maxBuffer: EXEC_MAX_BUFFER
42
+ });
43
+ return {
44
+ code: typeof res.status === "number" ? res.status : 1,
45
+ stdout: res.stdout ?? "",
46
+ stderr: res.stderr ?? ""
47
+ };
48
+ }
49
+ };
50
+ }
51
+
52
+ // src/lint-meta/registry.ts
53
+ import { pathToFileURL } from "url";
54
+ var DEFAULT_REGISTRY_RELATIVE_PATH = ".nightcore/lint-meta/registry.js";
55
+ var defaultImporter = (absPath) => import(pathToFileURL(absPath).href);
56
+ function isMetaRule(v) {
57
+ if (typeof v !== "object" || v === null) return false;
58
+ const r = v;
59
+ return typeof r.id === "string" && typeof r.run === "function";
60
+ }
61
+ function extractRules(mod) {
62
+ if (typeof mod !== "object" || mod === null) return null;
63
+ const m = mod;
64
+ const def = m.default;
65
+ const candidates = [
66
+ m.META_RULES,
67
+ def?.META_RULES,
68
+ m.default
69
+ ];
70
+ for (const candidate of candidates) {
71
+ if (Array.isArray(candidate) && candidate.every(isMetaRule)) return candidate;
72
+ }
73
+ return null;
74
+ }
75
+ async function loadRegistry(absRegistryPath, importer = defaultImporter) {
76
+ let mod;
77
+ try {
78
+ mod = await importer(absRegistryPath);
79
+ } catch (err) {
80
+ return { rules: [], error: err instanceof Error ? err.message : String(err) };
81
+ }
82
+ const rules = extractRules(mod);
83
+ if (rules === null) {
84
+ return {
85
+ rules: [],
86
+ error: "the registry must export `META_RULES` (a named export, or the default) as an array of { id, run } rule objects"
87
+ };
88
+ }
89
+ return { rules };
90
+ }
91
+
92
+ // src/lint-meta/run.ts
93
+ function runMetaRules(rules, ctx, onRule) {
94
+ const outcomes = [];
95
+ for (const rule of rules) {
96
+ onRule?.(rule);
97
+ const ciCritical = rule.ciCritical === true;
98
+ try {
99
+ outcomes.push({ id: rule.id, ciCritical, violations: rule.run(ctx), threw: null });
100
+ } catch (err) {
101
+ outcomes.push({
102
+ id: rule.id,
103
+ ciCritical,
104
+ violations: [],
105
+ threw: err instanceof Error ? err.message : String(err)
106
+ });
107
+ }
108
+ }
109
+ return outcomes;
110
+ }
111
+ function reportMetaOutcomes(outcomes) {
112
+ let criticalCount = 0;
113
+ let totalViolations = 0;
114
+ const lines = [];
115
+ for (const outcome of outcomes) {
116
+ if (outcome.threw !== null) {
117
+ lines.push(`[ERROR] ${outcome.id}: rule threw \u2014 ${outcome.threw}`);
118
+ criticalCount += 1;
119
+ continue;
120
+ }
121
+ for (const v of outcome.violations) {
122
+ totalViolations += 1;
123
+ const tag = outcome.ciCritical ? "ERROR" : "info";
124
+ lines.push(`[${tag}] ${v.rule} (${v.file}): ${v.message}`);
125
+ if (outcome.ciCritical) criticalCount += 1;
126
+ }
127
+ }
128
+ return { criticalCount, totalViolations, lines };
129
+ }
130
+ function exitCodeFor(report) {
131
+ return report.criticalCount > 0 ? 1 : 0;
132
+ }
133
+
134
+ // src/manifest.ts
135
+ import path2 from "path";
136
+ var MANIFEST_RELATIVE_PATH = path2.join(".nightcore", "harness.json");
137
+ var DEFAULT_CHECK_TIMEOUT_MS = 3e5;
138
+ var SUPPORTED_SCHEMA_MAJOR = 1;
139
+ function manifestPath(dir) {
140
+ return path2.join(dir, MANIFEST_RELATIVE_PATH);
141
+ }
142
+ function resolveSchema(raw) {
143
+ if (raw === void 0 || raw === null) return { tooNew: false, found: SUPPORTED_SCHEMA_MAJOR };
144
+ const n = typeof raw === "number" ? raw : Number(raw);
145
+ if (!Number.isFinite(n)) return { tooNew: true, found: Number.NaN };
146
+ const major = Math.floor(n);
147
+ return { tooNew: major > SUPPORTED_SCHEMA_MAJOR, found: major };
148
+ }
149
+ function planCheck(entry) {
150
+ if (typeof entry !== "object" || entry === null) return null;
151
+ const cfg = entry;
152
+ if (typeof cfg.name !== "string" || cfg.name.trim() === "") return null;
153
+ if (cfg.enabled === false) return null;
154
+ const kind = typeof cfg.kind === "string" ? cfg.kind : "";
155
+ if (kind === "shell") return null;
156
+ const command = typeof cfg.command === "string" ? cfg.command.trim() : "";
157
+ if (command === "") return null;
158
+ const tokens = command.split(/\s+/).filter((t) => t.length > 0);
159
+ const program = tokens[0];
160
+ if (program === void 0) return null;
161
+ const args = tokens.slice(1);
162
+ const declared = cfg.timeoutMs;
163
+ const timeoutMs = typeof declared === "number" && Number.isFinite(declared) && declared > 0 ? Math.floor(declared) : DEFAULT_CHECK_TIMEOUT_MS;
164
+ return { name: cfg.name, kind, command, program, args, timeoutMs };
165
+ }
166
+ function loadChecks(dir, read) {
167
+ const raw = read(manifestPath(dir));
168
+ if (raw === null) return { kind: "no-config" };
169
+ let value;
170
+ try {
171
+ value = JSON.parse(raw);
172
+ } catch {
173
+ return { kind: "no-config" };
174
+ }
175
+ if (typeof value !== "object" || value === null) return { kind: "no-config" };
176
+ const root = value;
177
+ const schema = resolveSchema(root.schemaVersion);
178
+ if (schema.tooNew) return { kind: "schema-too-new", found: schema.found };
179
+ const entries = root.checks;
180
+ if (!Array.isArray(entries)) return { kind: "no-config" };
181
+ const checks = [];
182
+ for (const entry of entries) {
183
+ const planned = planCheck(entry);
184
+ if (planned !== null) checks.push(planned);
185
+ }
186
+ return { kind: "ready", checks };
187
+ }
188
+
189
+ // src/run.ts
190
+ var TAIL_LIMIT = 4e3;
191
+ function tailOutput(stdout, stderr) {
192
+ let combined = stdout;
193
+ if (stderr.length > 0) combined += `
194
+ ${stderr}`;
195
+ if (combined.length > TAIL_LIMIT) {
196
+ return `\u2026${combined.slice(combined.length - TAIL_LIMIT)}`;
197
+ }
198
+ return combined;
199
+ }
200
+ function timeoutMessage(timeoutMs) {
201
+ return `timed out after ${timeoutMs}ms (the check was killed; it may hang or need a higher timeoutMs)`;
202
+ }
203
+ function interpret(res, timeoutMs) {
204
+ const timedOut = res.error?.code === "ETIMEDOUT" || res.signal != null;
205
+ if (timedOut) {
206
+ return { status: "failed", output: timeoutMessage(timeoutMs) };
207
+ }
208
+ if (res.error) {
209
+ const detail = res.error.message ?? res.error.code ?? "unknown error";
210
+ return { status: "failed", output: `failed to launch: ${detail}` };
211
+ }
212
+ if (res.status === 0) {
213
+ return { status: "passed", exitCode: 0 };
214
+ }
215
+ return {
216
+ status: "failed",
217
+ exitCode: res.status ?? void 0,
218
+ output: tailOutput(res.stdout, res.stderr)
219
+ };
220
+ }
221
+ function emptyPass() {
222
+ return { passed: true, checks: [] };
223
+ }
224
+ function runChecks(planned, runDir, spawn, onCommand) {
225
+ if (planned.length === 0) return emptyPass();
226
+ const checks = [];
227
+ let failedCheck;
228
+ for (const check of planned) {
229
+ onCommand?.(check);
230
+ const start = Date.now();
231
+ const res = spawn(check.program, check.args, {
232
+ cwd: runDir,
233
+ timeoutMs: check.timeoutMs
234
+ });
235
+ const durationMs = Date.now() - start;
236
+ const outcome = interpret(res, check.timeoutMs);
237
+ if (outcome.status === "failed" && failedCheck === void 0) {
238
+ failedCheck = check.name;
239
+ }
240
+ checks.push({
241
+ name: check.name,
242
+ kind: check.kind,
243
+ command: check.command,
244
+ status: outcome.status,
245
+ exitCode: outcome.exitCode,
246
+ output: outcome.output,
247
+ durationMs
248
+ });
249
+ }
250
+ const result = { passed: failedCheck === void 0, checks };
251
+ if (failedCheck !== void 0) result.failedCheck = failedCheck;
252
+ return result;
253
+ }
254
+ function fixInstruction(result) {
255
+ const failed = result.checks.filter((c) => c.status === "failed");
256
+ if (failed.length === 0) {
257
+ return "The Structure-Lock check failed. Fix the project's harness checks before this work can be verified or merged.";
258
+ }
259
+ let out = `The Structure-Lock check failed: ${failed.length} project harness check${failed.length === 1 ? "" : "s"} did not pass. They MUST all pass before this work can be verified or merged. Re-run each one locally and fix every violation it reports:`;
260
+ for (const c of failed) {
261
+ out += `
262
+
263
+ --- \`${c.name}\` ---
264
+ Command: ${c.command}
265
+
266
+ Output:
267
+ ${c.output ?? "(no output captured)"}`;
268
+ }
269
+ return out;
270
+ }
271
+
272
+ // src/cli.ts
273
+ var HELP = `@noctcore/harness \u2014 portable Structure-Lock runner
274
+
275
+ Usage:
276
+ harness [check] [options] Run the checks declared in .nightcore/harness.json
277
+ harness lint-meta [options] Run the portable lint-meta rules from the enumerated registry
278
+
279
+ Options:
280
+ --dir <path> Target directory to operate in (default: current directory)
281
+ --registry <path> lint-meta only: the rule registry to load
282
+ (default: ${DEFAULT_REGISTRY_RELATIVE_PATH}, relative to --dir)
283
+ --json check only: emit the machine-readable result to stdout instead of a summary
284
+ --version Print the runner version and exit
285
+ --help Print this help and exit
286
+
287
+ Exit codes:
288
+ 0 every check/rule passed (or nothing is configured to enforce)
289
+ 1 a check failed, a rule reported a critical violation or threw, or the manifest requires a newer runner
290
+ 2 a usage error`;
291
+ function parseArgs(argv, cwd) {
292
+ let command = "check";
293
+ let sawCommand = false;
294
+ let dir = cwd;
295
+ let registry;
296
+ let json = false;
297
+ let help = false;
298
+ let version = false;
299
+ for (let i = 0; i < argv.length; i += 1) {
300
+ const arg = argv[i];
301
+ if (arg === void 0) continue;
302
+ if (arg === "--json") {
303
+ json = true;
304
+ } else if (arg === "--help" || arg === "-h") {
305
+ help = true;
306
+ } else if (arg === "--version" || arg === "-v") {
307
+ version = true;
308
+ } else if (arg === "--dir") {
309
+ const next = argv[i + 1];
310
+ if (next !== void 0) {
311
+ dir = next;
312
+ i += 1;
313
+ }
314
+ } else if (arg.startsWith("--dir=")) {
315
+ dir = arg.slice("--dir=".length);
316
+ } else if (arg === "--registry") {
317
+ const next = argv[i + 1];
318
+ if (next !== void 0) {
319
+ registry = next;
320
+ i += 1;
321
+ }
322
+ } else if (arg.startsWith("--registry=")) {
323
+ registry = arg.slice("--registry=".length);
324
+ } else if (!arg.startsWith("-") && !sawCommand) {
325
+ command = arg;
326
+ sawCommand = true;
327
+ }
328
+ }
329
+ return { command, dir: path3.resolve(cwd, dir), registry, json, help, version };
330
+ }
331
+ function readVersion() {
332
+ try {
333
+ const raw = readFileSync2(new URL("../package.json", import.meta.url), "utf8");
334
+ const pkg = JSON.parse(raw);
335
+ return typeof pkg.version === "string" ? pkg.version : "0.0.0";
336
+ } catch {
337
+ return "0.0.0";
338
+ }
339
+ }
340
+ function nodeIO() {
341
+ return {
342
+ cwd: process.cwd(),
343
+ read(absolutePath) {
344
+ try {
345
+ return readFileSync2(absolutePath, "utf8");
346
+ } catch {
347
+ return null;
348
+ }
349
+ },
350
+ spawn(program, args, options) {
351
+ const res = spawnSync2(program, args, {
352
+ cwd: options.cwd,
353
+ timeout: options.timeoutMs,
354
+ killSignal: "SIGKILL",
355
+ encoding: "utf8",
356
+ stdio: ["ignore", "pipe", "pipe"],
357
+ maxBuffer: 16 * 1024 * 1024
358
+ });
359
+ const err = res.error;
360
+ return {
361
+ status: res.status,
362
+ signal: res.signal,
363
+ stdout: res.stdout ?? "",
364
+ stderr: res.stderr ?? "",
365
+ error: err ? { code: err.code, message: err.message } : void 0
366
+ };
367
+ },
368
+ stdout: (line) => process.stdout.write(`${line}
369
+ `),
370
+ stderr: (line) => process.stderr.write(`${line}
371
+ `),
372
+ importModule: defaultImporter
373
+ };
374
+ }
375
+ function runCheck(parsed, io) {
376
+ const outcome = loadChecks(parsed.dir, io.read);
377
+ if (outcome.kind === "no-config") {
378
+ if (parsed.json) {
379
+ io.stdout(JSON.stringify(emptyPass()));
380
+ } else {
381
+ io.stdout("No structure lock configured \u2014 nothing to enforce.");
382
+ }
383
+ return 0;
384
+ }
385
+ if (outcome.kind === "schema-too-new") {
386
+ const label = Number.isFinite(outcome.found) ? String(outcome.found) : "an unrecognized value";
387
+ io.stderr(
388
+ `This .nightcore/harness.json declares schemaVersion ${label}, which this runner does not understand. This bundle was authored by a newer Nightcore \u2014 upgrade @noctcore/harness.`
389
+ );
390
+ return 1;
391
+ }
392
+ const result = runChecks(outcome.checks, parsed.dir, io.spawn, (check) => {
393
+ const line = `\u2192 ${check.name}: ${check.command}`;
394
+ if (parsed.json) io.stderr(line);
395
+ else io.stdout(line);
396
+ });
397
+ if (parsed.json) {
398
+ io.stdout(JSON.stringify(result));
399
+ return result.passed ? 0 : 1;
400
+ }
401
+ for (const check of result.checks) {
402
+ const mark = check.status === "passed" ? "\u2713" : "\u2717";
403
+ const exit = check.exitCode != null ? ` (exit ${check.exitCode})` : "";
404
+ io.stdout(`${mark} ${check.name}${exit}`);
405
+ }
406
+ if (result.passed) {
407
+ const n = result.checks.length;
408
+ io.stdout(`
409
+ Structure lock passed (${n} check${n === 1 ? "" : "s"}).`);
410
+ return 0;
411
+ }
412
+ io.stderr(`
413
+ ${fixInstruction(result)}`);
414
+ return 1;
415
+ }
416
+ async function runLintMeta(parsed, io) {
417
+ const registryPath = parsed.registry ? path3.resolve(parsed.dir, parsed.registry) : path3.join(parsed.dir, DEFAULT_REGISTRY_RELATIVE_PATH);
418
+ if (io.read(registryPath) === null) {
419
+ io.stdout(
420
+ `No lint-meta registry at ${registryPath} \u2014 nothing to enforce. (Point --registry at your rule registry, or commit one at ${DEFAULT_REGISTRY_RELATIVE_PATH}.)`
421
+ );
422
+ return 0;
423
+ }
424
+ const loaded = await loadRegistry(registryPath, io.importModule ?? defaultImporter);
425
+ if (loaded.error !== void 0) {
426
+ io.stderr(`Failed to load the lint-meta registry at ${registryPath}: ${loaded.error}`);
427
+ return 1;
428
+ }
429
+ const n = loaded.rules.length;
430
+ io.stdout(`lint-meta: running ${n} rule${n === 1 ? "" : "s"} from ${registryPath}`);
431
+ const ctx = createNodeCtx(parsed.dir);
432
+ const outcomes = runMetaRules(loaded.rules, ctx, (rule) => io.stdout(`\u2192 ${rule.id}`));
433
+ const report = reportMetaOutcomes(outcomes);
434
+ for (const line of report.lines) io.stderr(line);
435
+ if (report.lines.length === 0) io.stdout("lint-meta: no violations");
436
+ return exitCodeFor(report);
437
+ }
438
+ function runCli(argv, io = nodeIO()) {
439
+ const parsed = parseArgs(argv, io.cwd);
440
+ if (parsed.help) {
441
+ io.stdout(HELP);
442
+ return 0;
443
+ }
444
+ if (parsed.version) {
445
+ io.stdout(readVersion());
446
+ return 0;
447
+ }
448
+ if (parsed.command === "check") return runCheck(parsed, io);
449
+ if (parsed.command === "lint-meta") return runLintMeta(parsed, io);
450
+ io.stderr(`Unknown command: ${parsed.command}. Run \`harness --help\`.`);
451
+ return 2;
452
+ }
453
+ function isMainModule() {
454
+ try {
455
+ const entry = process.argv[1];
456
+ if (entry === void 0) return false;
457
+ return realpathSync(entry) === realpathSync(fileURLToPath(import.meta.url));
458
+ } catch {
459
+ return false;
460
+ }
461
+ }
462
+ if (isMainModule()) {
463
+ void Promise.resolve(runCli(process.argv.slice(2))).then((code) => {
464
+ process.exit(code);
465
+ });
466
+ }
467
+ export {
468
+ nodeIO,
469
+ runCli
470
+ };
package/dist/index.cjs ADDED
@@ -0,0 +1,64 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ DEFAULT_BASELINE_DIR: () => DEFAULT_BASELINE_DIR,
24
+ isGrandfathered: () => isGrandfathered,
25
+ loadBaseline: () => loadBaseline,
26
+ serializeBaseline: () => serializeBaseline
27
+ });
28
+ module.exports = __toCommonJS(index_exports);
29
+
30
+ // src/lint-meta/baseline.ts
31
+ var DEFAULT_BASELINE_DIR = ".nightcore/lint-meta/baselines";
32
+ function loadBaseline(ctx, ruleId, baselineDir = DEFAULT_BASELINE_DIR) {
33
+ const raw = ctx.read(`${baselineDir}/${ruleId}.json`);
34
+ if (raw === null) return {};
35
+ try {
36
+ const parsed = JSON.parse(raw);
37
+ return isNumberMap(parsed) ? parsed : {};
38
+ } catch {
39
+ return {};
40
+ }
41
+ }
42
+ function isGrandfathered(baseline, key, current) {
43
+ const frozen = baseline[key];
44
+ return frozen !== void 0 && current <= frozen;
45
+ }
46
+ function serializeBaseline(map) {
47
+ const sorted = {};
48
+ for (const key of Object.keys(map).sort()) {
49
+ const value = map[key];
50
+ if (value !== void 0) sorted[key] = value;
51
+ }
52
+ return `${JSON.stringify(sorted, null, 2)}
53
+ `;
54
+ }
55
+ function isNumberMap(v) {
56
+ return typeof v === "object" && v !== null && Object.values(v).every((n) => typeof n === "number");
57
+ }
58
+ // Annotate the CommonJS export names for ESM import in node:
59
+ 0 && (module.exports = {
60
+ DEFAULT_BASELINE_DIR,
61
+ isGrandfathered,
62
+ loadBaseline,
63
+ serializeBaseline
64
+ });
@@ -0,0 +1,136 @@
1
+ export { C as CheckStatus, M as ManifestOutcome, P as PlannedCheck, a as StructureLockCheck, b as StructureLockResult } from './run-B6_KI2bV.cjs';
2
+
3
+ /**
4
+ * The PORTABLE lint-meta contract — the types a target repo's generated
5
+ * `lint-meta-rule` artifacts implement and import from `@noctcore/harness`.
6
+ *
7
+ * This is an owned COPY of the Nightcore-internal `tools/lint-meta/types.ts`
8
+ * (the engine that enforces what ESLint cannot reach: cross-file contracts,
9
+ * non-JS files, config parity). It is reproduced here, not imported, so the
10
+ * published package carries the whole contract with ZERO runtime dependency on
11
+ * the monorepo — a generated rule in a stranger's repo does
12
+ * `import type { IMetaRule } from '@noctcore/harness'` and nothing else.
13
+ *
14
+ * Rules are pure functions of an {@link IMetaCtx} and return {@link IViolation}s;
15
+ * the runner exits non-zero when any `ciCritical` rule reports one (or throws).
16
+ */
17
+ interface IMetaCtx {
18
+ /** Absolute repo root the rule reads relative to. */
19
+ readonly root: string;
20
+ /** Read a repo-relative file (LF-normalized), or `null` if it does not exist. */
21
+ read(rel: string): string | null;
22
+ /** Whether a repo-relative path exists. */
23
+ exists(rel: string): boolean;
24
+ /** Glob repo-relative paths (cwd = {@link root}). */
25
+ glob(pattern: string): string[];
26
+ /** Run a shell command at {@link root}; never throws. */
27
+ exec(cmd: string): {
28
+ code: number;
29
+ stdout: string;
30
+ stderr: string;
31
+ };
32
+ }
33
+ interface IViolation {
34
+ file: string;
35
+ rule: string;
36
+ message: string;
37
+ /**
38
+ * 1-indexed source location, when the rule can pinpoint one. Optional and
39
+ * additive: the text reporter ignores it (it surfaces `file`/`rule`/`message`
40
+ * only), so a rule may omit it.
41
+ */
42
+ line?: number;
43
+ column?: number;
44
+ }
45
+ interface IMetaRule {
46
+ id: string;
47
+ category: 'config' | 'source-text' | 'supply-chain' | 'ci' | 'testing';
48
+ description: string;
49
+ /** When true, a violation fails CI (the runner exits non-zero). */
50
+ ciCritical?: boolean;
51
+ run(ctx: IMetaCtx): IViolation[];
52
+ /**
53
+ * Ratcheting rules implement this to snapshot the CURRENT offenders as a frozen
54
+ * baseline (a flat `metric-key → number` map). A rule's `run` then grandfathers
55
+ * any offender still within its recorded value (see `baseline.ts`). Omit for
56
+ * strict rules.
57
+ */
58
+ baseline?(ctx: IMetaCtx): Record<string, number>;
59
+ }
60
+
61
+ /**
62
+ * The generic lint-meta ratchet — a faithful port of `tools/lint-meta/baseline.ts`.
63
+ *
64
+ * A baseline is a committed `<baselineDir>/<rule-id>.json` — a flat `key → number`
65
+ * map freezing today's offenders at their current metric. The ratchet is one-way:
66
+ * an offender that is recorded AND has not grown past its frozen value is
67
+ * grandfathered (suppressed); a NEW offender, or a recorded one that GREW, is a
68
+ * live violation. As each offender is fixed, its entry is deleted (or lowered),
69
+ * never raised — the frozen debt only shrinks.
70
+ *
71
+ * The `key` is opaque to this module: a rule with more than one metric family
72
+ * namespaces its keys (`size:<file>`, `manifest:<file>`) so one flat map serves both.
73
+ *
74
+ * The only portability change from the internal engine is the baseline HOME: the
75
+ * Nightcore engine hardcodes `tools/lint-meta/baselines/`, whereas a portable rule
76
+ * ships its baselines under {@link DEFAULT_BASELINE_DIR} (overridable per call).
77
+ */
78
+
79
+ /** Where a portable rule's committed baselines live, relative to the repo root. */
80
+ declare const DEFAULT_BASELINE_DIR = ".nightcore/lint-meta/baselines";
81
+ /** Load a rule's committed baseline, or `{}` when none exists yet. */
82
+ declare function loadBaseline(ctx: IMetaCtx, ruleId: string, baselineDir?: string): Record<string, number>;
83
+ /**
84
+ * Whether `current` for `key` is grandfathered by `baseline`: recorded AND not
85
+ * grown past the frozen value. A key absent from the baseline is never
86
+ * grandfathered (a new offender always fails); a recorded key whose current value
87
+ * is `<=` its record passes (the ratchet permits staying same-or-shrinking).
88
+ */
89
+ declare function isGrandfathered(baseline: Record<string, number>, key: string, current: number): boolean;
90
+ /** Serialize a baseline map with sorted keys for a stable, diff-friendly file. */
91
+ declare function serializeBaseline(map: Record<string, number>): string;
92
+
93
+ /**
94
+ * Public type surface of `@noctcore/harness`. Exposes the
95
+ * `.nightcore/harness.json` manifest shape, the `StructureLockResult`-shaped
96
+ * verdict the runner emits (camelCase, wire-compatible with the Rust
97
+ * `StructureLockResult` / `StructureLockCheck`), AND the portable lint-meta
98
+ * contract (`IMetaRule` / `IMetaCtx` / `IViolation`) a target repo's generated
99
+ * `lint-meta-rule` artifacts implement.
100
+ *
101
+ * This barrel exists so generated artifacts and downstream tooling can
102
+ * `import type` from the published package.
103
+ */
104
+
105
+ /** One check as declared in `.nightcore/harness.json`. */
106
+ interface HarnessManifestCheck {
107
+ /** The logical check name (e.g. `folder-per-component`). */
108
+ name: string;
109
+ /** The harness kind (e.g. `lint-plugin`, `ast-grep`). Free-form on the wire. */
110
+ kind: string;
111
+ /** The exact command line to run (e.g. `npx eslint .`). */
112
+ command?: string;
113
+ /** Optional config path for the underlying tool (informational). */
114
+ configPath?: string;
115
+ /** Per-check wall-clock timeout in milliseconds (`> 0`, else the default). */
116
+ timeoutMs?: number;
117
+ /** Whether the check participates in the gate. Defaults to `true`. */
118
+ enabled?: boolean;
119
+ }
120
+ /** The `.nightcore/harness.json` manifest the runner reads. */
121
+ interface HarnessManifest {
122
+ /**
123
+ * The manifest MAJOR schema version. Absent ⇒ treated as `1`. A higher MAJOR
124
+ * than the runner supports reds the build (upgrade the runner).
125
+ */
126
+ schemaVersion?: number;
127
+ /** The checks the runner enforces. Absent/empty ⇒ nothing to enforce. */
128
+ checks?: HarnessManifestCheck[];
129
+ /**
130
+ * The Nightcore agent-runtime policy block. Carried for Nightcore-driven
131
+ * consumers; the CI runner does not interpret it.
132
+ */
133
+ policy?: Record<string, unknown>;
134
+ }
135
+
136
+ export { DEFAULT_BASELINE_DIR, type HarnessManifest, type HarnessManifestCheck, type IMetaCtx, type IMetaRule, type IViolation, isGrandfathered, loadBaseline, serializeBaseline };