@cosyte/script-utils 0.1.2 → 0.1.4
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/CHANGELOG.md +30 -12
- package/LICENSE +1 -1
- package/README.md +224 -15
- package/agent-notes.d.ts +92 -0
- package/agent-notes.js +512 -0
- package/attw.d.ts +18 -0
- package/attw.js +188 -8
- package/bin/cosyte-agent-notes.js +23 -0
- package/bin/cosyte-attw.js +24 -0
- package/bin/cosyte-catalog.js +23 -0
- package/bin/cosyte-claude-card.js +23 -0
- package/bin/cosyte-internal-refs.js +23 -0
- package/bin/cosyte-no-emdash.js +18 -0
- package/bin/cosyte-phi-scan.js +26 -0
- package/bin/cosyte-readme.js +22 -0
- package/bin/script-utils.js +44 -0
- package/catalog.d.ts +110 -0
- package/catalog.js +251 -0
- package/catalog.json +210 -0
- package/check-no-emdash.sh +69 -22
- package/claude-card.d.ts +64 -0
- package/claude-card.js +296 -0
- package/internal-refs.d.ts +12 -2
- package/internal-refs.js +131 -6
- package/package.json +40 -1
- package/phi-scan.d.ts +22 -0
- package/phi-scan.js +152 -4
- package/readme.d.ts +81 -0
- package/readme.js +489 -0
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;
|