rulebookguard 0.8.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +175 -56
- package/dist/{anchors-C2j9c5MI.mjs → anchors-jf2bKfyg.mjs} +1 -1
- package/dist/cli.mjs +519 -147
- package/dist/index.d.mts +44 -8
- package/dist/index.mjs +3 -3
- package/dist/{lint-DfibQDW3.mjs → lint-BPdk6XxH.mjs} +209 -16
- package/dist/{site-DmY5YVte.mjs → site-B7g8lbjx.mjs} +16 -2
- package/package.json +3 -3
- package/schemas/v1/rule.schema.json +4 -0
package/dist/index.d.mts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
type RuleStatus = "proposed" | "active" | "deprecated";
|
|
2
2
|
type RuleLanguage = "en" | "es";
|
|
3
|
+
type RuleOrigin = "authored" | "extracted";
|
|
3
4
|
interface ParseOptions {
|
|
4
5
|
/** Language of the issue messages; `en` when unset. */
|
|
5
6
|
language?: RuleLanguage | undefined;
|
|
@@ -27,6 +28,14 @@ interface RuleFrontmatter {
|
|
|
27
28
|
entities: string[];
|
|
28
29
|
status: RuleStatus;
|
|
29
30
|
supersedes?: string;
|
|
31
|
+
/** Rules this rule needs in force; `lint` rejects unknown ids and cycles. */
|
|
32
|
+
dependsOn?: string[];
|
|
33
|
+
/** Rules that cannot be active at the same time as this one. */
|
|
34
|
+
conflictsWith?: string[];
|
|
35
|
+
/** `extracted` when the rule was lifted from existing code rather than written from intent. */
|
|
36
|
+
origin?: RuleOrigin;
|
|
37
|
+
/** Where the rule shows in the code or tests, for example `src/x.ts:42`. */
|
|
38
|
+
evidence?: string[];
|
|
30
39
|
anchors: string[];
|
|
31
40
|
summary: string;
|
|
32
41
|
}
|
|
@@ -65,18 +74,22 @@ interface RuleTag {
|
|
|
65
74
|
interface RuleRegion extends RuleTag {
|
|
66
75
|
endLine: number;
|
|
67
76
|
}
|
|
77
|
+
type RegionKind = "rule" | "implements";
|
|
68
78
|
type RuleTagProblemCode = "unclosed-rule-tag" | "unmatched-endrule-tag";
|
|
69
|
-
|
|
70
|
-
|
|
79
|
+
type ImplementsTagProblemCode = "unclosed-implements-tag" | "unmatched-endimplements-tag";
|
|
80
|
+
interface RuleTagProblem<Code extends string = RuleTagProblemCode> extends RuleTag {
|
|
81
|
+
code: Code;
|
|
71
82
|
}
|
|
72
|
-
interface RuleRegions {
|
|
83
|
+
interface RuleRegions<Code extends string = RuleTagProblemCode> {
|
|
73
84
|
regions: RuleRegion[];
|
|
74
|
-
problems: RuleTagProblem[];
|
|
85
|
+
problems: RuleTagProblem<Code>[];
|
|
75
86
|
}
|
|
76
87
|
export declare const RULE_TAG_PATTERN: RegExp;
|
|
77
88
|
export declare const RULE_MARKER_PATTERN: RegExp;
|
|
89
|
+
export declare const IMPLEMENTS_MARKER_PATTERN: RegExp;
|
|
78
90
|
export declare function findRuleTags(source: string): RuleTag[];
|
|
79
|
-
export declare function findRuleRegions(source: string): RuleRegions;
|
|
91
|
+
export declare function findRuleRegions(source: string, kind?: "rule"): RuleRegions;
|
|
92
|
+
export declare function findRuleRegions(source: string, kind: "implements"): RuleRegions<ImplementsTagProblemCode>;
|
|
80
93
|
export declare function rulesForPath(rules: RuleDoc[], path: string): RuleDoc[];
|
|
81
94
|
export declare function toRepoPath(path: string): string;
|
|
82
95
|
export declare function createGlobMatcher(globs: string[]): (path: string) => boolean;
|
|
@@ -109,8 +122,8 @@ interface TagLocation {
|
|
|
109
122
|
interface TagRegion extends TagLocation {
|
|
110
123
|
endLine: number;
|
|
111
124
|
}
|
|
112
|
-
interface TagProblem extends TagLocation {
|
|
113
|
-
code:
|
|
125
|
+
interface TagProblem<Code extends string = RuleTagProblemCode> extends TagLocation {
|
|
126
|
+
code: Code;
|
|
114
127
|
id: string;
|
|
115
128
|
}
|
|
116
129
|
type TagIndex = Map<string, TagLocation[]>;
|
|
@@ -122,6 +135,11 @@ interface TagScan {
|
|
|
122
135
|
}
|
|
123
136
|
export declare function scanTestTags(root: string, testGlobs: string[], onGitFallback?: GitFallbackListener): Promise<TagIndex>;
|
|
124
137
|
export declare function scanTestRegions(root: string, testGlobs: string[], onGitFallback?: GitFallbackListener): Promise<TagScan>;
|
|
138
|
+
interface ImplementationScan {
|
|
139
|
+
regions: RegionIndex;
|
|
140
|
+
problems: TagProblem<ImplementsTagProblemCode>[];
|
|
141
|
+
}
|
|
142
|
+
export declare function scanImplementations(root: string, files: string[]): Promise<ImplementationScan>;
|
|
125
143
|
interface ContextsJson {
|
|
126
144
|
contexts: {
|
|
127
145
|
context: string;
|
|
@@ -145,6 +163,11 @@ interface ShowJson {
|
|
|
145
163
|
entities: string[];
|
|
146
164
|
anchors: string[];
|
|
147
165
|
supersedes: string | null;
|
|
166
|
+
dependsOn: string[];
|
|
167
|
+
conflictsWith: string[];
|
|
168
|
+
origin: RuleOrigin | null;
|
|
169
|
+
evidence: string[];
|
|
170
|
+
implementations: TagRegion[];
|
|
148
171
|
file: string;
|
|
149
172
|
body: string;
|
|
150
173
|
};
|
|
@@ -153,8 +176,21 @@ interface ForJson {
|
|
|
153
176
|
path: string;
|
|
154
177
|
rules: (RuleSummaryJson & {
|
|
155
178
|
file: string;
|
|
179
|
+
regions: LineSpanJson[];
|
|
180
|
+
})[];
|
|
181
|
+
}
|
|
182
|
+
interface ForChangedJson {
|
|
183
|
+
base: string;
|
|
184
|
+
files: string[];
|
|
185
|
+
rules: (RuleSummaryJson & {
|
|
186
|
+
file: string;
|
|
187
|
+
files: string[];
|
|
156
188
|
})[];
|
|
157
189
|
}
|
|
190
|
+
interface LineSpanJson {
|
|
191
|
+
line: number;
|
|
192
|
+
endLine: number;
|
|
193
|
+
}
|
|
158
194
|
interface ImpactJson {
|
|
159
195
|
id: string;
|
|
160
196
|
files: string[];
|
|
@@ -184,4 +220,4 @@ interface LintOptions {
|
|
|
184
220
|
export declare function lintCatalog(catalog: Catalog, scan?: TagScan, onGitFallback?: GitFallbackListener): Promise<LintFinding[]>;
|
|
185
221
|
export declare function pendingImplementation(catalog: Catalog, scan: TagScan, base: string, changed: string[], lines: Map<string, LineSpan[]>): LintFinding[];
|
|
186
222
|
export declare function formatFinding(finding: LintFinding): string;
|
|
187
|
-
export type { Catalog, CatalogIssue, CatalogIssueCode, ContextDoc, ContextFrontmatter, ContextsJson, ForJson, FormatIssue, Frontmatter, GitFallbackListener, ImpactJson, LintFinding, LintJson, LintOptions, LintSeverity, ListJson, ParseOptions, RegionIndex, RuleDoc, RuleFrontmatter, RuleLanguage, RuleRegion, RuleRegions, RuleStatus, RuleSummaryJson, RuleTag, RuleTagProblem, RuleTagProblemCode, RulebookConfig, ShowJson, TagIndex, TagLocation, TagProblem, TagRegion, TagScan };
|
|
223
|
+
export type { Catalog, CatalogIssue, CatalogIssueCode, ContextDoc, ContextFrontmatter, ContextsJson, ForChangedJson, ForJson, FormatIssue, Frontmatter, GitFallbackListener, ImpactJson, ImplementationScan, ImplementsTagProblemCode, LintFinding, LintJson, LintOptions, LintSeverity, ListJson, ParseOptions, RegionIndex, RegionKind, RuleDoc, RuleFrontmatter, RuleLanguage, RuleRegion, RuleRegions, RuleStatus, RuleSummaryJson, RuleTag, RuleTagProblem, RuleTagProblemCode, RulebookConfig, ShowJson, TagIndex, TagLocation, TagProblem, TagRegion, TagScan };
|
package/dist/index.mjs
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { A as
|
|
2
|
-
import { t as rulesForPath } from "./anchors-
|
|
3
|
-
export { CONFIG_FILE_NAME, CONTEXT_FILE_NAME, FormatError, RULE_MARKER_PATTERN, RULE_TAG_PATTERN, createGlobMatcher, expandGlobs, findRuleRegions, findRuleTags, findRulebookRoot, formatFinding, formatIssue, isRuleId, lintCatalog, listRepoFiles, loadCatalog, loadConfig, loadContext, loadRule, parseConfig, parseContext, parseRule, pendingImplementation, relativeToRoot, rulesForPath, scanTestRegions, scanTestTags, splitFrontmatter, toRepoPath };
|
|
1
|
+
import { A as loadRule, B as FormatError, D as findRuleRegions, E as RULE_TAG_PATTERN, F as splitFrontmatter, I as CONFIG_FILE_NAME, L as loadConfig, M as CONTEXT_FILE_NAME, N as loadContext, O as findRuleTags, P as parseContext, R as parseConfig, S as toRepoPath, T as RULE_MARKER_PATTERN, V as formatIssue, _ as loadCatalog, a as pendingImplementation, b as expandGlobs, g as findRulebookRoot, h as scanTestTags, j as parseRule, k as isRuleId, m as scanTestRegions, n as formatFinding, p as scanImplementations, r as lintCatalog, v as relativeToRoot, w as IMPLEMENTS_MARKER_PATTERN, x as listRepoFiles, y as createGlobMatcher } from "./lint-BPdk6XxH.mjs";
|
|
2
|
+
import { t as rulesForPath } from "./anchors-jf2bKfyg.mjs";
|
|
3
|
+
export { CONFIG_FILE_NAME, CONTEXT_FILE_NAME, FormatError, IMPLEMENTS_MARKER_PATTERN, RULE_MARKER_PATTERN, RULE_TAG_PATTERN, createGlobMatcher, expandGlobs, findRuleRegions, findRuleTags, findRulebookRoot, formatFinding, formatIssue, isRuleId, lintCatalog, listRepoFiles, loadCatalog, loadConfig, loadContext, loadRule, parseConfig, parseContext, parseRule, pendingImplementation, relativeToRoot, rulesForPath, scanImplementations, scanTestRegions, scanTestTags, splitFrontmatter, toRepoPath };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { access, lstat, readFile, readdir, realpath } from "node:fs/promises";
|
|
1
|
+
import { access, lstat, readFile, readdir, realpath, stat } from "node:fs/promises";
|
|
2
2
|
import { Ajv2020 } from "ajv/dist/2020.js";
|
|
3
3
|
import { YAMLParseError, parse } from "yaml";
|
|
4
4
|
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
@@ -115,6 +115,22 @@ var rule_schema_default = {
|
|
|
115
115
|
"deprecated"
|
|
116
116
|
] },
|
|
117
117
|
"supersedes": { "$ref": "#/$defs/ruleId" },
|
|
118
|
+
"dependsOn": {
|
|
119
|
+
"type": "array",
|
|
120
|
+
"items": { "$ref": "#/$defs/ruleId" }
|
|
121
|
+
},
|
|
122
|
+
"conflictsWith": {
|
|
123
|
+
"type": "array",
|
|
124
|
+
"items": { "$ref": "#/$defs/ruleId" }
|
|
125
|
+
},
|
|
126
|
+
"origin": { "enum": ["authored", "extracted"] },
|
|
127
|
+
"evidence": {
|
|
128
|
+
"type": "array",
|
|
129
|
+
"items": {
|
|
130
|
+
"type": "string",
|
|
131
|
+
"minLength": 1
|
|
132
|
+
}
|
|
133
|
+
},
|
|
118
134
|
"anchors": {
|
|
119
135
|
"type": "array",
|
|
120
136
|
"items": {
|
|
@@ -171,6 +187,8 @@ const MESSAGES = {
|
|
|
171
187
|
noContexts: "No contexts found. Create one with \"rulebookguard new <context>\".",
|
|
172
188
|
noRules: "No rules found.",
|
|
173
189
|
noActiveRulesFor: (path) => `No active rules anchored to ${path}.`,
|
|
190
|
+
noActiveRulesForChanged: (ref) => `No active rules anchored to the files changed since ${ref}.`,
|
|
191
|
+
lines: (spans) => `lines ${spans}`,
|
|
174
192
|
filesMatched: (count) => `Files matched by anchors (${count}):`,
|
|
175
193
|
testsTagging: (id, count) => `Tests tagging ${id} (${count}):`,
|
|
176
194
|
none: "(none)",
|
|
@@ -188,8 +206,35 @@ const MESSAGES = {
|
|
|
188
206
|
deprecatedRuleTag: (id) => `tags ${id}, which is deprecated`,
|
|
189
207
|
unclosedRuleTag: (id) => `@rule ${id} has no matching @endrule ${id}`,
|
|
190
208
|
unmatchedEndruleTag: (id) => `@endrule ${id} has no open @rule ${id} before it`,
|
|
209
|
+
unknownImplementsTag: (id) => `marks code as implementing ${id}, which does not exist`,
|
|
210
|
+
deprecatedImplementsTag: (id) => `marks code as implementing ${id}, which is deprecated`,
|
|
211
|
+
unclosedImplementsTag: (id) => `@implements ${id} has no matching @endimplements ${id}`,
|
|
212
|
+
unmatchedEndimplementsTag: (id) => `@endimplements ${id} has no open @implements ${id} before it`,
|
|
213
|
+
implementsOutsideAnchors: (id) => `marks code as implementing ${id}, but no anchor of ${id} matches this file`,
|
|
191
214
|
contextOverBudget: (context, count, budget) => `context "${context}" has ${count} rules, more than the contextBudget of ${budget}`,
|
|
192
215
|
pendingImplementation: (id, base) => `rule ${id} changed since ${base} but none of the tests tagging it changed`,
|
|
216
|
+
unknownRuleReference: (field, id) => `${field} names ${id}, which does not exist`,
|
|
217
|
+
selfRuleReference: (field) => `${field} names the rule itself`,
|
|
218
|
+
ruleReferenceHint: (field) => `fix the id, or remove it from ${field}`,
|
|
219
|
+
dependencyCycle: (cycle) => `dependsOn forms a cycle: ${cycle.join(" → ")}`,
|
|
220
|
+
dependencyCycleHint: "remove one dependsOn link of the cycle",
|
|
221
|
+
activeRuleConflict: (a, b) => `active rules ${a} and ${b} conflict`,
|
|
222
|
+
activeRuleConflictHint: "deprecate one of them or set it to proposed, or remove the conflictsWith entry",
|
|
223
|
+
noGherkinScenarios: "No rule has Gherkin scenarios.",
|
|
224
|
+
generatedFeature: (file) => `Generated by "rulebookguard export gherkin" from ${file}; edit the rule instead.`,
|
|
225
|
+
featureKeyword: "Feature",
|
|
226
|
+
noActiveRules: "No active rule to export.",
|
|
227
|
+
removedExport: (path) => `removed ${path}`,
|
|
228
|
+
handWrittenExport: (path, format) => `${path} was not generated by "rulebookguard export ${format}"; move it away and run the export again.`,
|
|
229
|
+
generatedRule: (format, file) => `Generated by "rulebookguard export ${format}" from ${file}; edit the rule instead.`,
|
|
230
|
+
generatedAgentsMd: "Generated by \"rulebookguard export agents-md\"; edit the rules instead.",
|
|
231
|
+
agentsMdHeading: "Business rules",
|
|
232
|
+
agentsMdIntro: "Before changing a file, follow the active rules anchored to it:",
|
|
233
|
+
agentsMdSeeFile: (file) => `See \`${file}\`.`,
|
|
234
|
+
agentsMdNoAnchors: "no anchors",
|
|
235
|
+
noAnchoredRules: "No active rule with anchors to export.",
|
|
236
|
+
unanchoredRuleSkipped: (id) => `${id} is active but has no anchors, so it was not exported.`,
|
|
237
|
+
invalidAgentsMdMarkers: (file, start, end) => `${file} must have one "${start}" followed by one "${end}", or neither; fix it and run the export again.`,
|
|
193
238
|
noIssues: "No issues found.",
|
|
194
239
|
hintLabel: "hint",
|
|
195
240
|
catalogHints: {
|
|
@@ -207,10 +252,15 @@ const MESSAGES = {
|
|
|
207
252
|
deprecatedRuleTagHint: (id, successor) => successor === void 0 ? `tag the rule that replaces ${id}, or remove the tag` : `tag ${successor}, which supersedes ${id}, instead`,
|
|
208
253
|
unclosedRuleTagHint: (id) => `add "@endrule ${id}" after the last line of the test, or close the inner regions first`,
|
|
209
254
|
unmatchedEndruleTagHint: (id) => `add "@rule ${id}" before the test, or remove the stray @endrule`,
|
|
255
|
+
unknownImplementsTagHint: "fix the id in the marker, or create the rule it refers to",
|
|
256
|
+
deprecatedImplementsTagHint: (id, successor) => successor === void 0 ? `mark the code with the rule that replaces ${id}, or remove the region` : `mark the code with ${successor}, which supersedes ${id}, instead`,
|
|
257
|
+
unclosedImplementsTagHint: (id) => `add "@endimplements ${id}" after the last line of the code that enforces the rule, or close the inner regions first`,
|
|
258
|
+
unmatchedEndimplementsTagHint: (id) => `add "@implements ${id}" before the code that enforces the rule, or remove the stray @endimplements`,
|
|
259
|
+
implementsOutsideAnchorsHint: (id) => `add an anchor to ${id} that matches this file, or move the region to a file it anchors`,
|
|
210
260
|
contextOverBudgetHint: "split the context into smaller ones, or raise contextBudget in rulebookguard.config.yaml",
|
|
211
261
|
pendingImplementationHint: (id) => `update or add a test inside an "@rule ${id}" … "@endrule ${id}" region that covers the changed rule`,
|
|
212
262
|
lintSummary: (errors, warnings) => `${errors} ${errors === 1 ? "error" : "errors"}, ${warnings} ${warnings === 1 ? "warning" : "warnings"}`,
|
|
213
|
-
needsWorkTree: (root) =>
|
|
263
|
+
needsWorkTree: (command, root) => `${command} needs a git work tree, but ${root} is not inside one.`,
|
|
214
264
|
unknownRef: (ref) => `Unknown git ref "${ref}".`,
|
|
215
265
|
cannotListChanged: (ref, reason) => `Cannot list the files changed since "${ref}": ${reason}`
|
|
216
266
|
},
|
|
@@ -245,6 +295,8 @@ const MESSAGES = {
|
|
|
245
295
|
noContexts: "No se encontraron contextos. Crea uno con \"rulebookguard new <contexto>\".",
|
|
246
296
|
noRules: "No se encontraron reglas.",
|
|
247
297
|
noActiveRulesFor: (path) => `No hay reglas activas ancladas a ${path}.`,
|
|
298
|
+
noActiveRulesForChanged: (ref) => `No hay reglas activas ancladas a los archivos cambiados desde ${ref}.`,
|
|
299
|
+
lines: (spans) => `líneas ${spans}`,
|
|
248
300
|
filesMatched: (count) => `Archivos que cubren los anchors (${count}):`,
|
|
249
301
|
testsTagging: (id, count) => `Tests que etiquetan ${id} (${count}):`,
|
|
250
302
|
none: "(ninguno)",
|
|
@@ -262,8 +314,35 @@ const MESSAGES = {
|
|
|
262
314
|
deprecatedRuleTag: (id) => `etiqueta ${id}, que está deprecada`,
|
|
263
315
|
unclosedRuleTag: (id) => `@rule ${id} no tiene su @endrule ${id}`,
|
|
264
316
|
unmatchedEndruleTag: (id) => `@endrule ${id} no tiene un @rule ${id} abierto antes`,
|
|
317
|
+
unknownImplementsTag: (id) => `marca código como implementación de ${id}, que no existe`,
|
|
318
|
+
deprecatedImplementsTag: (id) => `marca código como implementación de ${id}, que está deprecada`,
|
|
319
|
+
unclosedImplementsTag: (id) => `@implements ${id} no tiene su @endimplements ${id}`,
|
|
320
|
+
unmatchedEndimplementsTag: (id) => `@endimplements ${id} no tiene un @implements ${id} abierto antes`,
|
|
321
|
+
implementsOutsideAnchors: (id) => `marca código como implementación de ${id}, pero ningún anchor de ${id} coincide con este archivo`,
|
|
265
322
|
contextOverBudget: (context, count, budget) => `el contexto "${context}" tiene ${count} reglas, más que el contextBudget de ${budget}`,
|
|
266
323
|
pendingImplementation: (id, base) => `la regla ${id} cambió desde ${base} pero no cambió ninguno de los tests que la etiquetan`,
|
|
324
|
+
unknownRuleReference: (field, id) => `${field} nombra a ${id}, que no existe`,
|
|
325
|
+
selfRuleReference: (field) => `${field} nombra a la propia regla`,
|
|
326
|
+
ruleReferenceHint: (field) => `corrige el id o quítalo de ${field}`,
|
|
327
|
+
dependencyCycle: (cycle) => `dependsOn forma un ciclo: ${cycle.join(" → ")}`,
|
|
328
|
+
dependencyCycleHint: "quita un vínculo dependsOn del ciclo",
|
|
329
|
+
activeRuleConflict: (a, b) => `las reglas activas ${a} y ${b} están en conflicto`,
|
|
330
|
+
activeRuleConflictHint: "depreca una de ellas o déjala en proposed, o quita la entrada de conflictsWith",
|
|
331
|
+
noGherkinScenarios: "Ninguna regla tiene escenarios Gherkin.",
|
|
332
|
+
generatedFeature: (file) => `Generado por "rulebookguard export gherkin" desde ${file}; edita la regla en su lugar.`,
|
|
333
|
+
featureKeyword: "Característica",
|
|
334
|
+
noActiveRules: "Ninguna regla activa para exportar.",
|
|
335
|
+
removedExport: (path) => `eliminado ${path}`,
|
|
336
|
+
handWrittenExport: (path, format) => `${path} no fue generado por "rulebookguard export ${format}"; muévelo y vuelve a exportar.`,
|
|
337
|
+
generatedRule: (format, file) => `Generado por "rulebookguard export ${format}" desde ${file}; edita la regla en su lugar.`,
|
|
338
|
+
generatedAgentsMd: "Generado por \"rulebookguard export agents-md\"; edita las reglas en su lugar.",
|
|
339
|
+
agentsMdHeading: "Reglas de negocio",
|
|
340
|
+
agentsMdIntro: "Antes de cambiar un archivo, sigue las reglas activas ancladas a él:",
|
|
341
|
+
agentsMdSeeFile: (file) => `Ver \`${file}\`.`,
|
|
342
|
+
agentsMdNoAnchors: "sin anchors",
|
|
343
|
+
noAnchoredRules: "Ninguna regla activa con anchors para exportar.",
|
|
344
|
+
unanchoredRuleSkipped: (id) => `${id} está activa pero no tiene anchors, así que no se exportó.`,
|
|
345
|
+
invalidAgentsMdMarkers: (file, start, end) => `${file} debe tener un "${start}" seguido de un "${end}", o ninguno; corrígelo y vuelve a exportar.`,
|
|
267
346
|
noIssues: "No se encontraron problemas.",
|
|
268
347
|
hintLabel: "sugerencia",
|
|
269
348
|
catalogHints: {
|
|
@@ -281,10 +360,15 @@ const MESSAGES = {
|
|
|
281
360
|
deprecatedRuleTagHint: (id, successor) => successor === void 0 ? `etiqueta la regla que reemplaza a ${id} o quita la etiqueta` : `etiqueta ${successor}, que reemplaza a ${id}`,
|
|
282
361
|
unclosedRuleTagHint: (id) => `agrega "@endrule ${id}" después de la última línea del test, o cierra antes las regiones internas`,
|
|
283
362
|
unmatchedEndruleTagHint: (id) => `agrega "@rule ${id}" antes del test, o quita el @endrule que sobra`,
|
|
363
|
+
unknownImplementsTagHint: "corrige el id del marcador o crea la regla a la que se refiere",
|
|
364
|
+
deprecatedImplementsTagHint: (id, successor) => successor === void 0 ? `marca el código con la regla que reemplaza a ${id} o quita la región` : `marca el código con ${successor}, que reemplaza a ${id}`,
|
|
365
|
+
unclosedImplementsTagHint: (id) => `agrega "@endimplements ${id}" después de la última línea del código que cumple la regla, o cierra antes las regiones internas`,
|
|
366
|
+
unmatchedEndimplementsTagHint: (id) => `agrega "@implements ${id}" antes del código que cumple la regla, o quita el @endimplements que sobra`,
|
|
367
|
+
implementsOutsideAnchorsHint: (id) => `agrega a ${id} un anchor que coincida con este archivo, o mueve la región a un archivo que ancle`,
|
|
284
368
|
contextOverBudgetHint: "divide el contexto en otros más chicos o sube contextBudget en rulebookguard.config.yaml",
|
|
285
369
|
pendingImplementationHint: (id) => `actualiza o agrega un test dentro de una región "@rule ${id}" … "@endrule ${id}" que cubra el cambio de la regla`,
|
|
286
370
|
lintSummary: (errors, warnings) => `${errors} ${errors === 1 ? "error" : "errores"}, ${warnings} ${warnings === 1 ? "aviso" : "avisos"}`,
|
|
287
|
-
needsWorkTree: (root) =>
|
|
371
|
+
needsWorkTree: (command, root) => `${command} necesita un work tree de git, pero ${root} no está en uno.`,
|
|
288
372
|
unknownRef: (ref) => `La referencia de git "${ref}" no existe.`,
|
|
289
373
|
cannotListChanged: (ref, reason) => `No se pudieron listar los archivos cambiados desde "${ref}": ${reason}`
|
|
290
374
|
}
|
|
@@ -429,10 +513,16 @@ async function loadRule(path, options) {
|
|
|
429
513
|
}
|
|
430
514
|
// src/format/tag.ts
|
|
431
515
|
const RULE_TAG_PATTERN = /(?<![\w@.-])@rule\s+(R-[A-Z][A-Z0-9]*-[0-9]{3})(?![\w-])/g;
|
|
432
|
-
const RULE_MARKER_PATTERN =
|
|
516
|
+
const RULE_MARKER_PATTERN = markerPattern("rule");
|
|
517
|
+
const IMPLEMENTS_MARKER_PATTERN = markerPattern("implements");
|
|
518
|
+
function markerPattern(kind) {
|
|
519
|
+
return new RegExp(`(?<![\\w@.-])@(${kind}|end${kind})\\s+(R-[A-Z][A-Z0-9]*-[0-9]{3})(?![\\w-])`, "g");
|
|
520
|
+
}
|
|
521
|
+
const COMMENT_LINE = /^\s*(?:\/\/|\/\*|\*|#|--|;|<!--|%)/;
|
|
433
522
|
function findRuleTags(source) {
|
|
434
523
|
const tags = [];
|
|
435
524
|
source.split(/\r?\n/).forEach((text, index) => {
|
|
525
|
+
if (!COMMENT_LINE.test(text)) return;
|
|
436
526
|
for (const match of text.matchAll(RULE_TAG_PATTERN)) tags.push({
|
|
437
527
|
id: match[1],
|
|
438
528
|
line: index + 1
|
|
@@ -440,15 +530,17 @@ function findRuleTags(source) {
|
|
|
440
530
|
});
|
|
441
531
|
return tags;
|
|
442
532
|
}
|
|
443
|
-
function findRuleRegions(source) {
|
|
533
|
+
function findRuleRegions(source, kind = "rule") {
|
|
534
|
+
const pattern = kind === "rule" ? RULE_MARKER_PATTERN : IMPLEMENTS_MARKER_PATTERN;
|
|
444
535
|
const regions = [];
|
|
445
536
|
const problems = [];
|
|
446
537
|
const open = [];
|
|
447
538
|
source.split(/\r?\n/).forEach((text, index) => {
|
|
539
|
+
if (!COMMENT_LINE.test(text)) return;
|
|
448
540
|
const line = index + 1;
|
|
449
|
-
for (const match of text.matchAll(
|
|
541
|
+
for (const match of text.matchAll(pattern)) {
|
|
450
542
|
const id = match[2];
|
|
451
|
-
if (match[1] ===
|
|
543
|
+
if (match[1] === kind) {
|
|
452
544
|
open.push({
|
|
453
545
|
id,
|
|
454
546
|
line
|
|
@@ -458,14 +550,14 @@ function findRuleRegions(source) {
|
|
|
458
550
|
const at = open.map((tag) => tag.id).lastIndexOf(id);
|
|
459
551
|
if (at === -1) {
|
|
460
552
|
problems.push({
|
|
461
|
-
code:
|
|
553
|
+
code: `unmatched-end${kind}-tag`,
|
|
462
554
|
id,
|
|
463
555
|
line
|
|
464
556
|
});
|
|
465
557
|
continue;
|
|
466
558
|
}
|
|
467
559
|
for (const inner of open.splice(at + 1)) problems.push({
|
|
468
|
-
code:
|
|
560
|
+
code: `unclosed-${kind}-tag`,
|
|
469
561
|
...inner
|
|
470
562
|
});
|
|
471
563
|
const start = open.pop();
|
|
@@ -476,7 +568,7 @@ function findRuleRegions(source) {
|
|
|
476
568
|
}
|
|
477
569
|
});
|
|
478
570
|
for (const tag of open) problems.push({
|
|
479
|
-
code:
|
|
571
|
+
code: `unclosed-${kind}-tag`,
|
|
480
572
|
...tag
|
|
481
573
|
});
|
|
482
574
|
problems.sort((a, b) => a.line - b.line);
|
|
@@ -523,10 +615,11 @@ async function isCommitRef(cwd, ref) {
|
|
|
523
615
|
return false;
|
|
524
616
|
}
|
|
525
617
|
}
|
|
526
|
-
async function changedSince(cwd, ref) {
|
|
618
|
+
async function changedSince(cwd, ref, { bothRenamePaths = false } = {}) {
|
|
527
619
|
const changed = await gitPaths(cwd, "diff", [
|
|
528
620
|
"--name-only",
|
|
529
621
|
"--relative",
|
|
622
|
+
...bothRenamePaths ? ["--no-renames"] : [],
|
|
530
623
|
ref,
|
|
531
624
|
"--"
|
|
532
625
|
]);
|
|
@@ -847,6 +940,30 @@ async function scanTestRegions(root, testGlobs, onGitFallback) {
|
|
|
847
940
|
problems
|
|
848
941
|
};
|
|
849
942
|
}
|
|
943
|
+
async function scanImplementations(root, files) {
|
|
944
|
+
const regions = /* @__PURE__ */ new Map();
|
|
945
|
+
const problems = [];
|
|
946
|
+
const realRoot = await realpath(root);
|
|
947
|
+
for (const file of files) {
|
|
948
|
+
const path = join(root, file);
|
|
949
|
+
if (!await resolvesInside(path, realRoot)) continue;
|
|
950
|
+
if (!(await stat(path)).isFile()) continue;
|
|
951
|
+
const found = findRuleRegions(await readFile(path, "utf8"), "implements");
|
|
952
|
+
for (const { id, line, endLine } of found.regions) push(regions, id, {
|
|
953
|
+
file,
|
|
954
|
+
line,
|
|
955
|
+
endLine
|
|
956
|
+
});
|
|
957
|
+
problems.push(...found.problems.map((problem) => ({
|
|
958
|
+
...problem,
|
|
959
|
+
file
|
|
960
|
+
})));
|
|
961
|
+
}
|
|
962
|
+
return {
|
|
963
|
+
regions: sortById(regions),
|
|
964
|
+
problems
|
|
965
|
+
};
|
|
966
|
+
}
|
|
850
967
|
function push(index, id, value) {
|
|
851
968
|
const values = index.get(id) ?? [];
|
|
852
969
|
values.push(value);
|
|
@@ -885,6 +1002,10 @@ function writeJson(io, value) {
|
|
|
885
1002
|
function writeLines(io, lines) {
|
|
886
1003
|
io.stdout(lines.map((line) => `${line}\n`).join(""));
|
|
887
1004
|
}
|
|
1005
|
+
function shownPath(cwd, file) {
|
|
1006
|
+
const fromCwd = relative(cwd, file);
|
|
1007
|
+
return fromCwd.startsWith("..") || isAbsolute(fromCwd) ? file : toRepoPath(fromCwd);
|
|
1008
|
+
}
|
|
888
1009
|
function gitFallbackWarning(io, language) {
|
|
889
1010
|
const text = messages(language);
|
|
890
1011
|
let warned = false;
|
|
@@ -917,7 +1038,7 @@ async function lintCommand(io, options) {
|
|
|
917
1038
|
}, messages("en"))), options.json, messages("en"));
|
|
918
1039
|
}
|
|
919
1040
|
const text = messages(catalog.config.language);
|
|
920
|
-
const changed = options.base === void 0 ? void 0 : await changedFiles(catalog.root, options.base, text);
|
|
1041
|
+
const changed = options.base === void 0 ? void 0 : await changedFiles(catalog.root, options.base, text, "lint --base");
|
|
921
1042
|
const warn = gitFallbackWarning(io, catalog.config.language);
|
|
922
1043
|
const scan = await scanTestRegions(catalog.root, catalog.config.testGlobs, warn);
|
|
923
1044
|
const findings = await lintCatalog(catalog, scan, warn);
|
|
@@ -973,6 +1094,8 @@ async function lintCatalog(catalog, scan, onGitFallback) {
|
|
|
973
1094
|
line
|
|
974
1095
|
});
|
|
975
1096
|
}
|
|
1097
|
+
findings.push(...await implementations(catalog, files, invalid, successors, text));
|
|
1098
|
+
findings.push(...ruleRelations(catalog, invalid, text));
|
|
976
1099
|
const budget = catalog.config.contextBudget;
|
|
977
1100
|
for (const context of catalog.contexts) {
|
|
978
1101
|
const count = catalog.rules.filter((rule) => rule.context === context.context).length;
|
|
@@ -986,6 +1109,76 @@ async function lintCatalog(catalog, scan, onGitFallback) {
|
|
|
986
1109
|
}
|
|
987
1110
|
return findings;
|
|
988
1111
|
}
|
|
1112
|
+
async function implementations(catalog, files, invalid, successors, text) {
|
|
1113
|
+
const anchored = createGlobMatcher(catalog.rules.flatMap((rule) => rule.anchors));
|
|
1114
|
+
const scan = await scanImplementations(catalog.root, files.filter(anchored));
|
|
1115
|
+
const byId = new Map(catalog.rules.map((rule) => [rule.id, rule]));
|
|
1116
|
+
const findings = scan.problems.map(({ code, id, file, line }) => ({
|
|
1117
|
+
...code === "unclosed-implements-tag" ? error(code, file, text.unclosedImplementsTag(id), text.unclosedImplementsTagHint(id)) : error(code, file, text.unmatchedEndimplementsTag(id), text.unmatchedEndimplementsTagHint(id)),
|
|
1118
|
+
line
|
|
1119
|
+
}));
|
|
1120
|
+
for (const [id, regions] of scan.regions) {
|
|
1121
|
+
const rule = byId.get(id);
|
|
1122
|
+
if (rule === void 0 && invalid.has(id)) continue;
|
|
1123
|
+
for (const { file, line } of regions) if (rule === void 0) findings.push({
|
|
1124
|
+
...error("unknown-implements-tag", file, text.unknownImplementsTag(id), text.unknownImplementsTagHint),
|
|
1125
|
+
line
|
|
1126
|
+
});
|
|
1127
|
+
else if (rule.status === "deprecated") findings.push({
|
|
1128
|
+
...error("deprecated-implements-tag", file, text.deprecatedImplementsTag(id), text.deprecatedImplementsTagHint(id, successors.get(id))),
|
|
1129
|
+
line
|
|
1130
|
+
});
|
|
1131
|
+
else if (!createGlobMatcher(rule.anchors)(file)) findings.push({
|
|
1132
|
+
...error("implements-outside-anchors", file, text.implementsOutsideAnchors(id), text.implementsOutsideAnchorsHint(id)),
|
|
1133
|
+
line
|
|
1134
|
+
});
|
|
1135
|
+
}
|
|
1136
|
+
return findings;
|
|
1137
|
+
}
|
|
1138
|
+
const RELATION_FIELDS = ["dependsOn", "conflictsWith"];
|
|
1139
|
+
function ruleRelations(catalog, invalid, text) {
|
|
1140
|
+
const byId = new Map(catalog.rules.map((rule) => [rule.id, rule]));
|
|
1141
|
+
const findings = [];
|
|
1142
|
+
for (const rule of catalog.rules) for (const field of RELATION_FIELDS) for (const id of rule[field] ?? []) if (id === rule.id) findings.push(error("invalid-rule-reference", rule.file, text.selfRuleReference(field), text.ruleReferenceHint(field)));
|
|
1143
|
+
else if (!byId.has(id) && !invalid.has(id)) findings.push(error("invalid-rule-reference", rule.file, text.unknownRuleReference(field, id), text.ruleReferenceHint(field)));
|
|
1144
|
+
for (const cycle of dependencyCycles(byId)) findings.push(error("dependency-cycle", byId.get(cycle[0]).file, text.dependencyCycle([...cycle, cycle[0]]), text.dependencyCycleHint));
|
|
1145
|
+
const pairs = /* @__PURE__ */ new Set();
|
|
1146
|
+
for (const rule of catalog.rules) {
|
|
1147
|
+
if (rule.status !== "active") continue;
|
|
1148
|
+
for (const id of rule.conflictsWith ?? []) {
|
|
1149
|
+
if (id === rule.id || byId.get(id)?.status !== "active") continue;
|
|
1150
|
+
const [first, second] = [rule.id, id].sort(compare);
|
|
1151
|
+
const key = `${first} ${second}`;
|
|
1152
|
+
if (pairs.has(key)) continue;
|
|
1153
|
+
pairs.add(key);
|
|
1154
|
+
findings.push(error("active-rule-conflict", byId.get(first).file, text.activeRuleConflict(first, second), text.activeRuleConflictHint));
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
return findings;
|
|
1158
|
+
}
|
|
1159
|
+
function dependencyCycles(byId) {
|
|
1160
|
+
const state = /* @__PURE__ */ new Map();
|
|
1161
|
+
const stack = [];
|
|
1162
|
+
const cycles = /* @__PURE__ */ new Map();
|
|
1163
|
+
const visit = (id) => {
|
|
1164
|
+
state.set(id, "visiting");
|
|
1165
|
+
stack.push(id);
|
|
1166
|
+
for (const next of byId.get(id)?.dependsOn ?? []) {
|
|
1167
|
+
if (next === id || !byId.has(next)) continue;
|
|
1168
|
+
const seen = state.get(next);
|
|
1169
|
+
if (seen === "visiting") {
|
|
1170
|
+
const cycle = stack.slice(stack.indexOf(next));
|
|
1171
|
+
const start = cycle.indexOf([...cycle].sort(compare)[0]);
|
|
1172
|
+
const rotated = [...cycle.slice(start), ...cycle.slice(0, start)];
|
|
1173
|
+
cycles.set(rotated.join(" "), rotated);
|
|
1174
|
+
} else if (seen === void 0) visit(next);
|
|
1175
|
+
}
|
|
1176
|
+
stack.pop();
|
|
1177
|
+
state.set(id, "done");
|
|
1178
|
+
};
|
|
1179
|
+
for (const id of [...byId.keys()].sort(compare)) if (!state.has(id)) visit(id);
|
|
1180
|
+
return [...cycles.values()];
|
|
1181
|
+
}
|
|
989
1182
|
function pendingImplementation(catalog, scan, base, changed, lines) {
|
|
990
1183
|
const text = messages(catalog.config.language);
|
|
991
1184
|
return changedActiveRules(catalog, scan, changed).filter((rule) => !(scan.regions.get(rule.id) ?? []).some((region) => (lines.get(region.file) ?? []).some(([start, end]) => start <= region.endLine && end >= region.line))).map((rule) => error("pending-implementation", rule.file, text.pendingImplementation(rule.id, base), text.pendingImplementationHint(rule.id)));
|
|
@@ -1011,11 +1204,11 @@ async function testLineChanges(catalog, scan, base, changed, text) {
|
|
|
1011
1204
|
}
|
|
1012
1205
|
return lines;
|
|
1013
1206
|
}
|
|
1014
|
-
async function changedFiles(root, base, text) {
|
|
1015
|
-
if (!await isInsideWorkTree(root)) throw new CliError(text.needsWorkTree(root));
|
|
1207
|
+
async function changedFiles(root, base, text, command, options) {
|
|
1208
|
+
if (!await isInsideWorkTree(root)) throw new CliError(text.needsWorkTree(command, root));
|
|
1016
1209
|
if (!await isCommitRef(root, base)) throw new CliError(text.unknownRef(base));
|
|
1017
1210
|
try {
|
|
1018
|
-
return await changedSince(root, base);
|
|
1211
|
+
return await changedSince(root, base, options);
|
|
1019
1212
|
} catch (error) {
|
|
1020
1213
|
throw new CliError(text.cannotListChanged(base, gitFailureReason(error)));
|
|
1021
1214
|
}
|
|
@@ -1039,4 +1232,4 @@ function sortFindings(findings) {
|
|
|
1039
1232
|
function compare(a, b) {
|
|
1040
1233
|
return a < b ? -1 : a > b ? 1 : 0;
|
|
1041
1234
|
}
|
|
1042
|
-
export {
|
|
1235
|
+
export { loadRule as A, FormatError as B, runGit as C, findRuleRegions as D, RULE_TAG_PATTERN as E, splitFrontmatter as F, CONFIG_FILE_NAME as I, loadConfig as L, CONTEXT_FILE_NAME as M, loadContext as N, findRuleTags as O, parseContext as P, parseConfig as R, toRepoPath as S, RULE_MARKER_PATTERN as T, formatIssue as V, loadCatalog as _, pendingImplementation as a, expandGlobs as b, gitFallbackWarning as c, writeJson as d, writeLines as f, findRulebookRoot as g, scanTestTags as h, lintCommand as i, parseRule as j, isRuleId as k, openCatalog as l, scanTestRegions as m, formatFinding as n, CliError as o, scanImplementations as p, lintCatalog as r, findRule as s, changedFiles as t, shownPath as u, relativeToRoot as v, IMPLEMENTS_MARKER_PATTERN as w, listRepoFiles as x, createGlobMatcher as y, messages as z };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { C as runGit, S as toRepoPath, b as expandGlobs, c as gitFallbackWarning, f as writeLines, l as openCatalog, m as scanTestRegions, o as CliError, r as lintCatalog } from "./lint-BPdk6XxH.mjs";
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
3
|
import { readFile, readdir, stat, writeFile } from "node:fs/promises";
|
|
4
4
|
import { dirname, extname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
@@ -84,6 +84,9 @@ const TEXT = {
|
|
|
84
84
|
entities: "Entities",
|
|
85
85
|
supersedes: "Supersedes",
|
|
86
86
|
supersededBy: "Superseded by",
|
|
87
|
+
origin: "Origin",
|
|
88
|
+
notValidated: "not validated by the business yet",
|
|
89
|
+
evidence: "Evidence",
|
|
87
90
|
ruleFile: "Rule file",
|
|
88
91
|
ruleText: "Rule",
|
|
89
92
|
anchors: "Anchors",
|
|
@@ -123,6 +126,9 @@ const TEXT = {
|
|
|
123
126
|
entities: "Entidades",
|
|
124
127
|
supersedes: "Reemplaza a",
|
|
125
128
|
supersededBy: "Reemplazada por",
|
|
129
|
+
origin: "Origen",
|
|
130
|
+
notValidated: "sin validar todavía por el negocio",
|
|
131
|
+
evidence: "Evidencia",
|
|
126
132
|
ruleFile: "Archivo de la regla",
|
|
127
133
|
ruleText: "Regla",
|
|
128
134
|
anchors: "Anclas",
|
|
@@ -231,6 +237,8 @@ function rulePage({ rule, files, tests, history, supersedes, supersededBy }, tex
|
|
|
231
237
|
];
|
|
232
238
|
if (supersedes.length > 0) rows.push([text.supersedes, supersedes.map(ruleLink).join(" → ")]);
|
|
233
239
|
if (supersededBy !== void 0) rows.push([text.supersededBy, ruleLink(supersededBy)]);
|
|
240
|
+
if (rule.origin !== void 0) rows.push([text.origin, rule.origin === "extracted" && rule.status === "proposed" ? `${rule.origin}, ${text.notValidated}` : rule.origin]);
|
|
241
|
+
const evidence = rule.evidence ?? [];
|
|
234
242
|
return page(rule.id, rule.summary, [
|
|
235
243
|
`**${cell(rule.summary)}**`,
|
|
236
244
|
"",
|
|
@@ -242,6 +250,12 @@ function rulePage({ rule, files, tests, history, supersedes, supersededBy }, tex
|
|
|
242
250
|
"",
|
|
243
251
|
demote(rule.body.trim()),
|
|
244
252
|
"",
|
|
253
|
+
...evidence.length === 0 ? [] : [
|
|
254
|
+
`## ${text.evidence}`,
|
|
255
|
+
"",
|
|
256
|
+
...list(evidence.map((item) => `\`${item}\``), text.none),
|
|
257
|
+
""
|
|
258
|
+
],
|
|
245
259
|
`## ${text.anchors}`,
|
|
246
260
|
"",
|
|
247
261
|
...list(rule.anchors.map((anchor) => `\`${anchor}\``), text.none),
|
|
@@ -403,7 +417,7 @@ async function siteCommand(io, options, until = untilInterrupted()) {
|
|
|
403
417
|
const catalog = await openCatalog(io);
|
|
404
418
|
const out = resolve(io.cwd, options.out ?? "site");
|
|
405
419
|
const fromCwd = relative(io.cwd, out);
|
|
406
|
-
const shown = fromCwd === "" ? "." : fromCwd.startsWith("..") || isAbsolute(fromCwd) ? out : fromCwd;
|
|
420
|
+
const shown = fromCwd === "" ? "." : fromCwd.startsWith("..") || isAbsolute(fromCwd) ? out : toRepoPath(fromCwd);
|
|
407
421
|
await assertReplaceable(out, shown);
|
|
408
422
|
const site = await importSitePackage([catalog.root, dirname(fileURLToPath(import.meta.url))]);
|
|
409
423
|
const language = catalog.config.language ?? "en";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rulebookguard",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "Keep business rules as the single source of truth, bound to code and tests.",
|
|
5
5
|
"homepage": "https://github.com/JuanCJR/rulebookguard#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -36,14 +36,14 @@
|
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
38
|
"@modelcontextprotocol/client": "2.3.1",
|
|
39
|
-
"@rulebookguard/site": "0.
|
|
39
|
+
"@rulebookguard/site": "0.10.0",
|
|
40
40
|
"@types/node": "^20.19.43",
|
|
41
41
|
"@types/picomatch": "^4.0.3",
|
|
42
42
|
"typescript": "^7.0.2",
|
|
43
43
|
"vite-plus": "^1.4.0"
|
|
44
44
|
},
|
|
45
45
|
"peerDependencies": {
|
|
46
|
-
"@rulebookguard/site": "^0.
|
|
46
|
+
"@rulebookguard/site": "^0.10.0"
|
|
47
47
|
},
|
|
48
48
|
"peerDependenciesMeta": {
|
|
49
49
|
"@rulebookguard/site": {
|
|
@@ -12,6 +12,10 @@
|
|
|
12
12
|
"entities": { "type": "array", "items": { "type": "string", "minLength": 1 } },
|
|
13
13
|
"status": { "enum": ["proposed", "active", "deprecated"] },
|
|
14
14
|
"supersedes": { "$ref": "#/$defs/ruleId" },
|
|
15
|
+
"dependsOn": { "type": "array", "items": { "$ref": "#/$defs/ruleId" } },
|
|
16
|
+
"conflictsWith": { "type": "array", "items": { "$ref": "#/$defs/ruleId" } },
|
|
17
|
+
"origin": { "enum": ["authored", "extracted"] },
|
|
18
|
+
"evidence": { "type": "array", "items": { "type": "string", "minLength": 1 } },
|
|
15
19
|
"anchors": { "type": "array", "items": { "type": "string", "minLength": 1 } },
|
|
16
20
|
"summary": { "type": "string", "minLength": 1, "pattern": "^[^\\r\\n]*$" }
|
|
17
21
|
},
|