pasika 0.3.3 → 0.3.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/README.md +13 -17
- package/dist/cli/index.d.ts +0 -1
- package/dist/cli/index.js +4999 -103
- package/dist/eslint/pasika/index.d.ts +165 -53
- package/dist/eslint/pasika/index.js +4424 -89
- package/package.json +3 -3
- package/dist/cli/output.d.ts +0 -3
- package/dist/cli/output.js +0 -11
- package/dist/enforcement/coverage.d.ts +0 -55
- package/dist/enforcement/coverage.js +0 -239
- package/dist/enforcement/doctor.d.ts +0 -16
- package/dist/enforcement/doctor.js +0 -249
- package/dist/enforcement/normalize.d.ts +0 -11
- package/dist/enforcement/normalize.js +0 -21
- package/dist/enforcement/parse-docs.d.ts +0 -58
- package/dist/enforcement/parse-docs.js +0 -98
- package/dist/enforcement/types.d.ts +0 -54
- package/dist/enforcement/types.js +0 -50
- package/dist/eslint/pasika/ast-types.d.ts +0 -122
- package/dist/eslint/pasika/ast-types.js +0 -1
- package/dist/eslint/pasika/project/ccf.d.ts +0 -48
- package/dist/eslint/pasika/project/ccf.js +0 -119
- package/dist/eslint/pasika/project/index.d.ts +0 -21
- package/dist/eslint/pasika/project/index.js +0 -139
- package/dist/eslint/pasika/project/parse-module.d.ts +0 -27
- package/dist/eslint/pasika/project/parse-module.js +0 -128
- package/dist/eslint/pasika/rules/application-structure.d.ts +0 -2
- package/dist/eslint/pasika/rules/application-structure.js +0 -152
- package/dist/eslint/pasika/rules/component-conventions.d.ts +0 -16
- package/dist/eslint/pasika/rules/component-conventions.js +0 -145
- package/dist/eslint/pasika/rules/component-nesting.d.ts +0 -12
- package/dist/eslint/pasika/rules/component-nesting.js +0 -69
- package/dist/eslint/pasika/rules/component-placement.d.ts +0 -11
- package/dist/eslint/pasika/rules/component-placement.js +0 -75
- package/dist/eslint/pasika/rules/config-extraction.d.ts +0 -14
- package/dist/eslint/pasika/rules/config-extraction.js +0 -74
- package/dist/eslint/pasika/rules/cross-feature-import.d.ts +0 -2
- package/dist/eslint/pasika/rules/cross-feature-import.js +0 -75
- package/dist/eslint/pasika/rules/css/apply-usage.d.ts +0 -10
- package/dist/eslint/pasika/rules/css/apply-usage.js +0 -45
- package/dist/eslint/pasika/rules/css/base-layer-pair.d.ts +0 -10
- package/dist/eslint/pasika/rules/css/base-layer-pair.js +0 -49
- package/dist/eslint/pasika/rules/css/css-variable-naming.d.ts +0 -10
- package/dist/eslint/pasika/rules/css/css-variable-naming.js +0 -39
- package/dist/eslint/pasika/rules/css/custom-utility-apply.d.ts +0 -11
- package/dist/eslint/pasika/rules/css/custom-utility-apply.js +0 -52
- package/dist/eslint/pasika/rules/css/global-css-location.d.ts +0 -10
- package/dist/eslint/pasika/rules/css/global-css-location.js +0 -42
- package/dist/eslint/pasika/rules/css/helpers.d.ts +0 -29
- package/dist/eslint/pasika/rules/css/helpers.js +0 -93
- package/dist/eslint/pasika/rules/css/index.d.ts +0 -13
- package/dist/eslint/pasika/rules/css/index.js +0 -25
- package/dist/eslint/pasika/rules/css/root-variables.d.ts +0 -11
- package/dist/eslint/pasika/rules/css/root-variables.js +0 -53
- package/dist/eslint/pasika/rules/css/rule-tester.d.ts +0 -11
- package/dist/eslint/pasika/rules/css/rule-tester.js +0 -22
- package/dist/eslint/pasika/rules/css/stylesheet-ordering.d.ts +0 -11
- package/dist/eslint/pasika/rules/css/stylesheet-ordering.js +0 -76
- package/dist/eslint/pasika/rules/css/surface-utility.d.ts +0 -10
- package/dist/eslint/pasika/rules/css/surface-utility.js +0 -41
- package/dist/eslint/pasika/rules/css/theme-reset.d.ts +0 -9
- package/dist/eslint/pasika/rules/css/theme-reset.js +0 -36
- package/dist/eslint/pasika/rules/css/theme-variable-namespace.d.ts +0 -11
- package/dist/eslint/pasika/rules/css/theme-variable-namespace.js +0 -53
- package/dist/eslint/pasika/rules/cva-appearance-props.d.ts +0 -10
- package/dist/eslint/pasika/rules/cva-appearance-props.js +0 -77
- package/dist/eslint/pasika/rules/cva-boolean-variants.d.ts +0 -11
- package/dist/eslint/pasika/rules/cva-boolean-variants.js +0 -97
- package/dist/eslint/pasika/rules/data-testid-case.d.ts +0 -2
- package/dist/eslint/pasika/rules/data-testid-case.js +0 -76
- package/dist/eslint/pasika/rules/enforce-barrel-exports.d.ts +0 -9
- package/dist/eslint/pasika/rules/enforce-barrel-exports.js +0 -78
- package/dist/eslint/pasika/rules/enforce-cn-merge.d.ts +0 -21
- package/dist/eslint/pasika/rules/enforce-cn-merge.js +0 -86
- package/dist/eslint/pasika/rules/enforce-cva-variant-props.d.ts +0 -9
- package/dist/eslint/pasika/rules/enforce-cva-variant-props.js +0 -86
- package/dist/eslint/pasika/rules/filename-case.d.ts +0 -2
- package/dist/eslint/pasika/rules/filename-case.js +0 -123
- package/dist/eslint/pasika/rules/hook-complexity.d.ts +0 -12
- package/dist/eslint/pasika/rules/hook-complexity.js +0 -114
- package/dist/eslint/pasika/rules/hook-extraction.d.ts +0 -11
- package/dist/eslint/pasika/rules/hook-extraction.js +0 -55
- package/dist/eslint/pasika/rules/import-boundaries.d.ts +0 -2
- package/dist/eslint/pasika/rules/import-boundaries.js +0 -167
- package/dist/eslint/pasika/rules/import-through-index.d.ts +0 -2
- package/dist/eslint/pasika/rules/import-through-index.js +0 -52
- package/dist/eslint/pasika/rules/interactive-component.d.ts +0 -10
- package/dist/eslint/pasika/rules/interactive-component.js +0 -76
- package/dist/eslint/pasika/rules/json/index.d.ts +0 -5
- package/dist/eslint/pasika/rules/json/index.js +0 -9
- package/dist/eslint/pasika/rules/json/no-cache-flag.d.ts +0 -10
- package/dist/eslint/pasika/rules/json/no-cache-flag.js +0 -45
- package/dist/eslint/pasika/rules/json/no-vulyk-dependency.d.ts +0 -10
- package/dist/eslint/pasika/rules/json/no-vulyk-dependency.js +0 -41
- package/dist/eslint/pasika/rules/json/rule-tester.d.ts +0 -11
- package/dist/eslint/pasika/rules/json/rule-tester.js +0 -21
- package/dist/eslint/pasika/rules/jsx-hygiene.d.ts +0 -9
- package/dist/eslint/pasika/rules/jsx-hygiene.js +0 -108
- package/dist/eslint/pasika/rules/locale-dotted-path.d.ts +0 -10
- package/dist/eslint/pasika/rules/locale-dotted-path.js +0 -65
- package/dist/eslint/pasika/rules/locale-placement.d.ts +0 -13
- package/dist/eslint/pasika/rules/locale-placement.js +0 -163
- package/dist/eslint/pasika/rules/locales-location.d.ts +0 -11
- package/dist/eslint/pasika/rules/locales-location.js +0 -56
- package/dist/eslint/pasika/rules/md/doc-kind-suffix.d.ts +0 -5
- package/dist/eslint/pasika/rules/md/doc-kind-suffix.js +0 -29
- package/dist/eslint/pasika/rules/md/example-heading-description.d.ts +0 -5
- package/dist/eslint/pasika/rules/md/example-heading-description.js +0 -30
- package/dist/eslint/pasika/rules/md/glossary-term-linking.d.ts +0 -2
- package/dist/eslint/pasika/rules/md/glossary-term-linking.js +0 -99
- package/dist/eslint/pasika/rules/md/guide-folder-entry-point.d.ts +0 -2
- package/dist/eslint/pasika/rules/md/guide-folder-entry-point.js +0 -46
- package/dist/eslint/pasika/rules/md/guide-link-anchors.d.ts +0 -5
- package/dist/eslint/pasika/rules/md/guide-link-anchors.js +0 -50
- package/dist/eslint/pasika/rules/md/guide-overview-no-links.d.ts +0 -5
- package/dist/eslint/pasika/rules/md/guide-overview-no-links.js +0 -51
- package/dist/eslint/pasika/rules/md/guide-states-no-requirement.d.ts +0 -5
- package/dist/eslint/pasika/rules/md/guide-states-no-requirement.js +0 -26
- package/dist/eslint/pasika/rules/md/guide-step-single-link.d.ts +0 -5
- package/dist/eslint/pasika/rules/md/guide-step-single-link.js +0 -48
- package/dist/eslint/pasika/rules/md/guide-step-single-sentence.d.ts +0 -5
- package/dist/eslint/pasika/rules/md/guide-step-single-sentence.js +0 -42
- package/dist/eslint/pasika/rules/md/helpers.d.ts +0 -25
- package/dist/eslint/pasika/rules/md/helpers.js +0 -37
- package/dist/eslint/pasika/rules/md/index.d.ts +0 -27
- package/dist/eslint/pasika/rules/md/index.js +0 -53
- package/dist/eslint/pasika/rules/md/no-cross-document-link.d.ts +0 -5
- package/dist/eslint/pasika/rules/md/no-cross-document-link.js +0 -36
- package/dist/eslint/pasika/rules/md/no-nested-how-to.d.ts +0 -5
- package/dist/eslint/pasika/rules/md/no-nested-how-to.js +0 -39
- package/dist/eslint/pasika/rules/md/no-template-prompt.d.ts +0 -5
- package/dist/eslint/pasika/rules/md/no-template-prompt.js +0 -22
- package/dist/eslint/pasika/rules/md/overview-length.d.ts +0 -5
- package/dist/eslint/pasika/rules/md/overview-length.js +0 -47
- package/dist/eslint/pasika/rules/md/overview-present.d.ts +0 -5
- package/dist/eslint/pasika/rules/md/overview-present.js +0 -41
- package/dist/eslint/pasika/rules/md/policy-no-examples.d.ts +0 -5
- package/dist/eslint/pasika/rules/md/policy-no-examples.js +0 -28
- package/dist/eslint/pasika/rules/md/policy-single-document.d.ts +0 -5
- package/dist/eslint/pasika/rules/md/policy-single-document.js +0 -34
- package/dist/eslint/pasika/rules/md/policy-subject-headings.d.ts +0 -5
- package/dist/eslint/pasika/rules/md/policy-subject-headings.js +0 -58
- package/dist/eslint/pasika/rules/md/project-index.d.ts +0 -17
- package/dist/eslint/pasika/rules/md/project-index.js +0 -67
- package/dist/eslint/pasika/rules/md/reference-block-headings.d.ts +0 -6
- package/dist/eslint/pasika/rules/md/reference-block-headings.js +0 -31
- package/dist/eslint/pasika/rules/md/reference-no-rfc-vocabulary.d.ts +0 -6
- package/dist/eslint/pasika/rules/md/reference-no-rfc-vocabulary.js +0 -26
- package/dist/eslint/pasika/rules/md/requirement-present.d.ts +0 -5
- package/dist/eslint/pasika/rules/md/requirement-present.js +0 -41
- package/dist/eslint/pasika/rules/md/rfc-only-in-bullets.d.ts +0 -5
- package/dist/eslint/pasika/rules/md/rfc-only-in-bullets.js +0 -46
- package/dist/eslint/pasika/rules/md/rule-paired-examples.d.ts +0 -5
- package/dist/eslint/pasika/rules/md/rule-paired-examples.js +0 -36
- package/dist/eslint/pasika/rules/md/rule-tester.d.ts +0 -11
- package/dist/eslint/pasika/rules/md/rule-tester.js +0 -21
- package/dist/eslint/pasika/rules/md/support-document-placement.d.ts +0 -2
- package/dist/eslint/pasika/rules/md/support-document-placement.js +0 -36
- package/dist/eslint/pasika/rules/md/title-matches-file-name.d.ts +0 -2
- package/dist/eslint/pasika/rules/md/title-matches-file-name.js +0 -46
- package/dist/eslint/pasika/rules/named-exports.d.ts +0 -2
- package/dist/eslint/pasika/rules/named-exports.js +0 -49
- package/dist/eslint/pasika/rules/no-arbitrary-tailwind.d.ts +0 -9
- package/dist/eslint/pasika/rules/no-arbitrary-tailwind.js +0 -97
- package/dist/eslint/pasika/rules/no-eslint-disable.d.ts +0 -10
- package/dist/eslint/pasika/rules/no-eslint-disable.js +0 -33
- package/dist/eslint/pasika/rules/no-mixed-concerns.d.ts +0 -9
- package/dist/eslint/pasika/rules/no-mixed-concerns.js +0 -71
- package/dist/eslint/pasika/rules/no-util-barrel.d.ts +0 -2
- package/dist/eslint/pasika/rules/no-util-barrel.js +0 -48
- package/dist/eslint/pasika/rules/pure-function-extract.d.ts +0 -9
- package/dist/eslint/pasika/rules/pure-function-extract.js +0 -92
- package/dist/eslint/pasika/rules/shared-style-dedup.d.ts +0 -13
- package/dist/eslint/pasika/rules/shared-style-dedup.js +0 -91
- package/dist/eslint/pasika/rules/stay-flat.d.ts +0 -12
- package/dist/eslint/pasika/rules/stay-flat.js +0 -81
- package/dist/eslint/pasika/rules/support-file-placement.d.ts +0 -15
- package/dist/eslint/pasika/rules/support-file-placement.js +0 -70
- package/dist/eslint/pasika/rules/support-folder-shape.d.ts +0 -2
- package/dist/eslint/pasika/rules/support-folder-shape.js +0 -59
- package/dist/eslint/pasika/rules/type-extraction.d.ts +0 -13
- package/dist/eslint/pasika/rules/type-extraction.js +0 -85
- package/dist/eslint/pasika/rules/ui-state.d.ts +0 -9
- package/dist/eslint/pasika/rules/ui-state.js +0 -99
- package/dist/eslint/pasika/rules/util-file-name.d.ts +0 -2
- package/dist/eslint/pasika/rules/util-file-name.js +0 -48
- package/dist/eslint/pasika/rules/value-extraction.d.ts +0 -13
- package/dist/eslint/pasika/rules/value-extraction.js +0 -48
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pasika",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.4",
|
|
4
4
|
"description": "Reusable agent setup package",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -22,9 +22,8 @@
|
|
|
22
22
|
"dist"
|
|
23
23
|
],
|
|
24
24
|
"scripts": {
|
|
25
|
-
"build": "
|
|
25
|
+
"build": "tsup",
|
|
26
26
|
"coverage": "tsx cli/index.ts coverage",
|
|
27
|
-
"docs": "eslint docs",
|
|
28
27
|
"fix": "eslint . --fix",
|
|
29
28
|
"lint": "eslint .",
|
|
30
29
|
"prepack": "npm run build",
|
|
@@ -49,6 +48,7 @@
|
|
|
49
48
|
"eslint": "^10.9.1",
|
|
50
49
|
"jiti": "^2.7.0",
|
|
51
50
|
"prettier": "^3.8.1",
|
|
51
|
+
"tsup": "^8.5.1",
|
|
52
52
|
"tsx": "^4.23.12",
|
|
53
53
|
"typescript": "^5.9.2",
|
|
54
54
|
"zirka": "^0.0.42"
|
package/dist/cli/output.d.ts
DELETED
package/dist/cli/output.js
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
/* eslint-disable no-console -- cli-reports-to-terminal: stdout is the CLI's output channel */
|
|
2
|
-
export function log(message) {
|
|
3
|
-
console.log(message);
|
|
4
|
-
}
|
|
5
|
-
export function error(message) {
|
|
6
|
-
console.error(message);
|
|
7
|
-
}
|
|
8
|
-
export function json(data) {
|
|
9
|
-
console.log(JSON.stringify(data, undefined, 2));
|
|
10
|
-
}
|
|
11
|
-
/* eslint-enable no-console -- re-enable after CLI output block */
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
import { type EnforcementKind, type Registry, type Requirement } from "./types.js";
|
|
2
|
-
export interface CoverageIssue {
|
|
3
|
-
kind: "new" | "changed" | "removed" | "unknown-ref" | "missing-test";
|
|
4
|
-
doc: string;
|
|
5
|
-
line?: number;
|
|
6
|
-
text: string;
|
|
7
|
-
/** Hash of the requirement as it reads now, so a new entry can be recorded without recomputing it. */
|
|
8
|
-
hash?: string;
|
|
9
|
-
detail?: string;
|
|
10
|
-
}
|
|
11
|
-
export interface CoverageReport {
|
|
12
|
-
counts: Record<EnforcementKind, number>;
|
|
13
|
-
total: number;
|
|
14
|
-
mechanical: number;
|
|
15
|
-
issues: CoverageIssue[];
|
|
16
|
-
/** Registry updated for `--accept`: reworded requirements rehashed, removed ones dropped. */
|
|
17
|
-
nextRegistry: Registry;
|
|
18
|
-
}
|
|
19
|
-
export declare function buildCoverageReport(options: {
|
|
20
|
-
docsRoot: string;
|
|
21
|
-
registry: Registry;
|
|
22
|
-
rulesDir: string;
|
|
23
|
-
}): CoverageReport;
|
|
24
|
-
export interface ClassifyInput {
|
|
25
|
-
/** Hash of the requirement, as `coverage` prints it. */
|
|
26
|
-
hash: string;
|
|
27
|
-
kind: EnforcementKind;
|
|
28
|
-
ref?: string;
|
|
29
|
-
note?: string;
|
|
30
|
-
}
|
|
31
|
-
export interface ClassifyResult {
|
|
32
|
-
registry: Registry;
|
|
33
|
-
requirement: Requirement;
|
|
34
|
-
/** The kind this requirement carried before, when it was already classified. */
|
|
35
|
-
previousKind?: EnforcementKind;
|
|
36
|
-
}
|
|
37
|
-
/**
|
|
38
|
-
* Records how a requirement is checked.
|
|
39
|
-
*
|
|
40
|
-
* Validates that the requirement exists in the documentation as written, that a
|
|
41
|
-
* ref names a check that exists, and that the kinds which are meaningless
|
|
42
|
-
* without an explanation carry one. Throws with a readable message otherwise, so
|
|
43
|
-
* the caller can print it and exit.
|
|
44
|
-
*/
|
|
45
|
-
export declare function classifyRequirement(options: {
|
|
46
|
-
docsRoot: string;
|
|
47
|
-
registry: Registry;
|
|
48
|
-
input: ClassifyInput;
|
|
49
|
-
}): ClassifyResult;
|
|
50
|
-
export declare function readRegistry(registryPath: string): Registry;
|
|
51
|
-
/**
|
|
52
|
-
* Writes the registry sorted in the same order the requirements appear in the
|
|
53
|
-
* docs: document order, then line order within each document.
|
|
54
|
-
*/
|
|
55
|
-
export declare function writeRegistry(registryPath: string, registry: Registry, docsRoot: string): void;
|
|
@@ -1,239 +0,0 @@
|
|
|
1
|
-
import { readdirSync, readFileSync, writeFileSync, statSync } from "node:fs";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import { z } from "zod";
|
|
4
|
-
import { allPasikaRuleIds } from "../eslint/pasika/index.js";
|
|
5
|
-
import { normalizeRequirement } from "./normalize.js";
|
|
6
|
-
import { parseDocs } from "./parse-docs.js";
|
|
7
|
-
import { MECHANICAL_KINDS, registrySchema } from "./types.js";
|
|
8
|
-
/** Ratio of shared words, used only to guess which stored requirement a reworded bullet came from. */
|
|
9
|
-
function similarity(left, right) {
|
|
10
|
-
const leftWords = new Set(left.toLowerCase().split(/\W+/).filter(Boolean));
|
|
11
|
-
const rightWords = new Set(right.toLowerCase().split(/\W+/).filter(Boolean));
|
|
12
|
-
const shared = [...leftWords].filter((word) => rightWords.has(word)).length;
|
|
13
|
-
const union = new Set([...leftWords, ...rightWords]).size;
|
|
14
|
-
return union === 0 ? 0 : shared / union;
|
|
15
|
-
}
|
|
16
|
-
/** Titles passed to `describe` or `test` in the rule test files, recursing into subfolders. */
|
|
17
|
-
function collectTestTitles(rulesDir) {
|
|
18
|
-
const titles = new Set();
|
|
19
|
-
const visit = (dir) => {
|
|
20
|
-
for (const entry of readdirSync(dir)) {
|
|
21
|
-
const entryPath = path.join(dir, entry);
|
|
22
|
-
if (statSync(entryPath).isDirectory()) {
|
|
23
|
-
visit(entryPath);
|
|
24
|
-
continue;
|
|
25
|
-
}
|
|
26
|
-
if (!entry.endsWith(".test.ts"))
|
|
27
|
-
continue;
|
|
28
|
-
const body = readFileSync(entryPath, "utf8");
|
|
29
|
-
const titlePattern = /\b(?:describe|test|it)\(\s*(?:"(?<double>(?:[^"\\]|\\.)*)"|'(?<single>(?:[^'\\]|\\.)*)')/g;
|
|
30
|
-
for (const match of body.matchAll(titlePattern)) {
|
|
31
|
-
const title = match.groups?.double ?? match.groups?.single ?? "";
|
|
32
|
-
titles.add(title.replaceAll('\\\\"', '"').replaceAll("\\\\'", "'"));
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
};
|
|
36
|
-
visit(rulesDir);
|
|
37
|
-
return titles;
|
|
38
|
-
}
|
|
39
|
-
/** A requirement several checks cover lists them comma-separated. */
|
|
40
|
-
function refParts(ref) {
|
|
41
|
-
return (ref ?? "")
|
|
42
|
-
.split(",")
|
|
43
|
-
.map((part) => part.trim())
|
|
44
|
-
.filter(Boolean);
|
|
45
|
-
}
|
|
46
|
-
function isRefKnown(requirement) {
|
|
47
|
-
const parts = refParts(requirement.ref);
|
|
48
|
-
if (requirement.kind === "eslint")
|
|
49
|
-
return parts.length > 0 && parts.every((part) => allPasikaRuleIds.includes(part));
|
|
50
|
-
// Doctor checks do not exist yet, so a `doctor` entry is a forward reference.
|
|
51
|
-
return true;
|
|
52
|
-
}
|
|
53
|
-
export function buildCoverageReport(options) {
|
|
54
|
-
const { docsRoot, registry, rulesDir } = options;
|
|
55
|
-
const docs = parseDocs(docsRoot);
|
|
56
|
-
const testTitles = collectTestTitles(rulesDir);
|
|
57
|
-
const byHash = new Map(registry.requirements.map((requirement) => [requirement.hash, requirement]));
|
|
58
|
-
const matched = new Set();
|
|
59
|
-
const issues = [];
|
|
60
|
-
const counts = {
|
|
61
|
-
eslint: 0,
|
|
62
|
-
doctor: 0,
|
|
63
|
-
planned: 0,
|
|
64
|
-
judgment: 0,
|
|
65
|
-
permission: 0,
|
|
66
|
-
};
|
|
67
|
-
const nextRequirements = [];
|
|
68
|
-
const parsed = docs.flatMap((doc) => doc.requirements.map((requirement) => ({ doc: doc.doc, requirement })));
|
|
69
|
-
for (const { doc, requirement } of parsed) {
|
|
70
|
-
const recorded = byHash.get(requirement.hash);
|
|
71
|
-
if (recorded) {
|
|
72
|
-
matched.add(requirement.hash);
|
|
73
|
-
counts[recorded.kind] += 1;
|
|
74
|
-
nextRequirements.push({ ...recorded, doc, text: requirement.raw });
|
|
75
|
-
if (!isRefKnown(recorded)) {
|
|
76
|
-
issues.push({
|
|
77
|
-
kind: "unknown-ref",
|
|
78
|
-
doc,
|
|
79
|
-
line: requirement.line,
|
|
80
|
-
text: requirement.raw,
|
|
81
|
-
detail: `${recorded.kind} ref "${recorded.ref ?? "(none)"}" does not exist`,
|
|
82
|
-
});
|
|
83
|
-
}
|
|
84
|
-
if (recorded.kind === "eslint" && !testTitles.has(requirement.text)) {
|
|
85
|
-
issues.push({
|
|
86
|
-
kind: "missing-test",
|
|
87
|
-
doc,
|
|
88
|
-
line: requirement.line,
|
|
89
|
-
text: requirement.raw,
|
|
90
|
-
detail: `no rule test is titled with this requirement`,
|
|
91
|
-
});
|
|
92
|
-
}
|
|
93
|
-
continue;
|
|
94
|
-
}
|
|
95
|
-
// Not recorded under this hash: either a reworded requirement or a new one.
|
|
96
|
-
const candidate = registry.requirements
|
|
97
|
-
.filter((entry) => entry.doc === doc && !matched.has(entry.hash))
|
|
98
|
-
.map((entry) => ({ entry, score: similarity(normalizeRequirement(entry.text), requirement.text) }))
|
|
99
|
-
.sort((left, right) => right.score - left.score)
|
|
100
|
-
.find(({ score }) => score >= 0.5);
|
|
101
|
-
if (candidate) {
|
|
102
|
-
matched.add(candidate.entry.hash);
|
|
103
|
-
counts[candidate.entry.kind] += 1;
|
|
104
|
-
nextRequirements.push({ ...candidate.entry, doc, text: requirement.raw, hash: requirement.hash });
|
|
105
|
-
issues.push({
|
|
106
|
-
kind: "changed",
|
|
107
|
-
doc,
|
|
108
|
-
line: requirement.line,
|
|
109
|
-
text: requirement.raw,
|
|
110
|
-
hash: requirement.hash,
|
|
111
|
-
detail: `was "${candidate.entry.text}" — re-verify ${candidate.entry.kind}${candidate.entry.ref ? ` ${candidate.entry.ref}` : ""}`,
|
|
112
|
-
});
|
|
113
|
-
}
|
|
114
|
-
else {
|
|
115
|
-
issues.push({ kind: "new", doc, line: requirement.line, text: requirement.raw, hash: requirement.hash });
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
for (const entry of registry.requirements) {
|
|
119
|
-
if (matched.has(entry.hash))
|
|
120
|
-
continue;
|
|
121
|
-
issues.push({
|
|
122
|
-
kind: "removed",
|
|
123
|
-
doc: entry.doc,
|
|
124
|
-
text: entry.text,
|
|
125
|
-
detail: `recorded as ${entry.kind}${entry.ref ? ` ${entry.ref}` : ""} but the bullet is gone`,
|
|
126
|
-
});
|
|
127
|
-
}
|
|
128
|
-
const total = parsed.length;
|
|
129
|
-
let mechanical = 0;
|
|
130
|
-
for (const kind of MECHANICAL_KINDS)
|
|
131
|
-
mechanical += counts[kind];
|
|
132
|
-
return {
|
|
133
|
-
counts,
|
|
134
|
-
total,
|
|
135
|
-
mechanical,
|
|
136
|
-
issues,
|
|
137
|
-
nextRegistry: { requirements: nextRequirements },
|
|
138
|
-
};
|
|
139
|
-
}
|
|
140
|
-
/**
|
|
141
|
-
* Records how a requirement is checked.
|
|
142
|
-
*
|
|
143
|
-
* Validates that the requirement exists in the documentation as written, that a
|
|
144
|
-
* ref names a check that exists, and that the kinds which are meaningless
|
|
145
|
-
* without an explanation carry one. Throws with a readable message otherwise, so
|
|
146
|
-
* the caller can print it and exit.
|
|
147
|
-
*/
|
|
148
|
-
export function classifyRequirement(options) {
|
|
149
|
-
const { docsRoot, registry, input } = options;
|
|
150
|
-
const parsed = parseDocs(docsRoot).flatMap((doc) => doc.requirements.map((requirement) => ({ doc: doc.doc, requirement })));
|
|
151
|
-
const match = parsed.find((entry) => entry.requirement.hash === input.hash);
|
|
152
|
-
if (!match) {
|
|
153
|
-
throw new Error(`No requirement in the documentation has hash "${input.hash}". Run coverage to list them.`);
|
|
154
|
-
}
|
|
155
|
-
const refs = refParts(input.ref);
|
|
156
|
-
if (input.kind === "eslint") {
|
|
157
|
-
if (refs.length === 0)
|
|
158
|
-
throw new Error('Kind "eslint" needs --ref naming the rule that reports it.');
|
|
159
|
-
const unknown = refs.filter((ref) => !allPasikaRuleIds.includes(ref));
|
|
160
|
-
if (unknown.length > 0) {
|
|
161
|
-
throw new Error(`--ref ${unknown.map((ref) => `"${ref}"`).join(", ")} is not a rule in the plugin.`);
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
else if (input.kind !== "doctor" && refs.length > 0) {
|
|
165
|
-
throw new Error(`Kind "${input.kind}" takes no --ref, because nothing reports it.`);
|
|
166
|
-
}
|
|
167
|
-
if ((input.kind === "judgment" || input.kind === "planned") && (input.note ?? "").trim() === "") {
|
|
168
|
-
const reason = input.kind === "judgment" ? "why no check can decide it" : "the check that should cover it";
|
|
169
|
-
throw new Error(`Kind "${input.kind}" needs --note naming ${reason}.`);
|
|
170
|
-
}
|
|
171
|
-
const requirement = {
|
|
172
|
-
doc: match.doc,
|
|
173
|
-
text: match.requirement.raw,
|
|
174
|
-
hash: match.requirement.hash,
|
|
175
|
-
kind: input.kind,
|
|
176
|
-
...(refs.length > 0 ? { ref: refs.join(", ") } : {}),
|
|
177
|
-
...((input.note ?? "").trim() === "" ? {} : { note: input.note?.trim() }),
|
|
178
|
-
};
|
|
179
|
-
const existing = registry.requirements.find((entry) => entry.hash === input.hash);
|
|
180
|
-
const requirements = registry.requirements.filter((entry) => entry.hash !== input.hash);
|
|
181
|
-
requirements.push(requirement);
|
|
182
|
-
return { registry: { requirements }, requirement, previousKind: existing?.kind };
|
|
183
|
-
}
|
|
184
|
-
export function readRegistry(registryPath) {
|
|
185
|
-
const parsed = JSON.parse(readFileSync(registryPath, "utf8"));
|
|
186
|
-
const result = registrySchema.safeParse(parsed);
|
|
187
|
-
if (!result.success) {
|
|
188
|
-
throw new Error(`${registryPath} is not a valid enforcement registry:\n${z.prettifyError(result.error)}`);
|
|
189
|
-
}
|
|
190
|
-
return result.data;
|
|
191
|
-
}
|
|
192
|
-
/**
|
|
193
|
-
* Sequence position of each requirement in the docs: doc order as `parseDocs`
|
|
194
|
-
* yields it, then line order within each doc. Entries whose hash no longer
|
|
195
|
-
* exists in the docs (removed, not yet accepted) sort after everything parsed.
|
|
196
|
-
*/
|
|
197
|
-
function buildDocOrder(docsRoot) {
|
|
198
|
-
const order = new Map();
|
|
199
|
-
let position = 0;
|
|
200
|
-
for (const doc of parseDocs(docsRoot)) {
|
|
201
|
-
for (const requirement of doc.requirements) {
|
|
202
|
-
order.set(requirement.hash, position);
|
|
203
|
-
position += 1;
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
return order;
|
|
207
|
-
}
|
|
208
|
-
/**
|
|
209
|
-
* Code-point order on doc then text. Deliberately not `localeCompare`, whose
|
|
210
|
-
* result depends on the host's locale data — a generated file that is committed
|
|
211
|
-
* has to sort the same way everywhere.
|
|
212
|
-
*/
|
|
213
|
-
function compareRequirements(left, right) {
|
|
214
|
-
if (left.doc !== right.doc)
|
|
215
|
-
return left.doc < right.doc ? -1 : 1;
|
|
216
|
-
if (left.text !== right.text)
|
|
217
|
-
return left.text < right.text ? -1 : 1;
|
|
218
|
-
return 0;
|
|
219
|
-
}
|
|
220
|
-
/**
|
|
221
|
-
* Writes the registry sorted in the same order the requirements appear in the
|
|
222
|
-
* docs: document order, then line order within each document.
|
|
223
|
-
*/
|
|
224
|
-
export function writeRegistry(registryPath, registry, docsRoot) {
|
|
225
|
-
const order = buildDocOrder(docsRoot);
|
|
226
|
-
const withPosition = registry.requirements.map((requirement) => ({
|
|
227
|
-
requirement,
|
|
228
|
-
position: order.get(requirement.hash) ?? Number.MAX_SAFE_INTEGER,
|
|
229
|
-
}));
|
|
230
|
-
withPosition.sort((left, right) => {
|
|
231
|
-
if (left.position !== right.position)
|
|
232
|
-
return left.position - right.position;
|
|
233
|
-
// Two entries with the same hash cannot coexist; this fallback orders
|
|
234
|
-
// entries whose hash the docs no longer contain, deterministically.
|
|
235
|
-
return compareRequirements(left.requirement, right.requirement);
|
|
236
|
-
});
|
|
237
|
-
const sorted = { requirements: withPosition.map((entry) => entry.requirement) };
|
|
238
|
-
writeFileSync(registryPath, `${JSON.stringify(sorted, null, 2)}\n`);
|
|
239
|
-
}
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `pasika doctor` checks.
|
|
3
|
-
*
|
|
4
|
-
* Diagnoses gaps between a consumer repository and the pasika framework
|
|
5
|
-
* baseline. Scope: dependencies, configuration, managed files, and
|
|
6
|
-
* source structure. Code-level and path-level rules belong to ESLint.
|
|
7
|
-
*/
|
|
8
|
-
export interface DoctorFinding {
|
|
9
|
-
/** Which requirement this relates to. */
|
|
10
|
-
check: string;
|
|
11
|
-
/** Human-readable description. */
|
|
12
|
-
message: string;
|
|
13
|
-
/** Severity: error blocks adoption, warning is informational. */
|
|
14
|
-
severity: "error" | "warning";
|
|
15
|
-
}
|
|
16
|
-
export declare function runDoctor(cwd: string): DoctorFinding[];
|
|
@@ -1,249 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `pasika doctor` checks.
|
|
3
|
-
*
|
|
4
|
-
* Diagnoses gaps between a consumer repository and the pasika framework
|
|
5
|
-
* baseline. Scope: dependencies, configuration, managed files, and
|
|
6
|
-
* source structure. Code-level and path-level rules belong to ESLint.
|
|
7
|
-
*/
|
|
8
|
-
import fs from "node:fs";
|
|
9
|
-
import path from "node:path";
|
|
10
|
-
import { z } from "zod";
|
|
11
|
-
const packageJsonSchema = z.object({
|
|
12
|
-
dependencies: z.record(z.string(), z.string()).optional(),
|
|
13
|
-
devDependencies: z.record(z.string(), z.string()).optional(),
|
|
14
|
-
scripts: z.record(z.string(), z.string()).optional(),
|
|
15
|
-
});
|
|
16
|
-
function readPackageJson(filePath) {
|
|
17
|
-
try {
|
|
18
|
-
const result = packageJsonSchema.safeParse(JSON.parse(fs.readFileSync(filePath, "utf8")));
|
|
19
|
-
if (result.success)
|
|
20
|
-
return result.data;
|
|
21
|
-
}
|
|
22
|
-
catch {
|
|
23
|
-
// missing or unparseable
|
|
24
|
-
}
|
|
25
|
-
return {};
|
|
26
|
-
}
|
|
27
|
-
/**
|
|
28
|
-
* Check that pasika and zirka are installed at compatible versions.
|
|
29
|
-
*/
|
|
30
|
-
function checkFrameworkPackages(pkg) {
|
|
31
|
-
const findings = [];
|
|
32
|
-
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
33
|
-
if (!allDeps.pasika) {
|
|
34
|
-
findings.push({
|
|
35
|
-
check: "pasika-installed",
|
|
36
|
-
message: "pasika is not listed in package.json.",
|
|
37
|
-
severity: "error",
|
|
38
|
-
});
|
|
39
|
-
}
|
|
40
|
-
if (!allDeps.zirka) {
|
|
41
|
-
findings.push({
|
|
42
|
-
check: "zirka-installed",
|
|
43
|
-
message: "zirka is not listed in package.json.",
|
|
44
|
-
severity: "error",
|
|
45
|
-
});
|
|
46
|
-
}
|
|
47
|
-
return findings;
|
|
48
|
-
}
|
|
49
|
-
/**
|
|
50
|
-
* Check that application source lives under src/.
|
|
51
|
-
*/
|
|
52
|
-
function checkSourceRoot(cwd) {
|
|
53
|
-
const findings = [];
|
|
54
|
-
const srcDir = path.join(cwd, "src");
|
|
55
|
-
if (!fs.existsSync(srcDir)) {
|
|
56
|
-
findings.push({
|
|
57
|
-
check: "source-under-src",
|
|
58
|
-
message: "Application source must live under src/.",
|
|
59
|
-
severity: "error",
|
|
60
|
-
});
|
|
61
|
-
return findings;
|
|
62
|
-
}
|
|
63
|
-
// Check for common non-src source directories at the project root
|
|
64
|
-
const entries = fs.readdirSync(cwd, { withFileTypes: true });
|
|
65
|
-
const suspicious = entries.filter((e) => e.isDirectory() &&
|
|
66
|
-
!e.name.startsWith(".") &&
|
|
67
|
-
e.name !== "src" &&
|
|
68
|
-
e.name !== "node_modules" &&
|
|
69
|
-
e.name !== "dist" &&
|
|
70
|
-
e.name !== "build" &&
|
|
71
|
-
e.name !== ".vulyk" &&
|
|
72
|
-
e.name !== "docs" &&
|
|
73
|
-
e.name !== "enforcement" &&
|
|
74
|
-
e.name !== "eslint" &&
|
|
75
|
-
e.name !== "cli" &&
|
|
76
|
-
e.name !== "__tests__" &&
|
|
77
|
-
e.name !== "test" &&
|
|
78
|
-
e.name !== "tests" &&
|
|
79
|
-
e.name !== "coverage" &&
|
|
80
|
-
e.name !== ".github" &&
|
|
81
|
-
e.name !== ".agents" &&
|
|
82
|
-
e.name !== ".husky" &&
|
|
83
|
-
e.name !== ".next" &&
|
|
84
|
-
e.name !== ".turbo" &&
|
|
85
|
-
e.name !== ".vercel" &&
|
|
86
|
-
e.name !== ".cache" &&
|
|
87
|
-
e.name !== ".tmp" &&
|
|
88
|
-
e.name !== "tmp" &&
|
|
89
|
-
e.name !== "public" &&
|
|
90
|
-
e.name !== "static" &&
|
|
91
|
-
e.name !== "assets" &&
|
|
92
|
-
e.name !== "bin" &&
|
|
93
|
-
e.name !== "vendor");
|
|
94
|
-
if (suspicious.length > 0) {
|
|
95
|
-
const names = suspicious.map((e) => e.name).join(", ");
|
|
96
|
-
findings.push({
|
|
97
|
-
check: "source-under-src",
|
|
98
|
-
message: `Possible source directories outside src/: ${names}.`,
|
|
99
|
-
severity: "warning",
|
|
100
|
-
});
|
|
101
|
-
}
|
|
102
|
-
return findings;
|
|
103
|
-
}
|
|
104
|
-
/**
|
|
105
|
-
* Find the global CSS file that registers Tailwind.
|
|
106
|
-
*/
|
|
107
|
-
function findGlobalStylesheet(cwd) {
|
|
108
|
-
const candidates = ["src/app/globals.css", "src/styles/globals.css", "src/globals.css"];
|
|
109
|
-
for (const candidate of candidates) {
|
|
110
|
-
const full = path.join(cwd, candidate);
|
|
111
|
-
if (fs.existsSync(full))
|
|
112
|
-
return full;
|
|
113
|
-
}
|
|
114
|
-
// Search for any CSS file containing @import "tailwindcss"
|
|
115
|
-
const srcDir = path.join(cwd, "src");
|
|
116
|
-
if (!fs.existsSync(srcDir))
|
|
117
|
-
return undefined;
|
|
118
|
-
const walk = (dir) => {
|
|
119
|
-
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
120
|
-
const full = path.join(dir, entry.name);
|
|
121
|
-
if (entry.isDirectory() && entry.name !== "node_modules" && entry.name !== ".next") {
|
|
122
|
-
const found = walk(full);
|
|
123
|
-
if (found)
|
|
124
|
-
return found;
|
|
125
|
-
}
|
|
126
|
-
if (entry.isFile() && entry.name.endsWith(".css")) {
|
|
127
|
-
const content = fs.readFileSync(full, "utf8");
|
|
128
|
-
if (content.includes('@import "tailwindcss"') || content.includes("@import 'tailwindcss'")) {
|
|
129
|
-
return full;
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
return undefined;
|
|
134
|
-
};
|
|
135
|
-
return walk(srcDir);
|
|
136
|
-
}
|
|
137
|
-
/**
|
|
138
|
-
* Check that the repository has a global stylesheet entry point that
|
|
139
|
-
* registers Tailwind. The stylesheet's contents are the ESLint CSS rules'
|
|
140
|
-
* concern, so doctor only checks that it exists.
|
|
141
|
-
*/
|
|
142
|
-
function checkGlobalStylesheet(cwd) {
|
|
143
|
-
const findings = [];
|
|
144
|
-
const stylesheet = findGlobalStylesheet(cwd);
|
|
145
|
-
if (!stylesheet) {
|
|
146
|
-
findings.push({
|
|
147
|
-
check: "global-stylesheet",
|
|
148
|
-
message: 'No global stylesheet found. Create src/app/globals.css with @import "tailwindcss".',
|
|
149
|
-
severity: "error",
|
|
150
|
-
});
|
|
151
|
-
}
|
|
152
|
-
return findings;
|
|
153
|
-
}
|
|
154
|
-
/**
|
|
155
|
-
* Check that configuration files follow the pasika baseline.
|
|
156
|
-
*
|
|
157
|
-
* Repositories must take their lint, format, and TypeScript configuration
|
|
158
|
-
* from pasika and zirka rather than restating it locally.
|
|
159
|
-
*/
|
|
160
|
-
function checkConfigBaseline(cwd) {
|
|
161
|
-
const findings = [];
|
|
162
|
-
// eslint.config.ts must exist and reference zirka
|
|
163
|
-
const eslintConfigs = ["eslint.config.ts", "eslint.config.mjs", "eslint.config.js"];
|
|
164
|
-
const eslintConfig = eslintConfigs.find((name) => fs.existsSync(path.join(cwd, name)));
|
|
165
|
-
if (!eslintConfig) {
|
|
166
|
-
findings.push({
|
|
167
|
-
check: "config-baseline",
|
|
168
|
-
message: "No eslint config found. Create eslint.config.ts extending the pasika/zirka baseline.",
|
|
169
|
-
severity: "error",
|
|
170
|
-
});
|
|
171
|
-
}
|
|
172
|
-
else {
|
|
173
|
-
const content = fs.readFileSync(path.join(cwd, eslintConfig), "utf8");
|
|
174
|
-
if (!content.includes("zirka")) {
|
|
175
|
-
findings.push({
|
|
176
|
-
check: "config-baseline",
|
|
177
|
-
message: `eslint config does not reference zirka. Use the pasika/zirka baseline configuration.`,
|
|
178
|
-
severity: "warning",
|
|
179
|
-
});
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
// tsconfig.json must exist
|
|
183
|
-
if (!fs.existsSync(path.join(cwd, "tsconfig.json"))) {
|
|
184
|
-
findings.push({
|
|
185
|
-
check: "config-baseline",
|
|
186
|
-
message: "No tsconfig.json found. Create one extending the pasika baseline.",
|
|
187
|
-
severity: "error",
|
|
188
|
-
});
|
|
189
|
-
}
|
|
190
|
-
return findings;
|
|
191
|
-
}
|
|
192
|
-
/**
|
|
193
|
-
* Check that vulyk-managed files have not been hand-edited.
|
|
194
|
-
*
|
|
195
|
-
* Files listed in the .vulyk manifest are generated by vulyk commands
|
|
196
|
-
* and must not be edited directly.
|
|
197
|
-
*/
|
|
198
|
-
function checkManagedFiles(cwd) {
|
|
199
|
-
const findings = [];
|
|
200
|
-
const vulykDir = path.join(cwd, ".vulyk");
|
|
201
|
-
if (!fs.existsSync(vulykDir))
|
|
202
|
-
return findings;
|
|
203
|
-
const manifestPath = path.join(vulykDir, "manifest.json");
|
|
204
|
-
if (!fs.existsSync(manifestPath))
|
|
205
|
-
return findings;
|
|
206
|
-
try {
|
|
207
|
-
const manifestEntrySchema = z.looseObject({
|
|
208
|
-
targets: z.array(z.string()).optional(),
|
|
209
|
-
});
|
|
210
|
-
const manifestSchema = z.record(z.string(), manifestEntrySchema);
|
|
211
|
-
const result = manifestSchema.safeParse(JSON.parse(fs.readFileSync(manifestPath, "utf8")));
|
|
212
|
-
if (!result.success)
|
|
213
|
-
return findings;
|
|
214
|
-
for (const [, entry] of Object.entries(result.data)) {
|
|
215
|
-
const targets = entry.targets ?? [];
|
|
216
|
-
for (const target of targets) {
|
|
217
|
-
const targetPath = path.join(cwd, target);
|
|
218
|
-
if (fs.existsSync(targetPath)) {
|
|
219
|
-
// File exists — check it hasn't been modified since vulyk last wrote it
|
|
220
|
-
const stat = fs.statSync(targetPath);
|
|
221
|
-
const vulykStat = fs.statSync(manifestPath);
|
|
222
|
-
// If target is newer than the manifest, it was edited after vulyk wrote it
|
|
223
|
-
if (stat.mtimeMs > vulykStat.mtimeMs + 1000) {
|
|
224
|
-
findings.push({
|
|
225
|
-
check: "managed-file-edit",
|
|
226
|
-
message: `File "${target}" appears to have been edited after vulyk wrote it. Use vulyk commands to update managed files.`,
|
|
227
|
-
severity: "warning",
|
|
228
|
-
});
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
catch {
|
|
235
|
-
// manifest unreadable
|
|
236
|
-
}
|
|
237
|
-
return findings;
|
|
238
|
-
}
|
|
239
|
-
export function runDoctor(cwd) {
|
|
240
|
-
const pkgPath = path.join(cwd, "package.json");
|
|
241
|
-
const pkg = readPackageJson(pkgPath);
|
|
242
|
-
return [
|
|
243
|
-
...checkFrameworkPackages(pkg),
|
|
244
|
-
...checkConfigBaseline(cwd),
|
|
245
|
-
...checkManagedFiles(cwd),
|
|
246
|
-
...checkSourceRoot(cwd),
|
|
247
|
-
...checkGlobalStylesheet(cwd),
|
|
248
|
-
];
|
|
249
|
-
}
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Reduces a requirement bullet to the text its meaning depends on.
|
|
3
|
-
*
|
|
4
|
-
* Markdown links collapse to their link text and code spans lose their
|
|
5
|
-
* backticks, so editing a URL or adding backticks does not read as a change in
|
|
6
|
-
* what the requirement demands. Whitespace collapses so re-wrapping a long
|
|
7
|
-
* bullet is not a change either.
|
|
8
|
-
*/
|
|
9
|
-
export declare function normalizeRequirement(bullet: string): string;
|
|
10
|
-
/** Short, stable fingerprint of a canonical requirement text. */
|
|
11
|
-
export declare function hashRequirement(canonicalText: string): string;
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
2
|
-
/**
|
|
3
|
-
* Reduces a requirement bullet to the text its meaning depends on.
|
|
4
|
-
*
|
|
5
|
-
* Markdown links collapse to their link text and code spans lose their
|
|
6
|
-
* backticks, so editing a URL or adding backticks does not read as a change in
|
|
7
|
-
* what the requirement demands. Whitespace collapses so re-wrapping a long
|
|
8
|
-
* bullet is not a change either.
|
|
9
|
-
*/
|
|
10
|
-
export function normalizeRequirement(bullet) {
|
|
11
|
-
return bullet
|
|
12
|
-
.replace(/^\s*[-*]\s+/, "")
|
|
13
|
-
.replaceAll(/\[(?<text>[^\]]+)\]\([^)]*\)/g, "$<text>")
|
|
14
|
-
.replaceAll("`", "")
|
|
15
|
-
.replaceAll(/\s+/g, " ")
|
|
16
|
-
.trim();
|
|
17
|
-
}
|
|
18
|
-
/** Short, stable fingerprint of a canonical requirement text. */
|
|
19
|
-
export function hashRequirement(canonicalText) {
|
|
20
|
-
return createHash("sha256").update(canonicalText).digest("hex").slice(0, 10);
|
|
21
|
-
}
|