doctrina-cli 0.15.1 → 0.17.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 +27 -72
- package/package.json +3 -3
- package/src/commands/adapter.js +27 -3
- package/src/commands/analyze.js +16 -212
- package/src/commands/change.js +144 -267
- package/src/commands/ci.js +182 -0
- package/src/commands/clarify.js +65 -21
- package/src/commands/close.js +349 -45
- package/src/commands/completion.js +8 -2
- package/src/commands/context.js +284 -111
- package/src/commands/contract.js +95 -75
- package/src/commands/coverage.js +58 -273
- package/src/commands/decision.js +66 -7
- package/src/commands/doctor.js +293 -130
- package/src/commands/handoff.js +10 -57
- package/src/commands/hooks.js +17 -3
- package/src/commands/index-rebuild.js +55 -44
- package/src/commands/init.js +88 -16
- package/src/commands/intake.js +85 -99
- package/src/commands/intent.js +14 -1
- package/src/commands/metrics.js +65 -88
- package/src/commands/next.js +105 -147
- package/src/commands/prime.js +23 -91
- package/src/commands/report.js +40 -109
- package/src/commands/review.js +68 -36
- package/src/commands/search.js +18 -4
- package/src/commands/show.js +24 -9
- package/src/commands/skill.js +23 -39
- package/src/commands/spec.js +38 -11
- package/src/commands/status.js +69 -171
- package/src/commands/templates.js +35 -148
- package/src/commands/trace.js +36 -71
- package/src/commands/triage.js +4 -82
- package/src/commands/upgrade.js +31 -6
- package/src/commands/validate.js +44 -877
- package/src/commands/verify.js +60 -138
- package/src/commands/why.js +2 -2
- package/src/commands/work.js +243 -267
- package/src/index.js +139 -12
- package/src/lib/actions.js +490 -0
- package/src/lib/adapters.js +0 -11
- package/src/lib/adr-guard.js +19 -0
- package/src/lib/agent-changelog.js +133 -0
- package/src/lib/analysis.js +264 -0
- package/src/lib/args.js +14 -0
- package/src/lib/change-ops.js +133 -0
- package/src/lib/commands.js +135 -10
- package/src/lib/config.js +206 -0
- package/src/lib/constitution-model.js +102 -0
- package/src/lib/coverage-model.js +431 -0
- package/src/lib/criteria.js +23 -0
- package/src/lib/doc-model.js +370 -3
- package/src/lib/docs-impact.js +270 -27
- package/src/lib/ears.js +24 -1
- package/src/lib/flag-catalog.js +0 -16
- package/src/lib/fs-ops.js +58 -1
- package/src/lib/gates.js +231 -29
- package/src/lib/git.js +132 -0
- package/src/lib/index-json.js +42 -24
- package/src/lib/intake-model.js +155 -0
- package/src/lib/json-out.js +100 -2
- package/src/lib/ledger.js +192 -0
- package/src/lib/lexicon.js +228 -0
- package/src/lib/metrics-model.js +189 -0
- package/src/lib/names.js +42 -0
- package/src/lib/playbook.js +97 -0
- package/src/lib/project.js +105 -0
- package/src/lib/reproducibility.js +137 -0
- package/src/lib/runtime.js +191 -5
- package/src/lib/scan.js +328 -25
- package/src/lib/signoff.js +147 -0
- package/src/lib/snapshot.js +231 -0
- package/src/lib/spec-ops.js +72 -8
- package/src/lib/templates-model.js +289 -0
- package/src/lib/trace-model.js +95 -0
- package/src/lib/triage-model.js +121 -0
- package/src/lib/usage.js +6 -4
- package/src/lib/validation-model.js +1381 -0
- package/src/lib/version.js +22 -0
- package/src/lib/views.js +413 -0
- package/src/lib/work-model.js +102 -0
- package/templates/AGENTS.md.template +21 -28
- package/templates/README.md +35 -0
- package/templates/adapters/claude/.claude/commands/doctrina-work.md.template +6 -3
- package/templates/adapters/cursor/.cursor/commands/doctrina-work.md.template +4 -3
- package/templates/change/proposal.md.template +16 -0
- package/templates/change/tasks.md.template +3 -8
- package/templates/contract.md.template +2 -2
- package/templates/hooks/pre-commit.sample +16 -2
- package/templates/playbooks/bootstrap.md.template +47 -0
- package/templates/playbooks/chore.md.template +24 -0
- package/templates/playbooks/work.md.template +83 -0
- package/src/commands/constitution.js +0 -145
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import process from "node:process";
|
|
3
|
+
import { flagString } from "../lib/args.js";
|
|
4
|
+
import { c } from "../lib/colors.js";
|
|
5
|
+
import { suggest } from "../lib/suggest.js";
|
|
6
|
+
import { sequence } from "../lib/gates.js";
|
|
7
|
+
|
|
8
|
+
// The CI surface, GENERATED from the gate declaration (audit finding F1).
|
|
9
|
+
//
|
|
10
|
+
// `action.yml` used to be a fourth hand-maintained list of gates, beside
|
|
11
|
+
// `close`'s array and `doctor`'s rows. Nothing noticed when they diverged,
|
|
12
|
+
// so a gate could be in the close and absent from CI for a release — which
|
|
13
|
+
// is exactly what happened to the runtime checks.
|
|
14
|
+
//
|
|
15
|
+
// The action stays VERSIONED rather than generated on demand: a project that
|
|
16
|
+
// writes `uses: GCarin1/Doctrina@v1` has no CLI to generate it with, and a
|
|
17
|
+
// composite action that only exists after an npm install is not an action.
|
|
18
|
+
// So this command emits it, and a drift test holds the committed file to
|
|
19
|
+
// what this emits — the same pattern the AGENTS.md surface block uses.
|
|
20
|
+
|
|
21
|
+
const TARGETS = ["github"];
|
|
22
|
+
|
|
23
|
+
// Flags this command accepts. Declared HERE, with the command, so
|
|
24
|
+
// adding a command never requires editing the entrypoint.
|
|
25
|
+
export const flags = { boolean: ["json"], string: ["emit"] };
|
|
26
|
+
|
|
27
|
+
export async function run(_positional, flags) {
|
|
28
|
+
const target = flagString(flags, "emit", "");
|
|
29
|
+
if (!target) {
|
|
30
|
+
console.error(c.red("error:") + " ci requires --emit <target>");
|
|
31
|
+
console.error(c.gray("hint: ") + `available targets: ${TARGETS.join(", ")}`);
|
|
32
|
+
return 2;
|
|
33
|
+
}
|
|
34
|
+
if (!TARGETS.includes(target)) {
|
|
35
|
+
console.error(c.red("error:") + ` unknown CI target "${target}"`);
|
|
36
|
+
const guess = suggest(target, TARGETS);
|
|
37
|
+
console.error(c.gray("hint: ") + (guess ? `did you mean --emit ${guess}?` : `available: ${TARGETS.join(", ")}`));
|
|
38
|
+
return 2;
|
|
39
|
+
}
|
|
40
|
+
process.stdout.write(emitGithub());
|
|
41
|
+
return 0;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The composite action, rendered from SEQUENCES.ci.
|
|
46
|
+
*
|
|
47
|
+
* Takes the sequence as a parameter so a test can prove the surface follows
|
|
48
|
+
* the DECLARATION rather than a copy of it: add a step to the array passed
|
|
49
|
+
* in, and it appears in the YAML with no edit here.
|
|
50
|
+
*/
|
|
51
|
+
export function emitGithub(steps = sequence("ci")) {
|
|
52
|
+
// Two renderings of the same list: the file header says what each gate
|
|
53
|
+
// checks, the action's description just names them.
|
|
54
|
+
const gateSummaries = steps.map((s) => s.summary ?? s.label.replace(/^doctrina /, ""));
|
|
55
|
+
const gateNames = steps.map((s) => s.short ?? s.label.replace(/^doctrina /, "")).join(", ");
|
|
56
|
+
const out = [];
|
|
57
|
+
|
|
58
|
+
out.push("# Reusable composite action: run the Doctrina gates in CI.");
|
|
59
|
+
out.push("#");
|
|
60
|
+
out.push("# - uses: GCarin1/Doctrina@main # or a released tag");
|
|
61
|
+
out.push("# with:");
|
|
62
|
+
out.push("# strict: \"true\" # coverage/trace as hard gates");
|
|
63
|
+
out.push("#");
|
|
64
|
+
out.push("# GENERATED by `doctrina ci --emit github` from SEQUENCES.ci in");
|
|
65
|
+
out.push("# packages/doctrina-cli/src/lib/gates.js — the one declaration `close` and");
|
|
66
|
+
out.push("# `doctor` also read. Edit the declaration, re-emit, and commit; a drift");
|
|
67
|
+
out.push("# test fails when this file and the declaration disagree.");
|
|
68
|
+
out.push("#");
|
|
69
|
+
for (const line of wrapComment(`The gates: ${gateSummaries.join(", ")}.`, 74)) out.push(`# ${line}`);
|
|
70
|
+
out.push("#");
|
|
71
|
+
out.push("# `run-prefix` exists so this repository can dogfood the action against its");
|
|
72
|
+
out.push("# own working tree instead of the published package.");
|
|
73
|
+
out.push("name: \"Doctrina gates\"");
|
|
74
|
+
out.push(`description: "Run the Doctrina gates (${gateNames}) against a project's .doctrina/ tree."`);
|
|
75
|
+
out.push("branding:");
|
|
76
|
+
out.push(" icon: \"check-circle\"");
|
|
77
|
+
out.push(" color: \"purple\"");
|
|
78
|
+
out.push("");
|
|
79
|
+
out.push("inputs:");
|
|
80
|
+
out.push(" strict:");
|
|
81
|
+
out.push(" description: \"Run coverage and trace with --strict (fail on any gap). Set to 'false' for report-only.\"");
|
|
82
|
+
out.push(" required: false");
|
|
83
|
+
out.push(" default: \"true\"");
|
|
84
|
+
out.push(" version:");
|
|
85
|
+
out.push(" description: \"doctrina-cli version to run via npx (ignored when run-prefix is set).\"");
|
|
86
|
+
out.push(" required: false");
|
|
87
|
+
out.push(" default: \"latest\"");
|
|
88
|
+
out.push(" working-directory:");
|
|
89
|
+
out.push(" description: \"Directory holding the .doctrina/ tree.\"");
|
|
90
|
+
out.push(" required: false");
|
|
91
|
+
out.push(" default: \".\"");
|
|
92
|
+
out.push(" run-prefix:");
|
|
93
|
+
out.push(" description: \"Override the command used to invoke the CLI (e.g. 'node packages/doctrina-cli/src/index.js' for this repo's own CI).\"");
|
|
94
|
+
out.push(" required: false");
|
|
95
|
+
out.push(" default: \"\"");
|
|
96
|
+
out.push("");
|
|
97
|
+
out.push("runs:");
|
|
98
|
+
out.push(" using: \"composite\"");
|
|
99
|
+
out.push(" steps:");
|
|
100
|
+
out.push(" - name: Resolve CLI invocation");
|
|
101
|
+
out.push(" id: cli");
|
|
102
|
+
out.push(" shell: bash");
|
|
103
|
+
out.push(" working-directory: ${{ inputs.working-directory }}");
|
|
104
|
+
out.push(" run: |");
|
|
105
|
+
out.push(" PREFIX=\"${{ inputs.run-prefix }}\"");
|
|
106
|
+
out.push(" if [ -z \"$PREFIX\" ]; then PREFIX=\"npx --yes doctrina-cli@${{ inputs.version }}\"; fi");
|
|
107
|
+
out.push(" echo \"prefix=$PREFIX\" >> \"$GITHUB_OUTPUT\"");
|
|
108
|
+
|
|
109
|
+
const prefix = "${{ steps.cli.outputs.prefix }}";
|
|
110
|
+
for (const step of steps) {
|
|
111
|
+
out.push("");
|
|
112
|
+
if (step.why) {
|
|
113
|
+
for (const line of step.why.split("\n")) out.push(` # ${line}`.trimEnd());
|
|
114
|
+
}
|
|
115
|
+
out.push(` - name: ${step.label}`);
|
|
116
|
+
out.push(" shell: bash");
|
|
117
|
+
out.push(" working-directory: ${{ inputs.working-directory }}");
|
|
118
|
+
|
|
119
|
+
const command = `${prefix} ${(step.argv ?? []).join(" ")}`.trim();
|
|
120
|
+
if (step.script) {
|
|
121
|
+
out.push(" run: |");
|
|
122
|
+
for (const line of step.script.replaceAll("{{PREFIX}}", prefix).split("\n")) {
|
|
123
|
+
out.push(` ${line}`.trimEnd());
|
|
124
|
+
}
|
|
125
|
+
} else if (step.strict) {
|
|
126
|
+
// The one shape the declaration cannot flatten to a single line: the
|
|
127
|
+
// action's `strict` input decides whether the gate fails on a gap or
|
|
128
|
+
// only reports it, and an adopting project in its backfill phase needs
|
|
129
|
+
// the report-only half.
|
|
130
|
+
out.push(" run: |");
|
|
131
|
+
out.push(" if [ \"${{ inputs.strict }}\" = \"true\" ]; then");
|
|
132
|
+
out.push(` ${command} --strict`);
|
|
133
|
+
out.push(" else");
|
|
134
|
+
out.push(` ${command}`);
|
|
135
|
+
out.push(" fi");
|
|
136
|
+
} else {
|
|
137
|
+
out.push(` run: ${command}`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
out.push("");
|
|
141
|
+
return out.join("\n");
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Wrap prose to a column so the emitted comments read like hand-written ones
|
|
145
|
+
// rather than one very long line the reader has to scroll.
|
|
146
|
+
function wrapComment(text, width) {
|
|
147
|
+
const out = [];
|
|
148
|
+
let line = "";
|
|
149
|
+
for (const word of text.split(/\s+/)) {
|
|
150
|
+
if (line && (line + " " + word).length > width) {
|
|
151
|
+
out.push(line);
|
|
152
|
+
line = word;
|
|
153
|
+
} else {
|
|
154
|
+
line = line ? `${line} ${word}` : word;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
if (line) out.push(line);
|
|
158
|
+
return out;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export const help = `
|
|
162
|
+
Usage: doctrina ci --emit <target>
|
|
163
|
+
|
|
164
|
+
Emit the CI pipeline for the declared gate sequence, on stdout.
|
|
165
|
+
|
|
166
|
+
The gates a pipeline runs are declared once, in SEQUENCES.ci
|
|
167
|
+
(packages/doctrina-cli/src/lib/gates.js) — the same declaration \`close\`
|
|
168
|
+
executes and \`doctor\` reports. This command renders that declaration as CI
|
|
169
|
+
configuration, so the pipeline cannot fall behind the sequence the way a
|
|
170
|
+
hand-maintained YAML file did.
|
|
171
|
+
|
|
172
|
+
Targets:
|
|
173
|
+
github A composite GitHub Action (the repository's own action.yml).
|
|
174
|
+
|
|
175
|
+
The action stays versioned in the repository: a project that writes
|
|
176
|
+
\`uses: <owner>/<repo>@v1\` has no CLI to generate it with. Re-emit after
|
|
177
|
+
changing the declaration and commit the result; the drift test compares the
|
|
178
|
+
committed file against this output.
|
|
179
|
+
|
|
180
|
+
Read-only — it writes nothing, so redirect it yourself:
|
|
181
|
+
doctrina ci --emit github > action.yml
|
|
182
|
+
`;
|
package/src/commands/clarify.js
CHANGED
|
@@ -5,7 +5,9 @@ import { readdirSync } from "node:fs";
|
|
|
5
5
|
import { exists, isDir, isFile, read, relPath, walk } from "../lib/fs-ops.js";
|
|
6
6
|
import { flagBool, flagString } from "../lib/args.js";
|
|
7
7
|
import { c } from "../lib/colors.js";
|
|
8
|
-
import {
|
|
8
|
+
import { detectLanguage } from "../lib/lexicon.js";
|
|
9
|
+
import { loadConfig } from "../lib/config.js";
|
|
10
|
+
import { EXIT, notADoctrinaProject } from "../lib/exit-codes.js";
|
|
9
11
|
|
|
10
12
|
// Two lexicons, one linter (0.11.0 field review item 3: an English-only
|
|
11
13
|
// clarify is permanently red on a PT-BR project — "some" is the verb *sumir*,
|
|
@@ -26,7 +28,23 @@ const RULES_EN = [
|
|
|
26
28
|
},
|
|
27
29
|
{
|
|
28
30
|
name: "vague",
|
|
29
|
-
|
|
31
|
+
// Two exclusions, both structural rather than a vocabulary loosening.
|
|
32
|
+
// The lookahead was already here: `many 5` quantifies. The lookbehind is
|
|
33
|
+
// change 0079: `how many` is INTERROGATIVE, and a requirement that says
|
|
34
|
+
// "shall report how many contracts declared no rows" names exactly the
|
|
35
|
+
// number the command must print — the opposite of vague. Five of this
|
|
36
|
+
// repository's seventeen smells were that phrase, and the noise is what
|
|
37
|
+
// hid the one real finding among them ("name some of them", which said
|
|
38
|
+
// nothing about how many). A gate that cries wolf teaches people to
|
|
39
|
+
// ignore it.
|
|
40
|
+
//
|
|
41
|
+
// The quality adjectives sit here for the same reason the quantifiers do:
|
|
42
|
+
// "robust" names no threshold, so it cannot be met or missed. They were
|
|
43
|
+
// in the Portuguese lexicon and nowhere in this one, so the SAME sentence
|
|
44
|
+
// translated came back with three smells in one language and none in the
|
|
45
|
+
// other — and this project keeps its own pages in both. A smell test that
|
|
46
|
+
// answers differently per language is not testing the writing.
|
|
47
|
+
re: /(?<!\bhow\s+)\b(many|few|some|several|adequate|robust|scalable)\b(?!\s+\d)/gi,
|
|
30
48
|
hint: "quantify (use a number or a precise scope)",
|
|
31
49
|
},
|
|
32
50
|
{
|
|
@@ -36,6 +54,10 @@ const RULES_EN = [
|
|
|
36
54
|
},
|
|
37
55
|
];
|
|
38
56
|
|
|
57
|
+
// How much of the previous line a rule may look back into. Long enough for
|
|
58
|
+
// a wrapped two-word phrase, short enough that the probe stays cheap.
|
|
59
|
+
const LOOKBEHIND_TAIL = 40;
|
|
60
|
+
|
|
39
61
|
const RULES_PT = [
|
|
40
62
|
{
|
|
41
63
|
name: "weasel",
|
|
@@ -59,30 +81,34 @@ const RULES_PT = [
|
|
|
59
81
|
// Language for a file: the project-declared `.doctrina/config.json`
|
|
60
82
|
// ("language": "pt-BR" / "pt" / "en") wins; otherwise a deterministic
|
|
61
83
|
// stopword count over the file text decides. Never semantic (ADR 0005).
|
|
62
|
-
const PT_STOPWORDS = /\b(que|n[aã]o|para|uma|como|mais|ser|quando|est[aá]|s[aã]o|pela|pelo|dos|das|ou seja|deve)\b/gi;
|
|
63
|
-
const EN_STOPWORDS = /\b(the|and|that|with|shall|when|this|from|are|not|for|must)\b/gi;
|
|
64
84
|
|
|
65
85
|
function projectLanguage(projectRoot) {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
if (lang.startsWith("pt")) return "pt";
|
|
71
|
-
if (lang.startsWith("en")) return "en";
|
|
72
|
-
} catch { /* fall through to detection */ }
|
|
73
|
-
}
|
|
74
|
-
return null;
|
|
86
|
+
// One configuration reader (lib/config.js, change 0047): the key still
|
|
87
|
+
// lives in .doctrina/config.json, but `doctor` can now show what it
|
|
88
|
+
// resolved to, which is what a pt-BR project sitting red needed.
|
|
89
|
+
return loadConfig(projectRoot).language;
|
|
75
90
|
}
|
|
76
91
|
|
|
92
|
+
// Which lexicon a document is written in — the shared detector (change
|
|
93
|
+
// 0040), so `clarify` and any future consumer count the same grammar words.
|
|
77
94
|
function detectLang(text) {
|
|
78
|
-
|
|
79
|
-
const en = (text.match(EN_STOPWORDS) ?? []).length;
|
|
80
|
-
return pt > en ? "pt" : "en";
|
|
95
|
+
return detectLanguage(text);
|
|
81
96
|
}
|
|
82
97
|
|
|
83
98
|
function rulesFor(projectRoot, text, forcedLang = null) {
|
|
84
|
-
const lang = forcedLang ?? projectLanguage(projectRoot)
|
|
85
|
-
return lang === "pt" ? RULES_PT : RULES_EN;
|
|
99
|
+
const lang = forcedLang ?? projectLanguage(projectRoot);
|
|
100
|
+
if (lang) return lang === "pt" ? RULES_PT : RULES_EN;
|
|
101
|
+
// Detected per file, the document is scanned with BOTH lexicons (change
|
|
102
|
+
// 0107). A bilingual spec — Portuguese purpose, English EARS, or the
|
|
103
|
+
// reverse — detected as one language hid every smell written in the
|
|
104
|
+
// other: `clarify --all` found nothing where `--lang pt` found two. The
|
|
105
|
+
// two vocabularies do not overlap, so a smell in either is a smell; the
|
|
106
|
+
// detected language only decides which hint text the reader gets first.
|
|
107
|
+
const detected = detectLang(text);
|
|
108
|
+
const [first, second] = detected === "pt" ? [RULES_PT, RULES_EN] : [RULES_EN, RULES_PT];
|
|
109
|
+
// The `placeholder` rule is language-neutral and lives in both lists:
|
|
110
|
+
// one copy, or TBD is reported twice.
|
|
111
|
+
return [...first, ...second.filter((r) => r.name !== "placeholder")];
|
|
86
112
|
}
|
|
87
113
|
|
|
88
114
|
// --lang pt|en wins over the project config and the per-file heuristic
|
|
@@ -117,8 +143,15 @@ export async function run(positional, flags) {
|
|
|
117
143
|
}
|
|
118
144
|
const fullPath = path.resolve(projectRoot, target);
|
|
119
145
|
if (!isFile(fullPath)) {
|
|
146
|
+
// USAGE, not GATE (third audit, finding 9). A path that does not exist
|
|
147
|
+
// is a wrong invocation: retrying it unchanged never succeeds, and an
|
|
148
|
+
// agent branching on the exit code reads 1 as "the work is not ready"
|
|
149
|
+
// and iterates forever (ADR 0018). Every sibling that takes a path —
|
|
150
|
+
// `intake --file`, `init --intake-file`, `templates check --path` —
|
|
151
|
+
// already exits 2 here; `clarify` was the straggler.
|
|
120
152
|
console.error(c.red("error:") + ` file not found: ${target}`);
|
|
121
|
-
|
|
153
|
+
console.error(c.gray("hint: ") + "pass a path to an existing Markdown file, or `--all` for the tree");
|
|
154
|
+
return EXIT.USAGE;
|
|
122
155
|
}
|
|
123
156
|
|
|
124
157
|
console.log(`clarify ${relPath(projectRoot, fullPath)}`);
|
|
@@ -208,11 +241,22 @@ function scanFile(fullPath, projectRoot = process.cwd(), lang = null) {
|
|
|
208
241
|
cursor = lineEnd + 1;
|
|
209
242
|
continue;
|
|
210
243
|
}
|
|
244
|
+
// A rule reads the previous line as CONTEXT, never as content: a match
|
|
245
|
+
// is only reported when it starts inside this line. Prose wraps, and
|
|
246
|
+
// "report per spec how / many criteria..." split the interrogative
|
|
247
|
+
// across a line break — which a line-at-a-time scanner cannot see, so
|
|
248
|
+
// the phrase came back as a smell on one line of the same document and
|
|
249
|
+
// not on the next (change 0079). The prefix is trimmed to the tail of
|
|
250
|
+
// the previous line: enough for a lookbehind, cheap on every line.
|
|
251
|
+
const prev = i > 0 ? lines[i - 1].slice(-LOOKBEHIND_TAIL) : "";
|
|
252
|
+
const probe = prev === "" ? line : `${prev}\n${line}`;
|
|
253
|
+
const offset = probe.length - line.length;
|
|
211
254
|
for (const rule of rules) {
|
|
212
255
|
rule.re.lastIndex = 0;
|
|
213
256
|
let m;
|
|
214
|
-
while ((m = rule.re.exec(
|
|
215
|
-
|
|
257
|
+
while ((m = rule.re.exec(probe))) {
|
|
258
|
+
if (m.index < offset) continue; // the match belongs to the previous line
|
|
259
|
+
const absoluteStart = lineStart + m.index - offset;
|
|
216
260
|
if (isInSkippedRange(absoluteStart, skippedRanges)) continue;
|
|
217
261
|
smells.push({
|
|
218
262
|
line: i + 1,
|