@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.
@@ -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.js';
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 };
package/dist/index.js ADDED
@@ -0,0 +1,34 @@
1
+ // src/lint-meta/baseline.ts
2
+ var DEFAULT_BASELINE_DIR = ".nightcore/lint-meta/baselines";
3
+ function loadBaseline(ctx, ruleId, baselineDir = DEFAULT_BASELINE_DIR) {
4
+ const raw = ctx.read(`${baselineDir}/${ruleId}.json`);
5
+ if (raw === null) return {};
6
+ try {
7
+ const parsed = JSON.parse(raw);
8
+ return isNumberMap(parsed) ? parsed : {};
9
+ } catch {
10
+ return {};
11
+ }
12
+ }
13
+ function isGrandfathered(baseline, key, current) {
14
+ const frozen = baseline[key];
15
+ return frozen !== void 0 && current <= frozen;
16
+ }
17
+ function serializeBaseline(map) {
18
+ const sorted = {};
19
+ for (const key of Object.keys(map).sort()) {
20
+ const value = map[key];
21
+ if (value !== void 0) sorted[key] = value;
22
+ }
23
+ return `${JSON.stringify(sorted, null, 2)}
24
+ `;
25
+ }
26
+ function isNumberMap(v) {
27
+ return typeof v === "object" && v !== null && Object.values(v).every((n) => typeof n === "number");
28
+ }
29
+ export {
30
+ DEFAULT_BASELINE_DIR,
31
+ isGrandfathered,
32
+ loadBaseline,
33
+ serializeBaseline
34
+ };
@@ -0,0 +1,93 @@
1
+ /** A check resolved into the program + args to spawn. */
2
+ interface PlannedCheck {
3
+ name: string;
4
+ /** The manifest `kind` wire string, carried through verbatim to the result. */
5
+ kind: string;
6
+ /** The exact command line, retained for the result + fix instruction. */
7
+ command: string;
8
+ program: string;
9
+ args: string[];
10
+ /** The resolved per-check wall-clock timeout in milliseconds. */
11
+ timeoutMs: number;
12
+ }
13
+ /**
14
+ * The result of loading a manifest. `no-config` (exit 0, opt-in-by-presence),
15
+ * `schema-too-new` (exit non-zero, upgrade the runner), or `ready` with the
16
+ * planned checks (possibly empty ⇒ trivially passing).
17
+ */
18
+ type ManifestOutcome = {
19
+ kind: 'no-config';
20
+ } | {
21
+ kind: 'schema-too-new';
22
+ found: number;
23
+ } | {
24
+ kind: 'ready';
25
+ checks: PlannedCheck[];
26
+ };
27
+ /** Reads an absolute path, returning its contents or `null` if unreadable. */
28
+ type FileReader = (absolutePath: string) => string | null;
29
+
30
+ /**
31
+ * The runner + reporting — a faithful Node port of the in-process Rust runner
32
+ * (`workflow/gauntlet_project/runner.rs`), minus the Nightcore-orchestration-only
33
+ * machinery (retry-once/flaky, the security-critical no-retry exclusion, drift
34
+ * substrates, and the task verify-command append — none of which has a CI
35
+ * meaning). Statuses are therefore only `passed` / `failed`.
36
+ *
37
+ * FULL-RUN semantics (matching the live Rust runner, NOT the spec's stale
38
+ * stop-at-first text): every enabled check runs and records its own outcome, so
39
+ * a human reading CI sees the WHOLE failure set at once instead of one failure
40
+ * per pushed round. Each check is wall-clock BOUNDED by its `timeoutMs`; a
41
+ * timeout is a FAILED check (fail-closed — never a silent pass). `passed` is
42
+ * false iff ANY check failed; `failedCheck` names the FIRST failed check
43
+ * (back-compat).
44
+ *
45
+ * Pure over an injected {@link SpawnFn} so it is unit-testable without spawning.
46
+ */
47
+
48
+ /** A check outcome. `skipped`/`flaky` from the in-process runner are dropped. */
49
+ type CheckStatus = 'passed' | 'failed';
50
+ /**
51
+ * The outcome of one structure-lock check. Wire-compatible (camelCase) with the
52
+ * Rust `StructureLockCheck` — `exitCode` / `output` / `durationMs` are omitted
53
+ * when absent.
54
+ */
55
+ interface StructureLockCheck {
56
+ name: string;
57
+ kind: string;
58
+ command: string;
59
+ status: CheckStatus;
60
+ exitCode?: number;
61
+ output?: string;
62
+ durationMs?: number;
63
+ }
64
+ /**
65
+ * The structured verdict. Wire-compatible (camelCase) with the Rust
66
+ * `StructureLockResult` — `failedCheck` is omitted when everything passed.
67
+ */
68
+ interface StructureLockResult {
69
+ passed: boolean;
70
+ checks: StructureLockCheck[];
71
+ failedCheck?: string;
72
+ }
73
+ /** The normalized result of one spawn — a subset of Node's `spawnSync` return. */
74
+ interface SpawnResult {
75
+ /** Process exit code, or `null` when the process did not exit normally. */
76
+ status: number | null;
77
+ /** The signal that terminated the process, if any (e.g. a timeout kill). */
78
+ signal: string | null;
79
+ stdout: string;
80
+ stderr: string;
81
+ /** Set when the process could not be launched or was timed out. */
82
+ error?: {
83
+ code?: string;
84
+ message?: string;
85
+ };
86
+ }
87
+ /** Runs `program args` in `cwd`, bounded by `timeoutMs`. Must never throw. */
88
+ type SpawnFn = (program: string, args: string[], options: {
89
+ cwd: string;
90
+ timeoutMs: number;
91
+ }) => SpawnResult;
92
+
93
+ export type { CheckStatus as C, FileReader as F, ManifestOutcome as M, PlannedCheck as P, SpawnFn as S, StructureLockCheck as a, StructureLockResult as b };
@@ -0,0 +1,93 @@
1
+ /** A check resolved into the program + args to spawn. */
2
+ interface PlannedCheck {
3
+ name: string;
4
+ /** The manifest `kind` wire string, carried through verbatim to the result. */
5
+ kind: string;
6
+ /** The exact command line, retained for the result + fix instruction. */
7
+ command: string;
8
+ program: string;
9
+ args: string[];
10
+ /** The resolved per-check wall-clock timeout in milliseconds. */
11
+ timeoutMs: number;
12
+ }
13
+ /**
14
+ * The result of loading a manifest. `no-config` (exit 0, opt-in-by-presence),
15
+ * `schema-too-new` (exit non-zero, upgrade the runner), or `ready` with the
16
+ * planned checks (possibly empty ⇒ trivially passing).
17
+ */
18
+ type ManifestOutcome = {
19
+ kind: 'no-config';
20
+ } | {
21
+ kind: 'schema-too-new';
22
+ found: number;
23
+ } | {
24
+ kind: 'ready';
25
+ checks: PlannedCheck[];
26
+ };
27
+ /** Reads an absolute path, returning its contents or `null` if unreadable. */
28
+ type FileReader = (absolutePath: string) => string | null;
29
+
30
+ /**
31
+ * The runner + reporting — a faithful Node port of the in-process Rust runner
32
+ * (`workflow/gauntlet_project/runner.rs`), minus the Nightcore-orchestration-only
33
+ * machinery (retry-once/flaky, the security-critical no-retry exclusion, drift
34
+ * substrates, and the task verify-command append — none of which has a CI
35
+ * meaning). Statuses are therefore only `passed` / `failed`.
36
+ *
37
+ * FULL-RUN semantics (matching the live Rust runner, NOT the spec's stale
38
+ * stop-at-first text): every enabled check runs and records its own outcome, so
39
+ * a human reading CI sees the WHOLE failure set at once instead of one failure
40
+ * per pushed round. Each check is wall-clock BOUNDED by its `timeoutMs`; a
41
+ * timeout is a FAILED check (fail-closed — never a silent pass). `passed` is
42
+ * false iff ANY check failed; `failedCheck` names the FIRST failed check
43
+ * (back-compat).
44
+ *
45
+ * Pure over an injected {@link SpawnFn} so it is unit-testable without spawning.
46
+ */
47
+
48
+ /** A check outcome. `skipped`/`flaky` from the in-process runner are dropped. */
49
+ type CheckStatus = 'passed' | 'failed';
50
+ /**
51
+ * The outcome of one structure-lock check. Wire-compatible (camelCase) with the
52
+ * Rust `StructureLockCheck` — `exitCode` / `output` / `durationMs` are omitted
53
+ * when absent.
54
+ */
55
+ interface StructureLockCheck {
56
+ name: string;
57
+ kind: string;
58
+ command: string;
59
+ status: CheckStatus;
60
+ exitCode?: number;
61
+ output?: string;
62
+ durationMs?: number;
63
+ }
64
+ /**
65
+ * The structured verdict. Wire-compatible (camelCase) with the Rust
66
+ * `StructureLockResult` — `failedCheck` is omitted when everything passed.
67
+ */
68
+ interface StructureLockResult {
69
+ passed: boolean;
70
+ checks: StructureLockCheck[];
71
+ failedCheck?: string;
72
+ }
73
+ /** The normalized result of one spawn — a subset of Node's `spawnSync` return. */
74
+ interface SpawnResult {
75
+ /** Process exit code, or `null` when the process did not exit normally. */
76
+ status: number | null;
77
+ /** The signal that terminated the process, if any (e.g. a timeout kill). */
78
+ signal: string | null;
79
+ stdout: string;
80
+ stderr: string;
81
+ /** Set when the process could not be launched or was timed out. */
82
+ error?: {
83
+ code?: string;
84
+ message?: string;
85
+ };
86
+ }
87
+ /** Runs `program args` in `cwd`, bounded by `timeoutMs`. Must never throw. */
88
+ type SpawnFn = (program: string, args: string[], options: {
89
+ cwd: string;
90
+ timeoutMs: number;
91
+ }) => SpawnResult;
92
+
93
+ export type { CheckStatus as C, FileReader as F, ManifestOutcome as M, PlannedCheck as P, SpawnFn as S, StructureLockCheck as a, StructureLockResult as b };
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "@noctcore/harness",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "sideEffects": false,
6
+ "description": "Portable Structure-Lock runner — enforces a repo's .nightcore/harness.json checks in any CI, with no Nightcore install.",
7
+ "license": "MIT",
8
+ "homepage": "https://github.com/noctcore/nightcore/tree/main/packages/harness#readme",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/noctcore/nightcore.git",
12
+ "directory": "packages/harness"
13
+ },
14
+ "bugs": {
15
+ "url": "https://github.com/noctcore/nightcore/issues"
16
+ },
17
+ "publishConfig": {
18
+ "access": "public",
19
+ "provenance": true
20
+ },
21
+ "engines": {
22
+ "node": ">=22"
23
+ },
24
+ "bin": {
25
+ "harness": "./dist/cli.js"
26
+ },
27
+ "main": "./dist/index.cjs",
28
+ "module": "./dist/index.js",
29
+ "types": "./dist/index.d.ts",
30
+ "exports": {
31
+ ".": {
32
+ "types": "./dist/index.d.ts",
33
+ "import": "./dist/index.js",
34
+ "require": "./dist/index.cjs"
35
+ }
36
+ },
37
+ "files": [
38
+ "dist"
39
+ ],
40
+ "scripts": {
41
+ "build": "tsup src/cli.ts src/index.ts --format esm,cjs --dts --clean",
42
+ "prepack": "npm run build",
43
+ "check-types": "tsc --noEmit",
44
+ "test": "bun test"
45
+ },
46
+ "devDependencies": {
47
+ "@types/bun": "^1.1.0",
48
+ "@types/node": "^22.0.0",
49
+ "tsup": "^8.5.1",
50
+ "typescript": "^5.6.0"
51
+ }
52
+ }