@cosyte/script-utils 0.1.2 → 0.1.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/internal-refs.js CHANGED
@@ -24,8 +24,16 @@
24
24
  // change what this gate looks for or what it concludes.
25
25
 
26
26
  import { execFileSync } from "node:child_process";
27
- import { closeSync, constants as fsConstants, fstatSync, openSync, readFileSync } from "node:fs";
28
- import { join } from "node:path";
27
+ import {
28
+ closeSync,
29
+ constants as fsConstants,
30
+ fstatSync,
31
+ openSync,
32
+ readFileSync,
33
+ realpathSync,
34
+ } from "node:fs";
35
+ import { basename, join } from "node:path";
36
+ import { fileURLToPath } from "node:url";
29
37
 
30
38
  /**
31
39
  * The exit codes this gate assigns, as a caller may redeclare them.
@@ -332,7 +340,7 @@ export function compileCanonicalRules(resolved) {
332
340
  name: "phase or wave language",
333
341
  patterns: [
334
342
  new RegExp(
335
- `\\b(?:roadmap phase\\b[ ]?[A-Za-z0-9]*|${PHASE_NOT_CLINICAL}phase[ -]${PHASE_NOT_FIELD}` +
343
+ `\\b(?:roadmap phases?\\b[ ]?[A-Za-z0-9]*|${PHASE_NOT_CLINICAL}phases?[ -]${PHASE_NOT_FIELD}` +
336
344
  `[A-Za-z0-9]+[a-z]?\\b|wave \\d+\\b|the \\w+ and final phase\\b|` +
337
345
  `documentation residual\\b|${ORDINAL} (?:slice|wave)\\b)`,
338
346
  "gi",
@@ -340,6 +348,8 @@ export function compileCanonicalRules(resolved) {
340
348
  ],
341
349
  positives: [
342
350
  "Phase 5b closes it (Phase W, Phase-L and the thirteenth slice landed earlier, in wave 2)",
351
+ // The plural, which ten of the eleven per-repository copies matched (`phases?[ -]`).
352
+ "Phases 1 to 6 shipped together",
343
353
  ],
344
354
  negatives: [
345
355
  "CSP-1 Study Phase Identifier, CSP-2 Study Phase Start Date/Time, CSP-3 Study Phase End " +
@@ -351,8 +361,12 @@ export function compileCanonicalRules(resolved) {
351
361
  {
352
362
  id: "adr-reference",
353
363
  name: "ADR reference",
354
- patterns: [new RegExp("\\bADR[ -]?\\d{3,4}\\b", "gi")],
355
- positives: ["Decided in ADR 0015 and restated in ADR-0021"],
364
+ // `[ /-]`: ten of the eleven per-repository copies also matched an ADR path segment.
365
+ patterns: [new RegExp("\\bADR[ /-]?\\d{3,4}\\b", "gi")],
366
+ positives: [
367
+ "Decided in ADR 0015 and restated in ADR-0021",
368
+ "Recorded in docs/adr/0001-scope.md",
369
+ ],
356
370
  negatives: ["ADR is not a segment, and 0015 alone is a value"],
357
371
  },
358
372
  {
@@ -646,13 +660,26 @@ function compileExtraRule(rule, index) {
646
660
  }
647
661
  continue;
648
662
  }
663
+ // `{ source, flags }`: the form a JSON configuration can carry, so a rule read from
664
+ // `scripts/internal-refs.json` can be case-insensitive (`"flags": "i"`). `g` is always added.
665
+ if (isPlainObject(entry) && typeof entry.source === "string") {
666
+ const flags = typeof entry.flags === "string" ? entry.flags : "";
667
+ try {
668
+ patterns.push(new RegExp(entry.source, flags.includes("g") ? flags : `${flags}g`));
669
+ } catch (cause) {
670
+ problems.push(`${where} carries a pattern that does not compile: ${String(cause)}`);
671
+ }
672
+ continue;
673
+ }
649
674
  if (entry instanceof RegExp) {
650
675
  patterns.push(
651
676
  new RegExp(entry.source, entry.flags.includes("g") ? entry.flags : `${entry.flags}g`),
652
677
  );
653
678
  continue;
654
679
  }
655
- problems.push(`${where} carries a pattern that is neither a string nor a RegExp`);
680
+ problems.push(
681
+ `${where} carries a pattern that is neither a string, a RegExp nor a { source, flags } object`,
682
+ );
656
683
  }
657
684
  if (patterns.length === 0) problems.push(`${where} declares no pattern`);
658
685
  if (!isStringList(rule.positives) || rule.positives.length === 0) {
@@ -1361,3 +1388,101 @@ function report(hits) {
1361
1388
 
1362
1389
  /** Re-exported for a caller that wants to state, in its own docs, what it cannot turn off. */
1363
1390
  export { DEFAULT_EXIT_CODES, REFUSED_OPTIONS };
1391
+
1392
+ // ---- The command-line form (ST34) --------------------------------------------
1393
+ //
1394
+ // `node node_modules/@cosyte/script-utils/internal-refs.js` runs this gate over the repository in
1395
+ // the working directory with no caller script: the axes a caller would pass are read from
1396
+ // `scripts/internal-refs.json`, the one file a repository keeps for this gate. Keys starting with
1397
+ // `$` are notes and are ignored. The exit codes are the house contract, 0 clean, 1 hits, 2 the gate
1398
+ // could not run, and the file may not set them, the repository root or the output sinks: the
1399
+ // command-line form owns those, and a file that names one is refused rather than half-honoured.
1400
+
1401
+ /** Where the command-line form reads the repository's axes. */
1402
+ export const INTERNAL_REFS_CONFIG_PATH = "scripts/internal-refs.json";
1403
+
1404
+ /** The exit codes of the command-line form. */
1405
+ const CLI_EXIT_CODES = Object.freeze({ clean: 0, hits: 1, refuse: 2 });
1406
+
1407
+ /** Keys a configuration file may not set, because the command-line form sets them. */
1408
+ const CLI_OWNED_KEYS = ["exitCodes", "repoRoot", "write"];
1409
+
1410
+ /**
1411
+ * Run the gate from the command line over the repository in the working directory.
1412
+ *
1413
+ * @param {string[]} argv Only `--config <path>` is accepted (default `scripts/internal-refs.json`).
1414
+ * @returns {number} 0 clean, 1 hits, 2 the gate could not run.
1415
+ */
1416
+ export function runInternalRefsCli(argv) {
1417
+ let path = INTERNAL_REFS_CONFIG_PATH;
1418
+ const args = argv.filter((arg) => arg !== "--");
1419
+ for (let i = 0; i < args.length; i += 1) {
1420
+ if (args[i] === "--config" && typeof args[i + 1] === "string") {
1421
+ path = args[i + 1];
1422
+ i += 1;
1423
+ } else {
1424
+ process.stderr.write(
1425
+ `internal-refs: ${JSON.stringify(args[i])} is not an argument this gate accepts (only ` +
1426
+ `--config <path>). Nothing was checked.\n`,
1427
+ );
1428
+ return CLI_EXIT_CODES.refuse;
1429
+ }
1430
+ }
1431
+ let parsed;
1432
+ try {
1433
+ parsed = JSON.parse(readFileSync(join(process.cwd(), path), "utf8"));
1434
+ } catch (err) {
1435
+ process.stderr.write(
1436
+ `internal-refs: could not read ${path}: ${err.message} Nothing was checked.\n`,
1437
+ );
1438
+ return CLI_EXIT_CODES.refuse;
1439
+ }
1440
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
1441
+ process.stderr.write(`internal-refs: ${path} is not a JSON object. Nothing was checked.\n`);
1442
+ return CLI_EXIT_CODES.refuse;
1443
+ }
1444
+ const owned = CLI_OWNED_KEYS.filter((key) => key in parsed);
1445
+ if (owned.length > 0) {
1446
+ process.stderr.write(
1447
+ `internal-refs: ${path} sets ${owned.join(", ")}, which the command-line form sets ` +
1448
+ `itself. Remove ${owned.length === 1 ? "it" : "them"}. Nothing was checked.\n`,
1449
+ );
1450
+ return CLI_EXIT_CODES.refuse;
1451
+ }
1452
+ const config = Object.fromEntries(Object.entries(parsed).filter(([key]) => !key.startsWith("$")));
1453
+ return runInternalRefsScan({ ...config, exitCodes: CLI_EXIT_CODES, repoRoot: process.cwd() });
1454
+ }
1455
+
1456
+ /**
1457
+ * Whether node was pointed at this file, which is the command-line form.
1458
+ *
1459
+ * SELF-CONTAINED ON PURPOSE: this module is also vendored and copied as a single file, so it may not
1460
+ * import a sibling at load. A file whose name differs from the one node was pointed at cannot be the
1461
+ * entry point, and that is settled without loading anything; only a matching name asks `index.js`'s
1462
+ * `isCliEntrypoint`, and a copy with no `index.js` beside it compares real paths instead. Unsure is
1463
+ * yes, so a gate that cannot tell runs loudly rather than skipping.
1464
+ *
1465
+ * @param {string} moduleUrl This module's `import.meta.url`.
1466
+ * @returns {Promise<boolean>}
1467
+ */
1468
+ async function invokedAsCommand(moduleUrl) {
1469
+ const entry = process.argv[1];
1470
+ if (typeof entry !== "string" || entry === "") return false;
1471
+ const own = fileURLToPath(moduleUrl);
1472
+ const stem = (path) => basename(path).replace(/\.[cm]?[jt]s$/, "");
1473
+ if (stem(entry) !== stem(own)) return false;
1474
+ try {
1475
+ const { isCliEntrypoint } = await import("./index.js");
1476
+ return isCliEntrypoint(moduleUrl);
1477
+ } catch {
1478
+ try {
1479
+ return realpathSync(entry) === realpathSync(own);
1480
+ } catch {
1481
+ return true;
1482
+ }
1483
+ }
1484
+ }
1485
+
1486
+ void invokedAsCommand(import.meta.url).then((command) => {
1487
+ if (command) process.exitCode = runInternalRefsCli(process.argv.slice(2));
1488
+ });
package/package.json CHANGED
@@ -1,12 +1,13 @@
1
1
  {
2
2
  "name": "@cosyte/script-utils",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "Zero-dependency helpers for the repo-local gate scripts in @cosyte/* repos.",
5
5
  "keywords": [
6
6
  "cli",
7
7
  "entrypoint",
8
8
  "import-meta",
9
9
  "phi",
10
+ "readme",
10
11
  "scripts",
11
12
  "cosyte"
12
13
  ],
@@ -37,6 +38,22 @@
37
38
  "./attw": {
38
39
  "types": "./attw.d.ts",
39
40
  "default": "./attw.js"
41
+ },
42
+ "./readme": {
43
+ "types": "./readme.d.ts",
44
+ "default": "./readme.js"
45
+ },
46
+ "./claude-card": {
47
+ "types": "./claude-card.d.ts",
48
+ "default": "./claude-card.js"
49
+ },
50
+ "./catalog": {
51
+ "types": "./catalog.d.ts",
52
+ "default": "./catalog.js"
53
+ },
54
+ "./agent-notes": {
55
+ "types": "./agent-notes.d.ts",
56
+ "default": "./agent-notes.js"
40
57
  }
41
58
  },
42
59
  "files": [
@@ -48,6 +65,15 @@
48
65
  "internal-refs.d.ts",
49
66
  "attw.js",
50
67
  "attw.d.ts",
68
+ "readme.js",
69
+ "readme.d.ts",
70
+ "claude-card.js",
71
+ "claude-card.d.ts",
72
+ "catalog.js",
73
+ "catalog.d.ts",
74
+ "catalog.json",
75
+ "agent-notes.js",
76
+ "agent-notes.d.ts",
51
77
  "check-no-emdash.sh",
52
78
  "README.md",
53
79
  "CHANGELOG.md"
package/phi-scan.d.ts CHANGED
@@ -274,3 +274,25 @@ export declare function runPhiScan(config: PhiScanConfig): number;
274
274
  * is read by NEITHER sweeping route, while a `.md` named explicitly on argv IS scanned.
275
275
  */
276
276
  export declare function exemptsMarkdown(relPath: string): boolean;
277
+
278
+ /** Where the command-line form reads what a repository leaves out: `scripts/phi-scan.exclude`. */
279
+ export declare const PHI_EXCLUDE_PATH: "scripts/phi-scan.exclude";
280
+
281
+ /**
282
+ * Read an exclusions file (one literal tracked path per line, `#` lines are comments) against the
283
+ * repository's tracked paths. A directory, a pattern, a repeat or a path git does not track is a
284
+ * problem, never a silent skip.
285
+ */
286
+ export declare function readPhiExclusions(
287
+ text: string,
288
+ tracked: ReadonlySet<string>,
289
+ ): { paths: Set<string>; problems: string[] };
290
+
291
+ /**
292
+ * The command-line form: the engine over the repository in the working directory, with exit codes
293
+ * 0 clean, 1 hits, 2 refuse, the whole repository as the scan root, the `--staged` route reading
294
+ * everything but Markdown, and `scripts/phi-scan.exclude`. The cross-cutting floor only: a
295
+ * repository that needs field-level detectors keeps a caller passing `detect`.
296
+ * `node node_modules/@cosyte/script-utils/phi-scan.js [--staged | <path>...]`.
297
+ */
298
+ export declare function runPhiScanCli(argv: string[]): number;
package/phi-scan.js CHANGED
@@ -39,10 +39,11 @@
39
39
  // repository reads its own `node_modules` copy of this file only if it is not gitignored, and a
40
40
  // diagnostic ABOUT a PHI leak is itself a PHI surface. Describe the shape; never write one down.
41
41
 
42
- import { readFileSync, statSync, lstatSync, existsSync, readdirSync } from "node:fs";
42
+ import { readFileSync, statSync, lstatSync, existsSync, readdirSync, realpathSync } from "node:fs";
43
43
  import { execFileSync } from "node:child_process";
44
44
  import { createHash } from "node:crypto";
45
- import { join, resolve, relative, sep, isAbsolute } from "node:path";
45
+ import { basename, join, resolve, relative, sep, isAbsolute } from "node:path";
46
+ import { fileURLToPath } from "node:url";
46
47
 
47
48
  /**
48
49
  * Every state in which the scan cannot account for something. Raised throughout, caught in `run`,
@@ -1135,10 +1136,15 @@ class PhiScan {
1135
1136
  buildTargetsForStaged() {
1136
1137
  let listBuf;
1137
1138
  try {
1138
- // SECURITY: array-form execFileSync, no shell.
1139
+ // SECURITY: array-form execFileSync, no shell. `--ignore-submodules=none` because
1140
+ // `diff.ignoreSubmodules` in a user's own git config would otherwise hide a staged gitlink
1141
+ // from this list, and the route would never refuse it (x12, terminology and astm pass it;
1142
+ // measured in x12's own suite).
1139
1143
  listBuf = execFileSync(
1140
1144
  "git",
1141
- ["diff", "--cached", "--raw", "-z", "--no-renames", "--diff-filter=d"],
1145
+ ["diff", "--cached", "--raw", "-z", "--no-renames", "--diff-filter=d"].concat(
1146
+ "--ignore-submodules=none",
1147
+ ),
1142
1148
  {
1143
1149
  cwd: this.cfg.repoRoot,
1144
1150
  encoding: "buffer",
@@ -1942,3 +1948,145 @@ function gitModeKind(mode) {
1942
1948
  if (mode === "160000") return "a gitlink (a nested repository)";
1943
1949
  return `a git mode-${mode} entry`;
1944
1950
  }
1951
+
1952
+ // ---- The command-line form (ST34) ---------------------------------------------------------------
1953
+ //
1954
+ // `node node_modules/@cosyte/script-utils/phi-scan.js [--staged | <path>...]` runs this engine over
1955
+ // the repository in the working directory with the house settings and no caller script: exit codes
1956
+ // 0 clean, 1 hits, 2 refuse; the whole repository as the one scan root; the `--staged` route reading
1957
+ // everything but Markdown (`exemptsMarkdown`); the allow-list at `scripts/phi-allow-list.txt` and the
1958
+ // override log at `phi-scan-overrides.md`, the engine's defaults; and what the repository leaves
1959
+ // out, in `scripts/phi-scan.exclude`.
1960
+ //
1961
+ // AN EXCLUSION IS ONE LITERAL TRACKED PATH, as `excludedPaths` requires: no pattern, no directory,
1962
+ // because a class quietly grows new members. One per line; a line whose first character is `#` is a
1963
+ // comment. An entry that names no tracked path is refused rather than ignored: an exclusion that has
1964
+ // outlived its file is a hole nobody is looking at.
1965
+ //
1966
+ // IT CARRIES THE CROSS-CUTTING FLOOR ONLY. A repository whose standard needs field-level detectors
1967
+ // keeps a caller passing `detect` until the per-standard detectors ship in this package.
1968
+
1969
+ /** Where the command-line form reads what a repository leaves out. */
1970
+ export const PHI_EXCLUDE_PATH = "scripts/phi-scan.exclude";
1971
+
1972
+ /** The exit codes of the command-line form. */
1973
+ const CLI_EXIT_CODES = Object.freeze({ clean: 0, hits: 1, refuse: 2 });
1974
+
1975
+ /**
1976
+ * Read an exclusions file against the repository's tracked paths.
1977
+ *
1978
+ * @param {string} text The file's contents.
1979
+ * @param {ReadonlySet<string>} tracked Every tracked path, repo-relative and forward-slashed.
1980
+ * @returns {{ paths: Set<string>, problems: string[] }}
1981
+ */
1982
+ export function readPhiExclusions(text, tracked) {
1983
+ const paths = new Set();
1984
+ const problems = [];
1985
+ text.split("\n").forEach((raw, index) => {
1986
+ const entry = raw.trim();
1987
+ if (entry === "" || entry.startsWith("#")) return;
1988
+ const where = `line ${index + 1}: ${JSON.stringify(entry)}`;
1989
+ if (entry.endsWith("/") || /[*?[\]]/.test(entry)) {
1990
+ problems.push(`${where} is a directory or a pattern; an exclusion is one literal path.`);
1991
+ } else if (paths.has(entry)) {
1992
+ problems.push(`${where} is listed twice.`);
1993
+ } else if (!tracked.has(entry)) {
1994
+ problems.push(`${where} is not a tracked path, so it excludes nothing.`);
1995
+ } else {
1996
+ paths.add(entry);
1997
+ }
1998
+ });
1999
+ return { paths, problems };
2000
+ }
2001
+
2002
+ /**
2003
+ * Run the engine from the command line over the repository in the working directory.
2004
+ *
2005
+ * @param {string[]} argv The engine's own arguments: `--staged`, paths, `--allow-fixture <path>`.
2006
+ * @returns {number} 0 clean, 1 hits, 2 the scan could not account for something.
2007
+ */
2008
+ export function runPhiScanCli(argv) {
2009
+ const repoRoot = process.cwd();
2010
+ let excludedPaths = new Set();
2011
+ const file = join(repoRoot, ...PHI_EXCLUDE_PATH.split("/"));
2012
+ if (existsSync(file)) {
2013
+ let read;
2014
+ try {
2015
+ const tracked = new Set(
2016
+ execFileSync("git", ["ls-files", "-z"], {
2017
+ cwd: repoRoot,
2018
+ encoding: "utf8",
2019
+ stdio: ["ignore", "pipe", "pipe"],
2020
+ maxBuffer: 256 * 1024 * 1024,
2021
+ })
2022
+ .split("\0")
2023
+ .filter((path) => path !== ""),
2024
+ );
2025
+ read = readPhiExclusions(readFileSync(file, "utf8"), tracked);
2026
+ } catch (err) {
2027
+ process.stderr.write(
2028
+ `[phi-scan] could not read ${PHI_EXCLUDE_PATH}: ${err instanceof Error ? err.message : String(err)}\n`,
2029
+ );
2030
+ return CLI_EXIT_CODES.refuse;
2031
+ }
2032
+ if (read.problems.length > 0) {
2033
+ process.stderr.write(
2034
+ `[phi-scan] ${PHI_EXCLUDE_PATH} is not usable, so nothing was scanned:\n` +
2035
+ read.problems.map((problem) => ` ${problem}\n`).join(""),
2036
+ );
2037
+ return CLI_EXIT_CODES.refuse;
2038
+ }
2039
+ excludedPaths = read.paths;
2040
+ }
2041
+ try {
2042
+ return runPhiScan({
2043
+ exitCodes: CLI_EXIT_CODES,
2044
+ scanRoots: ["."],
2045
+ isStagedReadable: exemptsMarkdown,
2046
+ excludedPaths,
2047
+ repoRoot,
2048
+ argv,
2049
+ });
2050
+ } catch (err) {
2051
+ // The backstop: an error the engine did not raise as a refusal must still not take node's own
2052
+ // exit 1, which this contract reserves for hits.
2053
+ process.stderr.write(
2054
+ `[phi-scan] the scan failed and did not complete: ${err instanceof Error ? err.message : String(err)}\n`,
2055
+ );
2056
+ return CLI_EXIT_CODES.refuse;
2057
+ }
2058
+ }
2059
+
2060
+ /**
2061
+ * Whether node was pointed at this file, which is the command-line form.
2062
+ *
2063
+ * SELF-CONTAINED ON PURPOSE: this module is also vendored and copied as a single file, so it may not
2064
+ * import a sibling at load. A file whose name differs from the one node was pointed at cannot be the
2065
+ * entry point, and that is settled without loading anything; only a matching name asks `index.js`'s
2066
+ * `isCliEntrypoint`, and a copy with no `index.js` beside it compares real paths instead. Unsure is
2067
+ * yes, so a gate that cannot tell runs loudly rather than skipping.
2068
+ *
2069
+ * @param {string} moduleUrl This module's `import.meta.url`.
2070
+ * @returns {Promise<boolean>}
2071
+ */
2072
+ async function invokedAsCommand(moduleUrl) {
2073
+ const entry = process.argv[1];
2074
+ if (typeof entry !== "string" || entry === "") return false;
2075
+ const own = fileURLToPath(moduleUrl);
2076
+ const stem = (path) => basename(path).replace(/\.[cm]?[jt]s$/, "");
2077
+ if (stem(entry) !== stem(own)) return false;
2078
+ try {
2079
+ const { isCliEntrypoint } = await import("./index.js");
2080
+ return isCliEntrypoint(moduleUrl);
2081
+ } catch {
2082
+ try {
2083
+ return realpathSync(entry) === realpathSync(own);
2084
+ } catch {
2085
+ return true;
2086
+ }
2087
+ }
2088
+ }
2089
+
2090
+ void invokedAsCommand(import.meta.url).then((command) => {
2091
+ if (command) process.exitCode = runPhiScanCli(process.argv.slice(2));
2092
+ });
package/readme.d.ts ADDED
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Types for `@cosyte/script-utils/readme`: the README gate for every `@cosyte/*` library, which
3
+ * checks the house skeleton and a size limit under npm's.
4
+ */
5
+
6
+ /** The tile's alt text, as assets `alt-text.json` declares it for the light tile. */
7
+ export declare const TILE_ALT: string;
8
+
9
+ /** The shared lockup tile every library README opens with, byte for byte. */
10
+ export declare const TILE: string;
11
+
12
+ /** npm's own cut: the registry keeps this many characters of a README. */
13
+ export declare const NPM_README_LIMIT: 65536;
14
+
15
+ /** This gate's limit, in characters (UTF-16 code units). */
16
+ export declare const README_CHARACTER_LIMIT: 60000;
17
+
18
+ /** The longest tagline the gate accepts, in characters. */
19
+ export declare const TAGLINE_LIMIT: 120;
20
+
21
+ /** The `##` sections every library README carries, in order, with License last. */
22
+ export declare const REQUIRED_SECTIONS: readonly string[];
23
+
24
+ /** A rule id a finding can carry. */
25
+ export type ReadmeRule =
26
+ | "tile"
27
+ | "tile-alt"
28
+ | "title"
29
+ | "tagline"
30
+ | "badges"
31
+ | "section-missing"
32
+ | "section-order"
33
+ | "section-empty"
34
+ | "docs-link"
35
+ | "license-last"
36
+ | "license-link"
37
+ | "size";
38
+
39
+ /** Every rule id, with what it requires. */
40
+ export declare const README_RULES: Readonly<Record<ReadmeRule, string>>;
41
+
42
+ /** One place a README departs from the skeleton. */
43
+ export interface ReadmeFinding {
44
+ rule: ReadmeRule;
45
+ /** 1-based line, or 0 when the finding is about the whole file. */
46
+ line: number;
47
+ message: string;
48
+ }
49
+
50
+ /**
51
+ * The badge row a library README carries (npm version, CI, license, Node), one badge per line,
52
+ * derived from its package.json.
53
+ *
54
+ * @throws {TypeError} When the manifest has no `@cosyte/*` name, no github.com/cosyte repository
55
+ * URL, or no `engines.node`.
56
+ */
57
+ export declare function badgeRow(manifest: Record<string, unknown>): string[];
58
+
59
+ /**
60
+ * Check a library README against the house skeleton. Returns no findings when it follows it.
61
+ *
62
+ * @throws {TypeError} When the manifest cannot supply what the skeleton is derived from.
63
+ */
64
+ export declare function checkReadme(
65
+ text: string,
66
+ manifest: Record<string, unknown>,
67
+ ): ReadmeFinding[];
68
+
69
+ export interface ReadmeGateOptions {
70
+ /** The package directory holding README.md and package.json. Default: the working directory. */
71
+ dir?: string;
72
+ /** CLI arguments; only `--dir <path>` is accepted. */
73
+ argv?: string[];
74
+ }
75
+
76
+ /**
77
+ * Run the gate over one package directory and return the exit code: 0 the README follows the
78
+ * skeleton, 1 at least one finding, 2 the input could not be read. It reports on stdout or stderr
79
+ * and never exits the process.
80
+ */
81
+ export declare function runReadmeGate(options?: ReadmeGateOptions): 0 | 1 | 2;