@noir-ai/cli 1.16.0-beta.1 → 1.17.0-beta.1
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/README.md +1 -1
- package/dist/bin.js +14 -3
- package/dist/bin.js.map +1 -1
- package/dist/hygiene-scan.js +11 -1
- package/dist/hygiene-scan.js.map +1 -1
- package/dist/index.js +14 -3
- package/dist/index.js.map +1 -1
- package/dist/tui/{chunk-6AUAIEFG.js → chunk-NZMM6YXE.js} +17 -6
- package/dist/tui/chunk-NZMM6YXE.js.map +1 -0
- package/dist/tui/{create-ZZN7XFLG.js → create-4XGNRRP7.js} +2 -2
- package/dist/tui/index.js +1 -1
- package/dist/tui/{sync-GHEPFPLX.js → sync-Q3KXM5RQ.js} +2 -2
- package/package.json +11 -11
- package/dist/tui/chunk-6AUAIEFG.js.map +0 -1
- /package/dist/tui/{create-ZZN7XFLG.js.map → create-4XGNRRP7.js.map} +0 -0
- /package/dist/tui/{sync-GHEPFPLX.js.map → sync-Q3KXM5RQ.js.map} +0 -0
package/dist/hygiene-scan.js
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
// src/hygiene-scan.ts
|
|
2
2
|
import { readdirSync, readFileSync, statSync } from "fs";
|
|
3
3
|
import { join } from "path";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
checkHygiene,
|
|
6
|
+
hasHygieneExemption,
|
|
7
|
+
humanizerFindings,
|
|
8
|
+
slopFindings
|
|
9
|
+
} from "@noir-ai/skills";
|
|
5
10
|
var HYGIENE_FINDING_CAP = 5;
|
|
6
11
|
var HYGIENE_MAX_FILE_BYTES = 512 * 1024;
|
|
7
12
|
var HYGIENE_MAX_FILES = 2e3;
|
|
@@ -136,6 +141,11 @@ function scanOutputHygiene(root) {
|
|
|
136
141
|
for (const found of checkHygiene(text, file.kind)) {
|
|
137
142
|
findings.push({ path: file.path, line: found.line, id: found.id, tier: found.tier });
|
|
138
143
|
}
|
|
144
|
+
if (file.kind === "markdown" && !hasHygieneExemption(text, file.kind)) {
|
|
145
|
+
for (const found of [...slopFindings(text), ...humanizerFindings(text)]) {
|
|
146
|
+
findings.push({ path: file.path, line: found.line, id: found.id, tier: found.tier });
|
|
147
|
+
}
|
|
148
|
+
}
|
|
139
149
|
} catch {
|
|
140
150
|
}
|
|
141
151
|
}
|
package/dist/hygiene-scan.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/hygiene-scan.ts"],"sourcesContent":["// Output hygiene — the repository-facing scan.\n//\n// The repository's own source and documents are checked against the hygiene\n// rules @noir-ai/skills declares: the patterns that make text read as machine\n// output. A fail-tier pattern is objectively mechanical (a divider drawn in\n// punctuation, numbered narration, a decorative emoji, a forbidden residue\n// token), so it blocks; a warn-tier pattern is a judgement call (a long comment\n// block, a marker nobody can act on), which would stall legitimate work if it\n// blocked, so it warns.\n//\n// The scan reads the repository's own text only. Generated and vendored trees,\n// the planning corpus, and formats the rules cannot read (JSON, an image\n// fixture) are out of scope; a file states its own exemption with the marker\n// the rules honour, so no path list is kept here.\n//\n// The scan runs only where the layout it is written for exists: a package\n// tree, `scripts/`, or `docs/`. Anywhere else — an ordinary application\n// repository, which is a README and little more — it reports an empty result\n// and reads nothing, root documents included. These rules judge the text this\n// project writes, not the prose of whoever happened to run the command.\n//\n// This module is the single source of truth for that scope: which files are\n// read, which directories are excluded, and the caps. `noir doctor` (via\n// `checkOutputHygiene` in ./commands/doctor.ts) and the CI gate\n// (scripts/hygiene-gate.mjs) both call `scanOutputHygiene`, so the two cannot\n// drift apart.\n\nimport { type Dirent, readdirSync, readFileSync, statSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { checkHygiene, type HygieneKind, type HygieneTier } from '@noir-ai/skills';\n\n/** How many findings the detail cell names before it reports the rest as a\n * count. A tree that has drifted can carry hundreds, and the row is a signal\n * to act on, not an inventory; the counts still cover every finding. */\nexport const HYGIENE_FINDING_CAP = 5;\n\n/** The largest file the scan reads. A file this big is generated data or a\n * bundle rather than the source and prose the rules are written for. */\nexport const HYGIENE_MAX_FILE_BYTES = 512 * 1024;\n\n/** The most files one scan reads, so the check stays responsive in a tree far\n * larger than this repository's. Reaching it is reported in the row. */\nexport const HYGIENE_MAX_FILES = 2000;\n\n/** The source extensions the check reads. Anything else is skipped: the rules\n * are written for comments and for prose, and a data format (JSON, YAML) has\n * no comment for them to read, while a binary fixture would only produce\n * nonsense. */\nconst HYGIENE_CODE_EXTENSIONS = /\\.(?:ts|tsx|js|jsx|mjs|cjs|sh)$/i;\n\n/** The document extensions the check reads as prose. */\nconst HYGIENE_MARKDOWN_EXTENSIONS = /\\.(?:md|mdx)$/i;\n\n/** Directories the walk never descends into: dependencies, build output and\n * coverage. A dot-directory is skipped too (`.git`, `.noir`, local scratch\n * directories), which is how the maintainer's exclusion of session scratch is\n * honoured by construction rather than by naming it here. */\nconst HYGIENE_SKIP_DIRS = new Set(['node_modules', 'dist', 'coverage']);\n\n/** Directories excluded from the scan by the maintainer's decision: the\n * planning corpus, where decisions are recorded in the maintainer's own\n * shorthand. Root-relative POSIX paths. */\nconst HYGIENE_EXCLUDED_DIRS = new Set(['docs/internal', 'docs/decisions', 'docs/roadmap']);\n\n/** The release log, excluded by basename wherever it sits: the root copy is the\n * single source of truth and `docs/CHANGELOG.md` is a pointer to it. */\nconst HYGIENE_EXCLUDED_FILES = new Set(['CHANGELOG.md']);\n\n/** A file the scan will read, with the kind of text it holds. */\ninterface HygieneScanFile {\n /** Root-relative POSIX path. */\n path: string;\n kind: HygieneKind;\n}\n\n/** One finding, reduced to what the report needs. */\nexport interface HygieneScanFinding {\n path: string;\n /** 1-based line number. */\n line: number;\n /** The rule id, exactly as the rule table declares it. */\n id: string;\n tier: HygieneTier;\n}\n\nexport interface HygieneScanResult {\n /** Files read. */\n scanned: number;\n /** Files the scan did not read because they exceed the size cap. */\n skipped: number;\n /** True when the file cap stopped the scan before it read the whole tree, so\n * the counts below describe a prefix of it. */\n truncated: boolean;\n /** Every finding, failures first, then in reading order across the tree. */\n findings: HygieneScanFinding[];\n fail: number;\n warn: number;\n}\n\n/** A walk in progress: the files it has accepted, and whether the file cap has\n * stopped it. */\ninterface HygieneWalk {\n files: HygieneScanFile[];\n truncated: boolean;\n}\n\n/** `readdirSync` entries, or none when the directory does not exist. */\nfunction readDirEntries(dir: string): Dirent[] {\n try {\n return readdirSync(dir, { withFileTypes: true });\n } catch {\n return []; // absent or unreadable — there is nothing to scan\n }\n}\n\n/** Whether `path` is an existing directory. */\nfunction isDirectory(path: string): boolean {\n try {\n return statSync(path).isDirectory();\n } catch {\n return false;\n }\n}\n\n/** The kind of text a path holds, or `null` when the rules cannot read it. */\nfunction hygieneKindOf(path: string): HygieneKind | null {\n if (HYGIENE_MARKDOWN_EXTENSIONS.test(path)) return 'markdown';\n if (HYGIENE_CODE_EXTENSIONS.test(path)) return 'code';\n return null;\n}\n\n/** The layout this check exists for: a package source or test tree, `scripts/`,\n * or a documentation tree. A repository that has none of them is not the\n * audience of these rules, and nothing is read there. */\nfunction hasScannableLayout(root: string): boolean {\n if (isDirectory(join(root, 'docs')) || isDirectory(join(root, 'scripts'))) return true;\n for (const entry of readDirEntries(join(root, 'packages'))) {\n if (!entry.isDirectory()) continue;\n if (isDirectory(join(root, 'packages', entry.name, 'src'))) return true;\n if (isDirectory(join(root, 'packages', entry.name, 'test'))) return true;\n }\n return false;\n}\n\n/** Adds `rel` to the walk when it is a file the rules can read and the walk has\n * room for it. A dot-file is never read (a dot-directory is skipped by the\n * walk itself), and once the file cap is reached the walk records the\n * truncation instead of adding more. */\nfunction addHygieneFile(rel: string, walk: HygieneWalk): void {\n const name = rel.slice(rel.lastIndexOf('/') + 1);\n if (name.startsWith('.') || HYGIENE_EXCLUDED_FILES.has(name)) return;\n const kind = hygieneKindOf(rel);\n if (kind === null) return;\n if (walk.files.length >= HYGIENE_MAX_FILES) {\n walk.truncated = true;\n return;\n }\n walk.files.push({ path: rel, kind });\n}\n\n/** Offers every readable file under `root`/`relDir` to the walk. Symlinked\n * directories are not followed: a link out of the tree is not the\n * repository's own source, and a link to an ancestor would not terminate. */\nfunction walkScannable(\n root: string,\n relDir: string,\n skip: ReadonlySet<string>,\n walk: HygieneWalk,\n): void {\n if (walk.truncated) return;\n for (const entry of readDirEntries(join(root, relDir))) {\n if (walk.truncated) return;\n const rel = `${relDir}/${entry.name}`;\n if (entry.isDirectory()) {\n if (HYGIENE_SKIP_DIRS.has(entry.name) || entry.name.startsWith('.') || skip.has(rel)) {\n continue;\n }\n walkScannable(root, rel, skip, walk);\n } else if (entry.isFile()) {\n addHygieneFile(rel, walk);\n }\n }\n}\n\nconst NO_SKIP_DIRS: ReadonlySet<string> = new Set();\n\n/** The files the check reads: documents at the root and under `docs/` (minus\n * the planning corpus), the repository-authored agent skills, each package's\n * sources and tests, and `scripts/`. Sorted by path so a report is the same on\n * every run. Called only where {@link hasScannableLayout} holds. */\nfunction collectHygieneFiles(root: string): HygieneWalk {\n const walk: HygieneWalk = { files: [], truncated: false };\n for (const entry of readDirEntries(root)) {\n if (entry.isFile()) addHygieneFile(entry.name, walk);\n }\n walkScannable(root, 'docs', HYGIENE_EXCLUDED_DIRS, walk);\n walkScannable(root, '.claude/skills', NO_SKIP_DIRS, walk);\n for (const entry of readDirEntries(join(root, 'packages'))) {\n if (!entry.isDirectory()) continue;\n walkScannable(root, `packages/${entry.name}/src`, NO_SKIP_DIRS, walk);\n walkScannable(root, `packages/${entry.name}/test`, NO_SKIP_DIRS, walk);\n }\n walkScannable(root, 'scripts', NO_SKIP_DIRS, walk);\n walk.files.sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));\n return walk;\n}\n\n/** What the scan left out, in one parenthesis. Empty when it left out nothing,\n * so a complete report carries no note. */\nfunction hygieneOmissions(result: HygieneScanResult): string {\n const notes: string[] = [];\n if (result.skipped > 0) {\n notes.push(\n `${result.skipped} file${result.skipped === 1 ? '' : 's'} over ${HYGIENE_MAX_FILE_BYTES / 1024} KiB skipped`,\n );\n }\n if (result.truncated) notes.push(`stopped at ${HYGIENE_MAX_FILES} files`);\n return notes.length > 0 ? ` (${notes.join('; ')})` : '';\n}\n\n/** The counts line: how many findings of each tier, in how many files, and what\n * the scan itself omitted. It names no location, so a caller that prints every\n * finding on a line of its own pairs this with those lines instead of printing\n * the same findings twice. */\nexport function hygieneCounts(result: HygieneScanResult): string {\n const omitted = hygieneOmissions(result);\n if (result.scanned === 0) {\n return `nothing to scan (no packages/*/src, packages/*/test, scripts/ or documents found)${omitted}`;\n }\n if (result.findings.length === 0) {\n return `clean — ${result.scanned} file${result.scanned === 1 ? '' : 's'} scanned${omitted}`;\n }\n const files = new Set(result.findings.map((f) => f.path)).size;\n return `${result.fail} fail, ${result.warn} warn in ${files} file${files === 1 ? '' : 's'}${omitted}`;\n}\n\n/** The counts line followed by the locations that carry them, up to the cap.\n * Failures are named before warnings, so a truncated list still shows what\n * blocks. This is the doctor row, which has one line to work with. */\nexport function hygieneDetail(result: HygieneScanResult): string {\n const counts = hygieneCounts(result);\n if (result.scanned === 0 || result.findings.length === 0) return counts;\n const named = result.findings.slice(0, HYGIENE_FINDING_CAP);\n const hidden = result.findings.length - named.length;\n const where = named.map((f) => `${f.path}:${f.line} ${f.id}`).join('; ');\n return `${counts} — ${where}${hidden > 0 ? ` (+${hidden} more)` : ''}`;\n}\n\n/**\n * Reads the repository's own source and documents through `checkHygiene` and\n * returns the scan result: the files read, the findings (failures first), and\n * the fail/warn counts. It pushes no doctor row — that is the caller's\n * business (`checkOutputHygiene` in the doctor command, or the CI gate's own\n * report).\n *\n * Nothing is read unless {@link hasScannableLayout} holds, so a repository\n * without the layout gets an empty result and pays one `stat` per candidate.\n *\n * Each file is read once, and a file that carries the exemption marker comes\n * back from `checkHygiene` with no findings at all, so the marker costs one\n * scan of a file's text rather than a path list kept here. Files past the size\n * cap and files past the count cap are left out; the result says so.\n *\n * The scan never throws and never writes: an unreadable file is skipped, and\n * the commands that repair the tree (`noir skills lint`, the repository's own\n * sweep) are the caller's business.\n */\nexport function scanOutputHygiene(root: string): HygieneScanResult {\n const empty: HygieneScanResult = {\n scanned: 0,\n skipped: 0,\n truncated: false,\n findings: [],\n fail: 0,\n warn: 0,\n };\n if (!hasScannableLayout(root)) return empty;\n const walk = collectHygieneFiles(root);\n const findings: HygieneScanFinding[] = [];\n let scanned = 0;\n let skipped = 0;\n for (const file of walk.files) {\n const abs = join(root, file.path);\n try {\n if (statSync(abs).size > HYGIENE_MAX_FILE_BYTES) {\n skipped++;\n continue;\n }\n const text = readFileSync(abs, 'utf8');\n scanned++;\n for (const found of checkHygiene(text, file.kind)) {\n findings.push({ path: file.path, line: found.line, id: found.id, tier: found.tier });\n }\n } catch {\n // An unreadable path says nothing about hygiene — the store check owns\n // broken files — so the scan moves on to the next one.\n }\n }\n findings.sort((a, b) => {\n if (a.tier !== b.tier) return a.tier === 'fail' ? -1 : 1;\n if (a.path !== b.path) return a.path < b.path ? -1 : 1;\n return a.line - b.line;\n });\n const fail = findings.filter((f) => f.tier === 'fail').length;\n return {\n scanned,\n skipped,\n truncated: walk.truncated,\n findings,\n fail,\n warn: findings.length - fail,\n };\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/hygiene-scan.ts"],"sourcesContent":["// Output hygiene — the repository-facing scan.\n//\n// The repository's own source and documents are checked against the hygiene\n// rules @noir-ai/skills declares: the patterns that make text read as machine\n// output. A fail-tier pattern is objectively mechanical (a divider drawn in\n// punctuation, numbered narration, a decorative emoji, a forbidden residue\n// token), so it blocks; a warn-tier pattern is a judgement call (a long comment\n// block, a marker nobody can act on), which would stall legitimate work if it\n// blocked, so it warns.\n//\n// The scan reads the repository's own text only. Generated and vendored trees,\n// the planning corpus, and formats the rules cannot read (JSON, an image\n// fixture) are out of scope; a file states its own exemption with the marker\n// the rules honour, so no path list is kept here.\n//\n// The scan runs only where the layout it is written for exists: a package\n// tree, `scripts/`, or `docs/`. Anywhere else — an ordinary application\n// repository, which is a README and little more — it reports an empty result\n// and reads nothing, root documents included. These rules judge the text this\n// project writes, not the prose of whoever happened to run the command.\n//\n// This module is the single source of truth for that scope: which files are\n// read, which directories are excluded, and the caps. `noir doctor` (via\n// `checkOutputHygiene` in ./commands/doctor.ts) and the CI gate\n// (scripts/hygiene-gate.mjs) both call `scanOutputHygiene`, so the two cannot\n// drift apart.\n\nimport { type Dirent, readdirSync, readFileSync, statSync } from 'node:fs';\nimport { join } from 'node:path';\nimport {\n checkHygiene,\n type HygieneKind,\n type HygieneTier,\n hasHygieneExemption,\n humanizerFindings,\n slopFindings,\n} from '@noir-ai/skills';\n\n/** How many findings the detail cell names before it reports the rest as a\n * count. A tree that has drifted can carry hundreds, and the row is a signal\n * to act on, not an inventory; the counts still cover every finding. */\nexport const HYGIENE_FINDING_CAP = 5;\n\n/** The largest file the scan reads. A file this big is generated data or a\n * bundle rather than the source and prose the rules are written for. */\nexport const HYGIENE_MAX_FILE_BYTES = 512 * 1024;\n\n/** The most files one scan reads, so the check stays responsive in a tree far\n * larger than this repository's. Reaching it is reported in the row. */\nexport const HYGIENE_MAX_FILES = 2000;\n\n/** The source extensions the check reads. Anything else is skipped: the rules\n * are written for comments and for prose, and a data format (JSON, YAML) has\n * no comment for them to read, while a binary fixture would only produce\n * nonsense. */\nconst HYGIENE_CODE_EXTENSIONS = /\\.(?:ts|tsx|js|jsx|mjs|cjs|sh)$/i;\n\n/** The document extensions the check reads as prose. */\nconst HYGIENE_MARKDOWN_EXTENSIONS = /\\.(?:md|mdx)$/i;\n\n/** Directories the walk never descends into: dependencies, build output and\n * coverage. A dot-directory is skipped too (`.git`, `.noir`, local scratch\n * directories), which is how the maintainer's exclusion of session scratch is\n * honoured by construction rather than by naming it here. */\nconst HYGIENE_SKIP_DIRS = new Set(['node_modules', 'dist', 'coverage']);\n\n/** Directories excluded from the scan by the maintainer's decision: the\n * planning corpus, where decisions are recorded in the maintainer's own\n * shorthand. Root-relative POSIX paths. */\nconst HYGIENE_EXCLUDED_DIRS = new Set(['docs/internal', 'docs/decisions', 'docs/roadmap']);\n\n/** The release log, excluded by basename wherever it sits: the root copy is the\n * single source of truth and `docs/CHANGELOG.md` is a pointer to it. */\nconst HYGIENE_EXCLUDED_FILES = new Set(['CHANGELOG.md']);\n\n/** A file the scan will read, with the kind of text it holds. */\ninterface HygieneScanFile {\n /** Root-relative POSIX path. */\n path: string;\n kind: HygieneKind;\n}\n\n/** One finding, reduced to what the report needs. */\nexport interface HygieneScanFinding {\n path: string;\n /** 1-based line number. */\n line: number;\n /** The rule id, exactly as the rule table declares it. */\n id: string;\n tier: HygieneTier;\n}\n\nexport interface HygieneScanResult {\n /** Files read. */\n scanned: number;\n /** Files the scan did not read because they exceed the size cap. */\n skipped: number;\n /** True when the file cap stopped the scan before it read the whole tree, so\n * the counts below describe a prefix of it. */\n truncated: boolean;\n /** Every finding, failures first, then in reading order across the tree. */\n findings: HygieneScanFinding[];\n fail: number;\n warn: number;\n}\n\n/** A walk in progress: the files it has accepted, and whether the file cap has\n * stopped it. */\ninterface HygieneWalk {\n files: HygieneScanFile[];\n truncated: boolean;\n}\n\n/** `readdirSync` entries, or none when the directory does not exist. */\nfunction readDirEntries(dir: string): Dirent[] {\n try {\n return readdirSync(dir, { withFileTypes: true });\n } catch {\n return []; // absent or unreadable — there is nothing to scan\n }\n}\n\n/** Whether `path` is an existing directory. */\nfunction isDirectory(path: string): boolean {\n try {\n return statSync(path).isDirectory();\n } catch {\n return false;\n }\n}\n\n/** The kind of text a path holds, or `null` when the rules cannot read it. */\nfunction hygieneKindOf(path: string): HygieneKind | null {\n if (HYGIENE_MARKDOWN_EXTENSIONS.test(path)) return 'markdown';\n if (HYGIENE_CODE_EXTENSIONS.test(path)) return 'code';\n return null;\n}\n\n/** The layout this check exists for: a package source or test tree, `scripts/`,\n * or a documentation tree. A repository that has none of them is not the\n * audience of these rules, and nothing is read there. */\nfunction hasScannableLayout(root: string): boolean {\n if (isDirectory(join(root, 'docs')) || isDirectory(join(root, 'scripts'))) return true;\n for (const entry of readDirEntries(join(root, 'packages'))) {\n if (!entry.isDirectory()) continue;\n if (isDirectory(join(root, 'packages', entry.name, 'src'))) return true;\n if (isDirectory(join(root, 'packages', entry.name, 'test'))) return true;\n }\n return false;\n}\n\n/** Adds `rel` to the walk when it is a file the rules can read and the walk has\n * room for it. A dot-file is never read (a dot-directory is skipped by the\n * walk itself), and once the file cap is reached the walk records the\n * truncation instead of adding more. */\nfunction addHygieneFile(rel: string, walk: HygieneWalk): void {\n const name = rel.slice(rel.lastIndexOf('/') + 1);\n if (name.startsWith('.') || HYGIENE_EXCLUDED_FILES.has(name)) return;\n const kind = hygieneKindOf(rel);\n if (kind === null) return;\n if (walk.files.length >= HYGIENE_MAX_FILES) {\n walk.truncated = true;\n return;\n }\n walk.files.push({ path: rel, kind });\n}\n\n/** Offers every readable file under `root`/`relDir` to the walk. Symlinked\n * directories are not followed: a link out of the tree is not the\n * repository's own source, and a link to an ancestor would not terminate. */\nfunction walkScannable(\n root: string,\n relDir: string,\n skip: ReadonlySet<string>,\n walk: HygieneWalk,\n): void {\n if (walk.truncated) return;\n for (const entry of readDirEntries(join(root, relDir))) {\n if (walk.truncated) return;\n const rel = `${relDir}/${entry.name}`;\n if (entry.isDirectory()) {\n if (HYGIENE_SKIP_DIRS.has(entry.name) || entry.name.startsWith('.') || skip.has(rel)) {\n continue;\n }\n walkScannable(root, rel, skip, walk);\n } else if (entry.isFile()) {\n addHygieneFile(rel, walk);\n }\n }\n}\n\nconst NO_SKIP_DIRS: ReadonlySet<string> = new Set();\n\n/** The files the check reads: documents at the root and under `docs/` (minus\n * the planning corpus), the repository-authored agent skills, each package's\n * sources and tests, and `scripts/`. Sorted by path so a report is the same on\n * every run. Called only where {@link hasScannableLayout} holds. */\nfunction collectHygieneFiles(root: string): HygieneWalk {\n const walk: HygieneWalk = { files: [], truncated: false };\n for (const entry of readDirEntries(root)) {\n if (entry.isFile()) addHygieneFile(entry.name, walk);\n }\n walkScannable(root, 'docs', HYGIENE_EXCLUDED_DIRS, walk);\n walkScannable(root, '.claude/skills', NO_SKIP_DIRS, walk);\n for (const entry of readDirEntries(join(root, 'packages'))) {\n if (!entry.isDirectory()) continue;\n walkScannable(root, `packages/${entry.name}/src`, NO_SKIP_DIRS, walk);\n walkScannable(root, `packages/${entry.name}/test`, NO_SKIP_DIRS, walk);\n }\n walkScannable(root, 'scripts', NO_SKIP_DIRS, walk);\n walk.files.sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));\n return walk;\n}\n\n/** What the scan left out, in one parenthesis. Empty when it left out nothing,\n * so a complete report carries no note. */\nfunction hygieneOmissions(result: HygieneScanResult): string {\n const notes: string[] = [];\n if (result.skipped > 0) {\n notes.push(\n `${result.skipped} file${result.skipped === 1 ? '' : 's'} over ${HYGIENE_MAX_FILE_BYTES / 1024} KiB skipped`,\n );\n }\n if (result.truncated) notes.push(`stopped at ${HYGIENE_MAX_FILES} files`);\n return notes.length > 0 ? ` (${notes.join('; ')})` : '';\n}\n\n/** The counts line: how many findings of each tier, in how many files, and what\n * the scan itself omitted. It names no location, so a caller that prints every\n * finding on a line of its own pairs this with those lines instead of printing\n * the same findings twice. */\nexport function hygieneCounts(result: HygieneScanResult): string {\n const omitted = hygieneOmissions(result);\n if (result.scanned === 0) {\n return `nothing to scan (no packages/*/src, packages/*/test, scripts/ or documents found)${omitted}`;\n }\n if (result.findings.length === 0) {\n return `clean — ${result.scanned} file${result.scanned === 1 ? '' : 's'} scanned${omitted}`;\n }\n const files = new Set(result.findings.map((f) => f.path)).size;\n return `${result.fail} fail, ${result.warn} warn in ${files} file${files === 1 ? '' : 's'}${omitted}`;\n}\n\n/** The counts line followed by the locations that carry them, up to the cap.\n * Failures are named before warnings, so a truncated list still shows what\n * blocks. This is the doctor row, which has one line to work with. */\nexport function hygieneDetail(result: HygieneScanResult): string {\n const counts = hygieneCounts(result);\n if (result.scanned === 0 || result.findings.length === 0) return counts;\n const named = result.findings.slice(0, HYGIENE_FINDING_CAP);\n const hidden = result.findings.length - named.length;\n const where = named.map((f) => `${f.path}:${f.line} ${f.id}`).join('; ');\n return `${counts} — ${where}${hidden > 0 ? ` (+${hidden} more)` : ''}`;\n}\n\n/**\n * Reads the repository's own source and documents through `checkHygiene` and\n * returns the scan result: the files read, the findings (failures first), and\n * the fail/warn counts. It pushes no doctor row — that is the caller's\n * business (`checkOutputHygiene` in the doctor command, or the CI gate's own\n * report).\n *\n * Nothing is read unless {@link hasScannableLayout} holds, so a repository\n * without the layout gets an empty result and pays one `stat` per candidate.\n *\n * Each file is read once, and a file that carries the exemption marker comes\n * back from `checkHygiene` with no findings at all, so the marker costs one\n * scan of a file's text rather than a path list kept here. Files past the size\n * cap and files past the count cap are left out; the result says so.\n *\n * The scan never throws and never writes: an unreadable file is skipped, and\n * the commands that repair the tree (`noir skills lint`, the repository's own\n * sweep) are the caller's business.\n */\nexport function scanOutputHygiene(root: string): HygieneScanResult {\n const empty: HygieneScanResult = {\n scanned: 0,\n skipped: 0,\n truncated: false,\n findings: [],\n fail: 0,\n warn: 0,\n };\n if (!hasScannableLayout(root)) return empty;\n const walk = collectHygieneFiles(root);\n const findings: HygieneScanFinding[] = [];\n let scanned = 0;\n let skipped = 0;\n for (const file of walk.files) {\n const abs = join(root, file.path);\n try {\n if (statSync(abs).size > HYGIENE_MAX_FILE_BYTES) {\n skipped++;\n continue;\n }\n const text = readFileSync(abs, 'utf8');\n scanned++;\n for (const found of checkHygiene(text, file.kind)) {\n findings.push({ path: file.path, line: found.line, id: found.id, tier: found.tier });\n }\n if (file.kind === 'markdown' && !hasHygieneExemption(text, file.kind)) {\n for (const found of [...slopFindings(text), ...humanizerFindings(text)]) {\n findings.push({ path: file.path, line: found.line, id: found.id, tier: found.tier });\n }\n }\n } catch {\n // An unreadable path says nothing about hygiene — the store check owns\n // broken files — so the scan moves on to the next one.\n }\n }\n findings.sort((a, b) => {\n if (a.tier !== b.tier) return a.tier === 'fail' ? -1 : 1;\n if (a.path !== b.path) return a.path < b.path ? -1 : 1;\n return a.line - b.line;\n });\n const fail = findings.filter((f) => f.tier === 'fail').length;\n return {\n scanned,\n skipped,\n truncated: walk.truncated,\n findings,\n fail,\n warn: findings.length - fail,\n };\n}\n"],"mappings":"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","names":[]}
|
package/dist/index.js
CHANGED
|
@@ -6224,12 +6224,13 @@ var init_skills = __esm({
|
|
|
6224
6224
|
"noir-recall": "memory",
|
|
6225
6225
|
"noir-remember": "memory",
|
|
6226
6226
|
"noir-context": "context",
|
|
6227
|
-
"noir-frontend": "domain",
|
|
6228
6227
|
"noir-backend": "domain",
|
|
6228
|
+
"noir-design": "domain",
|
|
6229
|
+
"noir-design-reference": "domain",
|
|
6229
6230
|
"noir-doctor": "meta",
|
|
6230
6231
|
"noir-writing-skills": "meta",
|
|
6231
6232
|
"noir-rules": "meta",
|
|
6232
|
-
"noir-
|
|
6233
|
+
"noir-codebase-audit": "meta"
|
|
6233
6234
|
};
|
|
6234
6235
|
}
|
|
6235
6236
|
});
|
|
@@ -7627,7 +7628,12 @@ var init_daemon_client = __esm({
|
|
|
7627
7628
|
// src/hygiene-scan.ts
|
|
7628
7629
|
import { readdirSync as readdirSync2, readFileSync as readFileSync4, statSync } from "fs";
|
|
7629
7630
|
import { join as join5 } from "path";
|
|
7630
|
-
import {
|
|
7631
|
+
import {
|
|
7632
|
+
checkHygiene,
|
|
7633
|
+
hasHygieneExemption,
|
|
7634
|
+
humanizerFindings,
|
|
7635
|
+
slopFindings
|
|
7636
|
+
} from "@noir-ai/skills";
|
|
7631
7637
|
function readDirEntries(dir) {
|
|
7632
7638
|
try {
|
|
7633
7639
|
return readdirSync2(dir, { withFileTypes: true });
|
|
@@ -7753,6 +7759,11 @@ function scanOutputHygiene(root) {
|
|
|
7753
7759
|
for (const found of checkHygiene(text, file.kind)) {
|
|
7754
7760
|
findings.push({ path: file.path, line: found.line, id: found.id, tier: found.tier });
|
|
7755
7761
|
}
|
|
7762
|
+
if (file.kind === "markdown" && !hasHygieneExemption(text, file.kind)) {
|
|
7763
|
+
for (const found of [...slopFindings(text), ...humanizerFindings(text)]) {
|
|
7764
|
+
findings.push({ path: file.path, line: found.line, id: found.id, tier: found.tier });
|
|
7765
|
+
}
|
|
7766
|
+
}
|
|
7756
7767
|
} catch {
|
|
7757
7768
|
}
|
|
7758
7769
|
}
|