qfai 0.3.8 → 0.4.2
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 +12 -0
- package/assets/init/.qfai/README.md +5 -0
- package/assets/init/.qfai/prompts/README.md +1 -0
- package/assets/init/.qfai/prompts/qfai-generate-test-globs.md +29 -0
- package/assets/init/.qfai/specs/README.md +1 -1
- package/assets/init/root/qfai.config.yaml +8 -0
- package/assets/init/root/tests/qfai-traceability.sample.test.ts +2 -0
- package/dist/cli/index.cjs +717 -364
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.d.ts +0 -2
- package/dist/cli/index.mjs +719 -366
- package/dist/cli/index.mjs.map +1 -1
- package/dist/index.cjs +710 -351
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +26 -3
- package/dist/index.d.ts +156 -2
- package/dist/index.mjs +714 -354
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -1
- package/dist/cli/commands/init.d.ts +0 -8
- package/dist/cli/commands/init.d.ts.map +0 -1
- package/dist/cli/commands/init.js +0 -30
- package/dist/cli/commands/init.js.map +0 -1
- package/dist/cli/commands/report.d.ts +0 -7
- package/dist/cli/commands/report.d.ts.map +0 -1
- package/dist/cli/commands/report.js +0 -80
- package/dist/cli/commands/report.js.map +0 -1
- package/dist/cli/commands/validate.d.ts +0 -9
- package/dist/cli/commands/validate.d.ts.map +0 -1
- package/dist/cli/commands/validate.js +0 -57
- package/dist/cli/commands/validate.js.map +0 -1
- package/dist/cli/index.d.ts.map +0 -1
- package/dist/cli/index.js +0 -7
- package/dist/cli/index.js.map +0 -1
- package/dist/cli/lib/args.d.ts +0 -18
- package/dist/cli/lib/args.d.ts.map +0 -1
- package/dist/cli/lib/args.js +0 -98
- package/dist/cli/lib/args.js.map +0 -1
- package/dist/cli/lib/assets.d.ts +0 -2
- package/dist/cli/lib/assets.d.ts.map +0 -1
- package/dist/cli/lib/assets.js +0 -24
- package/dist/cli/lib/assets.js.map +0 -1
- package/dist/cli/lib/failOn.d.ts +0 -5
- package/dist/cli/lib/failOn.d.ts.map +0 -1
- package/dist/cli/lib/failOn.js +0 -10
- package/dist/cli/lib/failOn.js.map +0 -1
- package/dist/cli/lib/fs.d.ts +0 -11
- package/dist/cli/lib/fs.d.ts.map +0 -1
- package/dist/cli/lib/fs.js +0 -91
- package/dist/cli/lib/fs.js.map +0 -1
- package/dist/cli/lib/logger.d.ts +0 -4
- package/dist/cli/lib/logger.d.ts.map +0 -1
- package/dist/cli/lib/logger.js +0 -10
- package/dist/cli/lib/logger.js.map +0 -1
- package/dist/cli/main.d.ts +0 -2
- package/dist/cli/main.d.ts.map +0 -1
- package/dist/cli/main.js +0 -66
- package/dist/cli/main.js.map +0 -1
- package/dist/core/config.d.ts +0 -44
- package/dist/core/config.d.ts.map +0 -1
- package/dist/core/config.js +0 -218
- package/dist/core/config.js.map +0 -1
- package/dist/core/contractIndex.d.ts +0 -13
- package/dist/core/contractIndex.d.ts.map +0 -1
- package/dist/core/contractIndex.js +0 -66
- package/dist/core/contractIndex.js.map +0 -1
- package/dist/core/contracts.d.ts +0 -5
- package/dist/core/contracts.d.ts.map +0 -1
- package/dist/core/contracts.js +0 -42
- package/dist/core/contracts.js.map +0 -1
- package/dist/core/discovery.d.ts +0 -14
- package/dist/core/discovery.d.ts.map +0 -1
- package/dist/core/discovery.js +0 -55
- package/dist/core/discovery.js.map +0 -1
- package/dist/core/fs.d.ts +0 -6
- package/dist/core/fs.d.ts.map +0 -1
- package/dist/core/fs.js +0 -55
- package/dist/core/fs.js.map +0 -1
- package/dist/core/gherkin/parse.d.ts +0 -7
- package/dist/core/gherkin/parse.d.ts.map +0 -1
- package/dist/core/gherkin/parse.js +0 -25
- package/dist/core/gherkin/parse.js.map +0 -1
- package/dist/core/ids.d.ts +0 -6
- package/dist/core/ids.d.ts.map +0 -1
- package/dist/core/ids.js +0 -52
- package/dist/core/ids.js.map +0 -1
- package/dist/core/index.d.ts +0 -13
- package/dist/core/index.d.ts.map +0 -1
- package/dist/core/index.js +0 -13
- package/dist/core/index.js.map +0 -1
- package/dist/core/parse/adr.d.ts +0 -13
- package/dist/core/parse/adr.d.ts.map +0 -1
- package/dist/core/parse/adr.js +0 -33
- package/dist/core/parse/adr.js.map +0 -1
- package/dist/core/parse/gherkin.d.ts +0 -12
- package/dist/core/parse/gherkin.d.ts.map +0 -1
- package/dist/core/parse/gherkin.js +0 -22
- package/dist/core/parse/gherkin.js.map +0 -1
- package/dist/core/parse/markdown.d.ts +0 -14
- package/dist/core/parse/markdown.d.ts.map +0 -1
- package/dist/core/parse/markdown.js +0 -45
- package/dist/core/parse/markdown.js.map +0 -1
- package/dist/core/parse/spec.d.ts +0 -28
- package/dist/core/parse/spec.d.ts.map +0 -1
- package/dist/core/parse/spec.js +0 -80
- package/dist/core/parse/spec.js.map +0 -1
- package/dist/core/report.d.ts +0 -39
- package/dist/core/report.d.ts.map +0 -1
- package/dist/core/report.js +0 -226
- package/dist/core/report.js.map +0 -1
- package/dist/core/scenarioModel.d.ts +0 -33
- package/dist/core/scenarioModel.d.ts.map +0 -1
- package/dist/core/scenarioModel.js +0 -130
- package/dist/core/scenarioModel.js.map +0 -1
- package/dist/core/specLayout.d.ts +0 -8
- package/dist/core/specLayout.d.ts.map +0 -1
- package/dist/core/specLayout.js +0 -36
- package/dist/core/specLayout.js.map +0 -1
- package/dist/core/types.d.ts +0 -25
- package/dist/core/types.d.ts.map +0 -1
- package/dist/core/types.js +0 -2
- package/dist/core/types.js.map +0 -1
- package/dist/core/validate.d.ts +0 -4
- package/dist/core/validate.d.ts.map +0 -1
- package/dist/core/validate.js +0 -34
- package/dist/core/validate.js.map +0 -1
- package/dist/core/validators/contracts.d.ts +0 -5
- package/dist/core/validators/contracts.d.ts.map +0 -1
- package/dist/core/validators/contracts.js +0 -162
- package/dist/core/validators/contracts.js.map +0 -1
- package/dist/core/validators/delta.d.ts +0 -4
- package/dist/core/validators/delta.d.ts.map +0 -1
- package/dist/core/validators/delta.js +0 -68
- package/dist/core/validators/delta.js.map +0 -1
- package/dist/core/validators/ids.d.ts +0 -4
- package/dist/core/validators/ids.d.ts.map +0 -1
- package/dist/core/validators/ids.js +0 -88
- package/dist/core/validators/ids.js.map +0 -1
- package/dist/core/validators/scenario.d.ts +0 -5
- package/dist/core/validators/scenario.d.ts.map +0 -1
- package/dist/core/validators/scenario.js +0 -140
- package/dist/core/validators/scenario.js.map +0 -1
- package/dist/core/validators/spec.d.ts +0 -5
- package/dist/core/validators/spec.d.ts.map +0 -1
- package/dist/core/validators/spec.js +0 -94
- package/dist/core/validators/spec.js.map +0 -1
- package/dist/core/validators/traceability.d.ts +0 -4
- package/dist/core/validators/traceability.d.ts.map +0 -1
- package/dist/core/validators/traceability.js +0 -180
- package/dist/core/validators/traceability.js.map +0 -1
- package/dist/core/version.d.ts +0 -2
- package/dist/core/version.d.ts.map +0 -1
- package/dist/core/version.js +0 -25
- package/dist/core/version.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -2
- package/dist/index.js.map +0 -1
- package/dist/tsconfig.tsbuildinfo +0 -1
package/dist/cli/index.mjs
CHANGED
|
@@ -154,8 +154,8 @@ function report(copied, skipped, dryRun, label) {
|
|
|
154
154
|
}
|
|
155
155
|
|
|
156
156
|
// src/cli/commands/report.ts
|
|
157
|
-
import { mkdir as mkdir2, readFile as
|
|
158
|
-
import
|
|
157
|
+
import { mkdir as mkdir2, readFile as readFile12, writeFile } from "fs/promises";
|
|
158
|
+
import path15 from "path";
|
|
159
159
|
|
|
160
160
|
// src/core/config.ts
|
|
161
161
|
import { readFile } from "fs/promises";
|
|
@@ -187,6 +187,10 @@ var defaultConfig = {
|
|
|
187
187
|
traceability: {
|
|
188
188
|
brMustHaveSc: true,
|
|
189
189
|
scMustTouchContracts: true,
|
|
190
|
+
scMustHaveTest: true,
|
|
191
|
+
testFileGlobs: [],
|
|
192
|
+
testFileExcludeGlobs: [],
|
|
193
|
+
scNoTestSeverity: "error",
|
|
190
194
|
allowOrphanContracts: false,
|
|
191
195
|
unknownContractIdSeverity: "error"
|
|
192
196
|
}
|
|
@@ -366,6 +370,34 @@ function normalizeValidation(raw, configPath, issues) {
|
|
|
366
370
|
configPath,
|
|
367
371
|
issues
|
|
368
372
|
),
|
|
373
|
+
scMustHaveTest: readBoolean(
|
|
374
|
+
traceabilityRaw?.scMustHaveTest,
|
|
375
|
+
base.traceability.scMustHaveTest,
|
|
376
|
+
"validation.traceability.scMustHaveTest",
|
|
377
|
+
configPath,
|
|
378
|
+
issues
|
|
379
|
+
),
|
|
380
|
+
testFileGlobs: readStringArray(
|
|
381
|
+
traceabilityRaw?.testFileGlobs,
|
|
382
|
+
base.traceability.testFileGlobs,
|
|
383
|
+
"validation.traceability.testFileGlobs",
|
|
384
|
+
configPath,
|
|
385
|
+
issues
|
|
386
|
+
),
|
|
387
|
+
testFileExcludeGlobs: readStringArray(
|
|
388
|
+
traceabilityRaw?.testFileExcludeGlobs,
|
|
389
|
+
base.traceability.testFileExcludeGlobs,
|
|
390
|
+
"validation.traceability.testFileExcludeGlobs",
|
|
391
|
+
configPath,
|
|
392
|
+
issues
|
|
393
|
+
),
|
|
394
|
+
scNoTestSeverity: readTraceabilitySeverity(
|
|
395
|
+
traceabilityRaw?.scNoTestSeverity,
|
|
396
|
+
base.traceability.scNoTestSeverity,
|
|
397
|
+
"validation.traceability.scNoTestSeverity",
|
|
398
|
+
configPath,
|
|
399
|
+
issues
|
|
400
|
+
),
|
|
369
401
|
allowOrphanContracts: readBoolean(
|
|
370
402
|
traceabilityRaw?.allowOrphanContracts,
|
|
371
403
|
base.traceability.allowOrphanContracts,
|
|
@@ -491,8 +523,8 @@ function isRecord(value) {
|
|
|
491
523
|
}
|
|
492
524
|
|
|
493
525
|
// src/core/report.ts
|
|
494
|
-
import { readFile as
|
|
495
|
-
import
|
|
526
|
+
import { readFile as readFile11 } from "fs/promises";
|
|
527
|
+
import path14 from "path";
|
|
496
528
|
|
|
497
529
|
// src/core/discovery.ts
|
|
498
530
|
import { access as access3 } from "fs/promises";
|
|
@@ -500,6 +532,7 @@ import { access as access3 } from "fs/promises";
|
|
|
500
532
|
// src/core/fs.ts
|
|
501
533
|
import { access as access2, readdir as readdir2 } from "fs/promises";
|
|
502
534
|
import path5 from "path";
|
|
535
|
+
import fg from "fast-glob";
|
|
503
536
|
var DEFAULT_IGNORE_DIRS = /* @__PURE__ */ new Set([
|
|
504
537
|
"node_modules",
|
|
505
538
|
".git",
|
|
@@ -521,6 +554,18 @@ async function collectFiles(root, options = {}) {
|
|
|
521
554
|
await walk(root, root, ignoreDirs, extensions, entries);
|
|
522
555
|
return entries;
|
|
523
556
|
}
|
|
557
|
+
async function collectFilesByGlobs(root, options) {
|
|
558
|
+
if (options.globs.length === 0) {
|
|
559
|
+
return [];
|
|
560
|
+
}
|
|
561
|
+
return fg(options.globs, {
|
|
562
|
+
cwd: root,
|
|
563
|
+
ignore: options.ignore ?? [],
|
|
564
|
+
onlyFiles: true,
|
|
565
|
+
absolute: true,
|
|
566
|
+
unique: true
|
|
567
|
+
});
|
|
568
|
+
}
|
|
524
569
|
async function walk(base, current, ignoreDirs, extensions, out) {
|
|
525
570
|
const items = await readdir2(current, { withFileTypes: true });
|
|
526
571
|
for (const item of items) {
|
|
@@ -685,20 +730,335 @@ function isValidId(value, prefix) {
|
|
|
685
730
|
return strict.test(value);
|
|
686
731
|
}
|
|
687
732
|
|
|
688
|
-
// src/core/
|
|
689
|
-
var VALIDATION_SCHEMA_VERSION = "0.2";
|
|
690
|
-
|
|
691
|
-
// src/core/version.ts
|
|
733
|
+
// src/core/traceability.ts
|
|
692
734
|
import { readFile as readFile2 } from "fs/promises";
|
|
693
735
|
import path7 from "path";
|
|
736
|
+
|
|
737
|
+
// src/core/gherkin/parse.ts
|
|
738
|
+
import {
|
|
739
|
+
AstBuilder,
|
|
740
|
+
GherkinClassicTokenMatcher,
|
|
741
|
+
Parser
|
|
742
|
+
} from "@cucumber/gherkin";
|
|
743
|
+
import { randomUUID } from "crypto";
|
|
744
|
+
function parseGherkin(source, uri) {
|
|
745
|
+
const errors = [];
|
|
746
|
+
const uuidFn = () => randomUUID();
|
|
747
|
+
const builder = new AstBuilder(uuidFn);
|
|
748
|
+
const matcher = new GherkinClassicTokenMatcher();
|
|
749
|
+
const parser = new Parser(builder, matcher);
|
|
750
|
+
try {
|
|
751
|
+
const gherkinDocument = parser.parse(source);
|
|
752
|
+
gherkinDocument.uri = uri;
|
|
753
|
+
return { gherkinDocument, errors };
|
|
754
|
+
} catch (error2) {
|
|
755
|
+
errors.push(formatError2(error2));
|
|
756
|
+
return { gherkinDocument: null, errors };
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
function formatError2(error2) {
|
|
760
|
+
if (error2 instanceof Error) {
|
|
761
|
+
return error2.message;
|
|
762
|
+
}
|
|
763
|
+
return String(error2);
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
// src/core/scenarioModel.ts
|
|
767
|
+
var SPEC_TAG_RE = /^SPEC-\d{4}$/;
|
|
768
|
+
var SC_TAG_RE = /^SC-\d{4}$/;
|
|
769
|
+
var BR_TAG_RE = /^BR-\d{4}$/;
|
|
770
|
+
var UI_TAG_RE = /^UI-\d{4}$/;
|
|
771
|
+
var API_TAG_RE = /^API-\d{4}$/;
|
|
772
|
+
var DATA_TAG_RE = /^DATA-\d{4}$/;
|
|
773
|
+
function parseScenarioDocument(text, uri) {
|
|
774
|
+
const { gherkinDocument, errors } = parseGherkin(text, uri);
|
|
775
|
+
if (!gherkinDocument) {
|
|
776
|
+
return { document: null, errors };
|
|
777
|
+
}
|
|
778
|
+
const feature = gherkinDocument.feature;
|
|
779
|
+
if (!feature) {
|
|
780
|
+
return {
|
|
781
|
+
document: { uri, featureTags: [], scenarios: [] },
|
|
782
|
+
errors
|
|
783
|
+
};
|
|
784
|
+
}
|
|
785
|
+
const featureTags = collectTagNames(feature.tags);
|
|
786
|
+
const scenarios = collectScenarioNodes(feature, featureTags);
|
|
787
|
+
return {
|
|
788
|
+
document: {
|
|
789
|
+
uri,
|
|
790
|
+
featureName: feature.name,
|
|
791
|
+
featureTags,
|
|
792
|
+
scenarios
|
|
793
|
+
},
|
|
794
|
+
errors
|
|
795
|
+
};
|
|
796
|
+
}
|
|
797
|
+
function buildScenarioAtoms(document) {
|
|
798
|
+
return document.scenarios.map((scenario) => {
|
|
799
|
+
const specIds = scenario.tags.filter((tag) => SPEC_TAG_RE.test(tag));
|
|
800
|
+
const scIds = scenario.tags.filter((tag) => SC_TAG_RE.test(tag));
|
|
801
|
+
const brIds = unique2(scenario.tags.filter((tag) => BR_TAG_RE.test(tag)));
|
|
802
|
+
const contractIds = /* @__PURE__ */ new Set();
|
|
803
|
+
scenario.tags.forEach((tag) => {
|
|
804
|
+
if (UI_TAG_RE.test(tag) || API_TAG_RE.test(tag) || DATA_TAG_RE.test(tag)) {
|
|
805
|
+
contractIds.add(tag);
|
|
806
|
+
}
|
|
807
|
+
});
|
|
808
|
+
for (const step of scenario.steps) {
|
|
809
|
+
for (const text of collectStepTexts(step)) {
|
|
810
|
+
extractIds(text, "UI").forEach((id) => contractIds.add(id));
|
|
811
|
+
extractIds(text, "API").forEach((id) => contractIds.add(id));
|
|
812
|
+
extractIds(text, "DATA").forEach((id) => contractIds.add(id));
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
const atom = {
|
|
816
|
+
uri: document.uri,
|
|
817
|
+
featureName: document.featureName ?? "",
|
|
818
|
+
scenarioName: scenario.name,
|
|
819
|
+
kind: scenario.kind,
|
|
820
|
+
brIds,
|
|
821
|
+
contractIds: Array.from(contractIds).sort()
|
|
822
|
+
};
|
|
823
|
+
if (scenario.line !== void 0) {
|
|
824
|
+
atom.line = scenario.line;
|
|
825
|
+
}
|
|
826
|
+
if (specIds.length === 1) {
|
|
827
|
+
const specId = specIds[0];
|
|
828
|
+
if (specId) {
|
|
829
|
+
atom.specId = specId;
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
if (scIds.length === 1) {
|
|
833
|
+
const scId = scIds[0];
|
|
834
|
+
if (scId) {
|
|
835
|
+
atom.scId = scId;
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
return atom;
|
|
839
|
+
});
|
|
840
|
+
}
|
|
841
|
+
function collectScenarioNodes(feature, featureTags) {
|
|
842
|
+
const scenarios = [];
|
|
843
|
+
for (const child of feature.children) {
|
|
844
|
+
if (child.scenario) {
|
|
845
|
+
scenarios.push(buildScenarioNode(child.scenario, featureTags, []));
|
|
846
|
+
}
|
|
847
|
+
if (child.rule) {
|
|
848
|
+
const ruleTags = collectTagNames(child.rule.tags);
|
|
849
|
+
for (const ruleChild of child.rule.children) {
|
|
850
|
+
if (ruleChild.scenario) {
|
|
851
|
+
scenarios.push(
|
|
852
|
+
buildScenarioNode(ruleChild.scenario, featureTags, ruleTags)
|
|
853
|
+
);
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
return scenarios;
|
|
859
|
+
}
|
|
860
|
+
function buildScenarioNode(scenario, featureTags, ruleTags) {
|
|
861
|
+
const tags = [...featureTags, ...ruleTags, ...collectTagNames(scenario.tags)];
|
|
862
|
+
const kind = scenario.examples.length > 0 ? "ScenarioOutline" : "Scenario";
|
|
863
|
+
return {
|
|
864
|
+
name: scenario.name,
|
|
865
|
+
kind,
|
|
866
|
+
line: scenario.location?.line,
|
|
867
|
+
tags,
|
|
868
|
+
steps: scenario.steps
|
|
869
|
+
};
|
|
870
|
+
}
|
|
871
|
+
function collectTagNames(tags) {
|
|
872
|
+
return tags.map((tag) => tag.name.replace(/^@/, ""));
|
|
873
|
+
}
|
|
874
|
+
function collectStepTexts(step) {
|
|
875
|
+
const texts = [];
|
|
876
|
+
if (step.text) {
|
|
877
|
+
texts.push(step.text);
|
|
878
|
+
}
|
|
879
|
+
if (step.docString?.content) {
|
|
880
|
+
texts.push(step.docString.content);
|
|
881
|
+
}
|
|
882
|
+
if (step.dataTable?.rows) {
|
|
883
|
+
for (const row of step.dataTable.rows) {
|
|
884
|
+
for (const cell of row.cells) {
|
|
885
|
+
texts.push(cell.value);
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
return texts;
|
|
890
|
+
}
|
|
891
|
+
function unique2(values) {
|
|
892
|
+
return Array.from(new Set(values));
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
// src/core/traceability.ts
|
|
896
|
+
var SC_TAG_RE2 = /^SC-\d{4}$/;
|
|
897
|
+
var SC_TEST_ANNOTATION_RE = /\bQFAI:SC-(\d{4})\b/g;
|
|
898
|
+
var DEFAULT_TEST_FILE_EXCLUDE_GLOBS = [
|
|
899
|
+
"**/node_modules/**",
|
|
900
|
+
"**/.git/**",
|
|
901
|
+
"**/.qfai/**",
|
|
902
|
+
"**/dist/**",
|
|
903
|
+
"**/build/**",
|
|
904
|
+
"**/coverage/**",
|
|
905
|
+
"**/.next/**",
|
|
906
|
+
"**/out/**"
|
|
907
|
+
];
|
|
908
|
+
function extractAnnotatedScIds(text) {
|
|
909
|
+
const ids = /* @__PURE__ */ new Set();
|
|
910
|
+
for (const match of text.matchAll(SC_TEST_ANNOTATION_RE)) {
|
|
911
|
+
const suffix = match[1];
|
|
912
|
+
if (suffix) {
|
|
913
|
+
ids.add(`SC-${suffix}`);
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
return Array.from(ids);
|
|
917
|
+
}
|
|
918
|
+
async function collectScIdsFromScenarioFiles(scenarioFiles) {
|
|
919
|
+
const scIds = /* @__PURE__ */ new Set();
|
|
920
|
+
for (const file of scenarioFiles) {
|
|
921
|
+
const text = await readFile2(file, "utf-8");
|
|
922
|
+
const { document, errors } = parseScenarioDocument(text, file);
|
|
923
|
+
if (!document || errors.length > 0) {
|
|
924
|
+
continue;
|
|
925
|
+
}
|
|
926
|
+
for (const scenario of document.scenarios) {
|
|
927
|
+
for (const tag of scenario.tags) {
|
|
928
|
+
if (SC_TAG_RE2.test(tag)) {
|
|
929
|
+
scIds.add(tag);
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
return scIds;
|
|
935
|
+
}
|
|
936
|
+
async function collectScIdSourcesFromScenarioFiles(scenarioFiles) {
|
|
937
|
+
const sources = /* @__PURE__ */ new Map();
|
|
938
|
+
for (const file of scenarioFiles) {
|
|
939
|
+
const text = await readFile2(file, "utf-8");
|
|
940
|
+
const { document, errors } = parseScenarioDocument(text, file);
|
|
941
|
+
if (!document || errors.length > 0) {
|
|
942
|
+
continue;
|
|
943
|
+
}
|
|
944
|
+
for (const scenario of document.scenarios) {
|
|
945
|
+
for (const tag of scenario.tags) {
|
|
946
|
+
if (!SC_TAG_RE2.test(tag)) {
|
|
947
|
+
continue;
|
|
948
|
+
}
|
|
949
|
+
const current = sources.get(tag) ?? /* @__PURE__ */ new Set();
|
|
950
|
+
current.add(file);
|
|
951
|
+
sources.set(tag, current);
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
return sources;
|
|
956
|
+
}
|
|
957
|
+
async function collectScTestReferences(root, globs, excludeGlobs) {
|
|
958
|
+
const refs = /* @__PURE__ */ new Map();
|
|
959
|
+
const normalizedGlobs = normalizeGlobs(globs);
|
|
960
|
+
const normalizedExcludeGlobs = normalizeGlobs(excludeGlobs);
|
|
961
|
+
const mergedExcludeGlobs = Array.from(
|
|
962
|
+
/* @__PURE__ */ new Set([...DEFAULT_TEST_FILE_EXCLUDE_GLOBS, ...normalizedExcludeGlobs])
|
|
963
|
+
);
|
|
964
|
+
if (normalizedGlobs.length === 0) {
|
|
965
|
+
return {
|
|
966
|
+
refs,
|
|
967
|
+
scan: {
|
|
968
|
+
globs: normalizedGlobs,
|
|
969
|
+
excludeGlobs: mergedExcludeGlobs,
|
|
970
|
+
matchedFileCount: 0
|
|
971
|
+
}
|
|
972
|
+
};
|
|
973
|
+
}
|
|
974
|
+
let files = [];
|
|
975
|
+
try {
|
|
976
|
+
files = await collectFilesByGlobs(root, {
|
|
977
|
+
globs: normalizedGlobs,
|
|
978
|
+
ignore: mergedExcludeGlobs
|
|
979
|
+
});
|
|
980
|
+
} catch (error2) {
|
|
981
|
+
return {
|
|
982
|
+
refs,
|
|
983
|
+
scan: {
|
|
984
|
+
globs: normalizedGlobs,
|
|
985
|
+
excludeGlobs: mergedExcludeGlobs,
|
|
986
|
+
matchedFileCount: 0
|
|
987
|
+
},
|
|
988
|
+
error: formatError3(error2)
|
|
989
|
+
};
|
|
990
|
+
}
|
|
991
|
+
const normalizedFiles = Array.from(
|
|
992
|
+
new Set(files.map((file) => path7.normalize(file)))
|
|
993
|
+
);
|
|
994
|
+
for (const file of normalizedFiles) {
|
|
995
|
+
const text = await readFile2(file, "utf-8");
|
|
996
|
+
const scIds = extractAnnotatedScIds(text);
|
|
997
|
+
if (scIds.length === 0) {
|
|
998
|
+
continue;
|
|
999
|
+
}
|
|
1000
|
+
for (const scId of scIds) {
|
|
1001
|
+
const current = refs.get(scId) ?? /* @__PURE__ */ new Set();
|
|
1002
|
+
current.add(file);
|
|
1003
|
+
refs.set(scId, current);
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
return {
|
|
1007
|
+
refs,
|
|
1008
|
+
scan: {
|
|
1009
|
+
globs: normalizedGlobs,
|
|
1010
|
+
excludeGlobs: mergedExcludeGlobs,
|
|
1011
|
+
matchedFileCount: normalizedFiles.length
|
|
1012
|
+
}
|
|
1013
|
+
};
|
|
1014
|
+
}
|
|
1015
|
+
function buildScCoverage(scIds, refs) {
|
|
1016
|
+
const sortedScIds = toSortedArray(scIds);
|
|
1017
|
+
const refsRecord = {};
|
|
1018
|
+
const missingIds = [];
|
|
1019
|
+
let covered = 0;
|
|
1020
|
+
for (const scId of sortedScIds) {
|
|
1021
|
+
const files = refs.get(scId);
|
|
1022
|
+
const sortedFiles = files ? toSortedArray(files) : [];
|
|
1023
|
+
refsRecord[scId] = sortedFiles;
|
|
1024
|
+
if (sortedFiles.length === 0) {
|
|
1025
|
+
missingIds.push(scId);
|
|
1026
|
+
} else {
|
|
1027
|
+
covered += 1;
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
return {
|
|
1031
|
+
total: sortedScIds.length,
|
|
1032
|
+
covered,
|
|
1033
|
+
missing: missingIds.length,
|
|
1034
|
+
missingIds,
|
|
1035
|
+
refs: refsRecord
|
|
1036
|
+
};
|
|
1037
|
+
}
|
|
1038
|
+
function toSortedArray(values) {
|
|
1039
|
+
return Array.from(new Set(values)).sort((a, b) => a.localeCompare(b));
|
|
1040
|
+
}
|
|
1041
|
+
function normalizeGlobs(globs) {
|
|
1042
|
+
return globs.map((glob) => glob.trim()).filter((glob) => glob.length > 0);
|
|
1043
|
+
}
|
|
1044
|
+
function formatError3(error2) {
|
|
1045
|
+
if (error2 instanceof Error) {
|
|
1046
|
+
return error2.message;
|
|
1047
|
+
}
|
|
1048
|
+
return String(error2);
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
// src/core/version.ts
|
|
1052
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
1053
|
+
import path8 from "path";
|
|
694
1054
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
695
1055
|
async function resolveToolVersion() {
|
|
696
|
-
if ("0.
|
|
697
|
-
return "0.
|
|
1056
|
+
if ("0.4.2".length > 0) {
|
|
1057
|
+
return "0.4.2";
|
|
698
1058
|
}
|
|
699
1059
|
try {
|
|
700
1060
|
const packagePath = resolvePackageJsonPath();
|
|
701
|
-
const raw = await
|
|
1061
|
+
const raw = await readFile3(packagePath, "utf-8");
|
|
702
1062
|
const parsed = JSON.parse(raw);
|
|
703
1063
|
const version = typeof parsed.version === "string" ? parsed.version : "";
|
|
704
1064
|
return version.length > 0 ? version : "unknown";
|
|
@@ -709,18 +1069,18 @@ async function resolveToolVersion() {
|
|
|
709
1069
|
function resolvePackageJsonPath() {
|
|
710
1070
|
const base = import.meta.url;
|
|
711
1071
|
const basePath = base.startsWith("file:") ? fileURLToPath2(base) : base;
|
|
712
|
-
return
|
|
1072
|
+
return path8.resolve(path8.dirname(basePath), "../../package.json");
|
|
713
1073
|
}
|
|
714
1074
|
|
|
715
1075
|
// src/core/validators/contracts.ts
|
|
716
|
-
import { readFile as
|
|
717
|
-
import
|
|
1076
|
+
import { readFile as readFile4 } from "fs/promises";
|
|
1077
|
+
import path10 from "path";
|
|
718
1078
|
|
|
719
1079
|
// src/core/contracts.ts
|
|
720
|
-
import
|
|
1080
|
+
import path9 from "path";
|
|
721
1081
|
import { parse as parseYaml2 } from "yaml";
|
|
722
1082
|
function parseStructuredContract(file, text) {
|
|
723
|
-
const ext =
|
|
1083
|
+
const ext = path9.extname(file).toLowerCase();
|
|
724
1084
|
if (ext === ".json") {
|
|
725
1085
|
return JSON.parse(text);
|
|
726
1086
|
}
|
|
@@ -771,9 +1131,9 @@ var SQL_DANGEROUS_PATTERNS = [
|
|
|
771
1131
|
async function validateContracts(root, config) {
|
|
772
1132
|
const issues = [];
|
|
773
1133
|
const contractsRoot = resolvePath(root, config, "contractsDir");
|
|
774
|
-
issues.push(...await validateUiContracts(
|
|
775
|
-
issues.push(...await validateApiContracts(
|
|
776
|
-
issues.push(...await validateDataContracts(
|
|
1134
|
+
issues.push(...await validateUiContracts(path10.join(contractsRoot, "ui")));
|
|
1135
|
+
issues.push(...await validateApiContracts(path10.join(contractsRoot, "api")));
|
|
1136
|
+
issues.push(...await validateDataContracts(path10.join(contractsRoot, "db")));
|
|
777
1137
|
return issues;
|
|
778
1138
|
}
|
|
779
1139
|
async function validateUiContracts(uiRoot) {
|
|
@@ -791,7 +1151,7 @@ async function validateUiContracts(uiRoot) {
|
|
|
791
1151
|
}
|
|
792
1152
|
const issues = [];
|
|
793
1153
|
for (const file of files) {
|
|
794
|
-
const text = await
|
|
1154
|
+
const text = await readFile4(file, "utf-8");
|
|
795
1155
|
const invalidIds = extractInvalidIds(text, [
|
|
796
1156
|
"SPEC",
|
|
797
1157
|
"BR",
|
|
@@ -820,7 +1180,7 @@ async function validateUiContracts(uiRoot) {
|
|
|
820
1180
|
issues.push(
|
|
821
1181
|
issue(
|
|
822
1182
|
"QFAI-CONTRACT-001",
|
|
823
|
-
`UI \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${file} (${
|
|
1183
|
+
`UI \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${file} (${formatError4(error2)})`,
|
|
824
1184
|
"error",
|
|
825
1185
|
file,
|
|
826
1186
|
"contracts.ui.parse"
|
|
@@ -858,7 +1218,7 @@ async function validateApiContracts(apiRoot) {
|
|
|
858
1218
|
}
|
|
859
1219
|
const issues = [];
|
|
860
1220
|
for (const file of files) {
|
|
861
|
-
const text = await
|
|
1221
|
+
const text = await readFile4(file, "utf-8");
|
|
862
1222
|
const invalidIds = extractInvalidIds(text, [
|
|
863
1223
|
"SPEC",
|
|
864
1224
|
"BR",
|
|
@@ -887,7 +1247,7 @@ async function validateApiContracts(apiRoot) {
|
|
|
887
1247
|
issues.push(
|
|
888
1248
|
issue(
|
|
889
1249
|
"QFAI-CONTRACT-001",
|
|
890
|
-
`API \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${file} (${
|
|
1250
|
+
`API \u5951\u7D04\u30D5\u30A1\u30A4\u30EB\u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${file} (${formatError4(error2)})`,
|
|
891
1251
|
"error",
|
|
892
1252
|
file,
|
|
893
1253
|
"contracts.api.parse"
|
|
@@ -936,7 +1296,7 @@ async function validateDataContracts(dataRoot) {
|
|
|
936
1296
|
}
|
|
937
1297
|
const issues = [];
|
|
938
1298
|
for (const file of files) {
|
|
939
|
-
const text = await
|
|
1299
|
+
const text = await readFile4(file, "utf-8");
|
|
940
1300
|
const invalidIds = extractInvalidIds(text, [
|
|
941
1301
|
"SPEC",
|
|
942
1302
|
"BR",
|
|
@@ -982,7 +1342,7 @@ function lintSql(text, file) {
|
|
|
982
1342
|
function hasOpenApi(doc) {
|
|
983
1343
|
return typeof doc.openapi === "string" && doc.openapi.length > 0;
|
|
984
1344
|
}
|
|
985
|
-
function
|
|
1345
|
+
function formatError4(error2) {
|
|
986
1346
|
if (error2 instanceof Error) {
|
|
987
1347
|
return error2.message;
|
|
988
1348
|
}
|
|
@@ -1007,8 +1367,8 @@ function issue(code, message, severity, file, rule, refs) {
|
|
|
1007
1367
|
}
|
|
1008
1368
|
|
|
1009
1369
|
// src/core/validators/delta.ts
|
|
1010
|
-
import { readFile as
|
|
1011
|
-
import
|
|
1370
|
+
import { readFile as readFile5 } from "fs/promises";
|
|
1371
|
+
import path11 from "path";
|
|
1012
1372
|
var SECTION_RE = /^##\s+変更区分/m;
|
|
1013
1373
|
var COMPAT_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Compatibility\b/m;
|
|
1014
1374
|
var CHANGE_LINE_RE = /^\s*-\s*\[[ xX]\]\s*Change\/Improvement\b/m;
|
|
@@ -1022,10 +1382,10 @@ async function validateDeltas(root, config) {
|
|
|
1022
1382
|
}
|
|
1023
1383
|
const issues = [];
|
|
1024
1384
|
for (const pack of packs) {
|
|
1025
|
-
const deltaPath =
|
|
1385
|
+
const deltaPath = path11.join(pack, "delta.md");
|
|
1026
1386
|
let text;
|
|
1027
1387
|
try {
|
|
1028
|
-
text = await
|
|
1388
|
+
text = await readFile5(deltaPath, "utf-8");
|
|
1029
1389
|
} catch (error2) {
|
|
1030
1390
|
if (isMissingFileError2(error2)) {
|
|
1031
1391
|
issues.push(
|
|
@@ -1097,17 +1457,17 @@ function issue2(code, message, severity, file, rule, refs) {
|
|
|
1097
1457
|
}
|
|
1098
1458
|
|
|
1099
1459
|
// src/core/validators/ids.ts
|
|
1100
|
-
import { readFile as
|
|
1101
|
-
import
|
|
1460
|
+
import { readFile as readFile7 } from "fs/promises";
|
|
1461
|
+
import path13 from "path";
|
|
1102
1462
|
|
|
1103
1463
|
// src/core/contractIndex.ts
|
|
1104
|
-
import { readFile as
|
|
1105
|
-
import
|
|
1464
|
+
import { readFile as readFile6 } from "fs/promises";
|
|
1465
|
+
import path12 from "path";
|
|
1106
1466
|
async function buildContractIndex(root, config) {
|
|
1107
1467
|
const contractsRoot = resolvePath(root, config, "contractsDir");
|
|
1108
|
-
const uiRoot =
|
|
1109
|
-
const apiRoot =
|
|
1110
|
-
const dataRoot =
|
|
1468
|
+
const uiRoot = path12.join(contractsRoot, "ui");
|
|
1469
|
+
const apiRoot = path12.join(contractsRoot, "api");
|
|
1470
|
+
const dataRoot = path12.join(contractsRoot, "db");
|
|
1111
1471
|
const [uiFiles, apiFiles, dataFiles] = await Promise.all([
|
|
1112
1472
|
collectUiContractFiles(uiRoot),
|
|
1113
1473
|
collectApiContractFiles(apiRoot),
|
|
@@ -1126,7 +1486,7 @@ async function buildContractIndex(root, config) {
|
|
|
1126
1486
|
}
|
|
1127
1487
|
async function indexUiContracts(files, index) {
|
|
1128
1488
|
for (const file of files) {
|
|
1129
|
-
const text = await
|
|
1489
|
+
const text = await readFile6(file, "utf-8");
|
|
1130
1490
|
try {
|
|
1131
1491
|
const doc = parseStructuredContract(file, text);
|
|
1132
1492
|
extractUiContractIds(doc).forEach((id) => record(index, id, file));
|
|
@@ -1138,7 +1498,7 @@ async function indexUiContracts(files, index) {
|
|
|
1138
1498
|
}
|
|
1139
1499
|
async function indexApiContracts(files, index) {
|
|
1140
1500
|
for (const file of files) {
|
|
1141
|
-
const text = await
|
|
1501
|
+
const text = await readFile6(file, "utf-8");
|
|
1142
1502
|
try {
|
|
1143
1503
|
const doc = parseStructuredContract(file, text);
|
|
1144
1504
|
extractApiContractIds(doc).forEach((id) => record(index, id, file));
|
|
@@ -1150,7 +1510,7 @@ async function indexApiContracts(files, index) {
|
|
|
1150
1510
|
}
|
|
1151
1511
|
async function indexDataContracts(files, index) {
|
|
1152
1512
|
for (const file of files) {
|
|
1153
|
-
const text = await
|
|
1513
|
+
const text = await readFile6(file, "utf-8");
|
|
1154
1514
|
extractIds(text, "DATA").forEach((id) => record(index, id, file));
|
|
1155
1515
|
}
|
|
1156
1516
|
}
|
|
@@ -1197,248 +1557,90 @@ function extractH2Sections(md) {
|
|
|
1197
1557
|
startLine,
|
|
1198
1558
|
endLine,
|
|
1199
1559
|
body
|
|
1200
|
-
});
|
|
1201
|
-
}
|
|
1202
|
-
return sections;
|
|
1203
|
-
}
|
|
1204
|
-
|
|
1205
|
-
// src/core/parse/spec.ts
|
|
1206
|
-
var SPEC_ID_RE = /\bSPEC-\d{4}\b/;
|
|
1207
|
-
var BR_LINE_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[0-3])\]\s*(.+)$/;
|
|
1208
|
-
var BR_LINE_ANY_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[^\]]+)\]\s*(.+)$/;
|
|
1209
|
-
var BR_LINE_NO_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\](?!\s*\[P)\s*(.*\S.*)$/;
|
|
1210
|
-
var BR_SECTION_TITLE = "\u696D\u52D9\u30EB\u30FC\u30EB";
|
|
1211
|
-
var VALID_PRIORITIES = /* @__PURE__ */ new Set(["P0", "P1", "P2", "P3"]);
|
|
1212
|
-
function parseSpec(md, file) {
|
|
1213
|
-
const headings = parseHeadings(md);
|
|
1214
|
-
const h1 = headings.find((heading) => heading.level === 1);
|
|
1215
|
-
const specId = h1?.title.match(SPEC_ID_RE)?.[0];
|
|
1216
|
-
const sections = extractH2Sections(md);
|
|
1217
|
-
const sectionNames = new Set(Array.from(sections.keys()));
|
|
1218
|
-
const brSection = sections.get(BR_SECTION_TITLE);
|
|
1219
|
-
const brLines = brSection ? brSection.body.split(/\r?\n/) : [];
|
|
1220
|
-
const startLine = brSection?.startLine ?? 1;
|
|
1221
|
-
const brs = [];
|
|
1222
|
-
const brsWithoutPriority = [];
|
|
1223
|
-
const brsWithInvalidPriority = [];
|
|
1224
|
-
for (let i = 0; i < brLines.length; i++) {
|
|
1225
|
-
const lineText = brLines[i] ?? "";
|
|
1226
|
-
const lineNumber = startLine + i;
|
|
1227
|
-
const validMatch = lineText.match(BR_LINE_RE);
|
|
1228
|
-
if (validMatch) {
|
|
1229
|
-
const id = validMatch[1];
|
|
1230
|
-
const priority = validMatch[2];
|
|
1231
|
-
const text = validMatch[3];
|
|
1232
|
-
if (!id || !priority || !text) continue;
|
|
1233
|
-
brs.push({
|
|
1234
|
-
id,
|
|
1235
|
-
priority,
|
|
1236
|
-
text: text.trim(),
|
|
1237
|
-
line: lineNumber
|
|
1238
|
-
});
|
|
1239
|
-
continue;
|
|
1240
|
-
}
|
|
1241
|
-
const anyPriorityMatch = lineText.match(BR_LINE_ANY_PRIORITY_RE);
|
|
1242
|
-
if (anyPriorityMatch) {
|
|
1243
|
-
const id = anyPriorityMatch[1];
|
|
1244
|
-
const priority = anyPriorityMatch[2];
|
|
1245
|
-
const text = anyPriorityMatch[3];
|
|
1246
|
-
if (!id || !priority || !text) continue;
|
|
1247
|
-
if (!VALID_PRIORITIES.has(priority)) {
|
|
1248
|
-
brsWithInvalidPriority.push({
|
|
1249
|
-
id,
|
|
1250
|
-
priority,
|
|
1251
|
-
text: text.trim(),
|
|
1252
|
-
line: lineNumber
|
|
1253
|
-
});
|
|
1254
|
-
}
|
|
1255
|
-
continue;
|
|
1256
|
-
}
|
|
1257
|
-
const noPriorityMatch = lineText.match(BR_LINE_NO_PRIORITY_RE);
|
|
1258
|
-
if (noPriorityMatch) {
|
|
1259
|
-
const id = noPriorityMatch[1];
|
|
1260
|
-
const text = noPriorityMatch[2];
|
|
1261
|
-
if (!id || !text) continue;
|
|
1262
|
-
brsWithoutPriority.push({
|
|
1263
|
-
id,
|
|
1264
|
-
text: text.trim(),
|
|
1265
|
-
line: lineNumber
|
|
1266
|
-
});
|
|
1267
|
-
}
|
|
1268
|
-
}
|
|
1269
|
-
const parsed = {
|
|
1270
|
-
file,
|
|
1271
|
-
sections: sectionNames,
|
|
1272
|
-
brs,
|
|
1273
|
-
brsWithoutPriority,
|
|
1274
|
-
brsWithInvalidPriority
|
|
1275
|
-
};
|
|
1276
|
-
if (specId) {
|
|
1277
|
-
parsed.specId = specId;
|
|
1278
|
-
}
|
|
1279
|
-
return parsed;
|
|
1280
|
-
}
|
|
1281
|
-
|
|
1282
|
-
// src/core/gherkin/parse.ts
|
|
1283
|
-
import {
|
|
1284
|
-
AstBuilder,
|
|
1285
|
-
GherkinClassicTokenMatcher,
|
|
1286
|
-
Parser
|
|
1287
|
-
} from "@cucumber/gherkin";
|
|
1288
|
-
import { randomUUID } from "crypto";
|
|
1289
|
-
function parseGherkin(source, uri) {
|
|
1290
|
-
const errors = [];
|
|
1291
|
-
const uuidFn = () => randomUUID();
|
|
1292
|
-
const builder = new AstBuilder(uuidFn);
|
|
1293
|
-
const matcher = new GherkinClassicTokenMatcher();
|
|
1294
|
-
const parser = new Parser(builder, matcher);
|
|
1295
|
-
try {
|
|
1296
|
-
const gherkinDocument = parser.parse(source);
|
|
1297
|
-
gherkinDocument.uri = uri;
|
|
1298
|
-
return { gherkinDocument, errors };
|
|
1299
|
-
} catch (error2) {
|
|
1300
|
-
errors.push(formatError3(error2));
|
|
1301
|
-
return { gherkinDocument: null, errors };
|
|
1302
|
-
}
|
|
1303
|
-
}
|
|
1304
|
-
function formatError3(error2) {
|
|
1305
|
-
if (error2 instanceof Error) {
|
|
1306
|
-
return error2.message;
|
|
1307
|
-
}
|
|
1308
|
-
return String(error2);
|
|
1309
|
-
}
|
|
1310
|
-
|
|
1311
|
-
// src/core/scenarioModel.ts
|
|
1312
|
-
var SPEC_TAG_RE = /^SPEC-\d{4}$/;
|
|
1313
|
-
var SC_TAG_RE = /^SC-\d{4}$/;
|
|
1314
|
-
var BR_TAG_RE = /^BR-\d{4}$/;
|
|
1315
|
-
var UI_TAG_RE = /^UI-\d{4}$/;
|
|
1316
|
-
var API_TAG_RE = /^API-\d{4}$/;
|
|
1317
|
-
var DATA_TAG_RE = /^DATA-\d{4}$/;
|
|
1318
|
-
function parseScenarioDocument(text, uri) {
|
|
1319
|
-
const { gherkinDocument, errors } = parseGherkin(text, uri);
|
|
1320
|
-
if (!gherkinDocument) {
|
|
1321
|
-
return { document: null, errors };
|
|
1322
|
-
}
|
|
1323
|
-
const feature = gherkinDocument.feature;
|
|
1324
|
-
if (!feature) {
|
|
1325
|
-
return {
|
|
1326
|
-
document: { uri, featureTags: [], scenarios: [] },
|
|
1327
|
-
errors
|
|
1328
|
-
};
|
|
1329
|
-
}
|
|
1330
|
-
const featureTags = collectTagNames(feature.tags);
|
|
1331
|
-
const scenarios = collectScenarioNodes(feature, featureTags);
|
|
1332
|
-
return {
|
|
1333
|
-
document: {
|
|
1334
|
-
uri,
|
|
1335
|
-
featureName: feature.name,
|
|
1336
|
-
featureTags,
|
|
1337
|
-
scenarios
|
|
1338
|
-
},
|
|
1339
|
-
errors
|
|
1340
|
-
};
|
|
1341
|
-
}
|
|
1342
|
-
function buildScenarioAtoms(document) {
|
|
1343
|
-
return document.scenarios.map((scenario) => {
|
|
1344
|
-
const specIds = scenario.tags.filter((tag) => SPEC_TAG_RE.test(tag));
|
|
1345
|
-
const scIds = scenario.tags.filter((tag) => SC_TAG_RE.test(tag));
|
|
1346
|
-
const brIds = unique2(scenario.tags.filter((tag) => BR_TAG_RE.test(tag)));
|
|
1347
|
-
const contractIds = /* @__PURE__ */ new Set();
|
|
1348
|
-
scenario.tags.forEach((tag) => {
|
|
1349
|
-
if (UI_TAG_RE.test(tag) || API_TAG_RE.test(tag) || DATA_TAG_RE.test(tag)) {
|
|
1350
|
-
contractIds.add(tag);
|
|
1351
|
-
}
|
|
1352
|
-
});
|
|
1353
|
-
for (const step of scenario.steps) {
|
|
1354
|
-
for (const text of collectStepTexts(step)) {
|
|
1355
|
-
extractIds(text, "UI").forEach((id) => contractIds.add(id));
|
|
1356
|
-
extractIds(text, "API").forEach((id) => contractIds.add(id));
|
|
1357
|
-
extractIds(text, "DATA").forEach((id) => contractIds.add(id));
|
|
1358
|
-
}
|
|
1359
|
-
}
|
|
1360
|
-
const atom = {
|
|
1361
|
-
uri: document.uri,
|
|
1362
|
-
featureName: document.featureName ?? "",
|
|
1363
|
-
scenarioName: scenario.name,
|
|
1364
|
-
kind: scenario.kind,
|
|
1365
|
-
brIds,
|
|
1366
|
-
contractIds: Array.from(contractIds).sort()
|
|
1367
|
-
};
|
|
1368
|
-
if (scenario.line !== void 0) {
|
|
1369
|
-
atom.line = scenario.line;
|
|
1370
|
-
}
|
|
1371
|
-
if (specIds.length === 1) {
|
|
1372
|
-
const specId = specIds[0];
|
|
1373
|
-
if (specId) {
|
|
1374
|
-
atom.specId = specId;
|
|
1375
|
-
}
|
|
1376
|
-
}
|
|
1377
|
-
if (scIds.length === 1) {
|
|
1378
|
-
const scId = scIds[0];
|
|
1379
|
-
if (scId) {
|
|
1380
|
-
atom.scId = scId;
|
|
1381
|
-
}
|
|
1382
|
-
}
|
|
1383
|
-
return atom;
|
|
1384
|
-
});
|
|
1560
|
+
});
|
|
1561
|
+
}
|
|
1562
|
+
return sections;
|
|
1385
1563
|
}
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1564
|
+
|
|
1565
|
+
// src/core/parse/spec.ts
|
|
1566
|
+
var SPEC_ID_RE = /\bSPEC-\d{4}\b/;
|
|
1567
|
+
var BR_LINE_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[0-3])\]\s*(.+)$/;
|
|
1568
|
+
var BR_LINE_ANY_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[^\]]+)\]\s*(.+)$/;
|
|
1569
|
+
var BR_LINE_NO_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\](?!\s*\[P)\s*(.*\S.*)$/;
|
|
1570
|
+
var BR_SECTION_TITLE = "\u696D\u52D9\u30EB\u30FC\u30EB";
|
|
1571
|
+
var VALID_PRIORITIES = /* @__PURE__ */ new Set(["P0", "P1", "P2", "P3"]);
|
|
1572
|
+
function parseSpec(md, file) {
|
|
1573
|
+
const headings = parseHeadings(md);
|
|
1574
|
+
const h1 = headings.find((heading) => heading.level === 1);
|
|
1575
|
+
const specId = h1?.title.match(SPEC_ID_RE)?.[0];
|
|
1576
|
+
const sections = extractH2Sections(md);
|
|
1577
|
+
const sectionNames = new Set(Array.from(sections.keys()));
|
|
1578
|
+
const brSection = sections.get(BR_SECTION_TITLE);
|
|
1579
|
+
const brLines = brSection ? brSection.body.split(/\r?\n/) : [];
|
|
1580
|
+
const startLine = brSection?.startLine ?? 1;
|
|
1581
|
+
const brs = [];
|
|
1582
|
+
const brsWithoutPriority = [];
|
|
1583
|
+
const brsWithInvalidPriority = [];
|
|
1584
|
+
for (let i = 0; i < brLines.length; i++) {
|
|
1585
|
+
const lineText = brLines[i] ?? "";
|
|
1586
|
+
const lineNumber = startLine + i;
|
|
1587
|
+
const validMatch = lineText.match(BR_LINE_RE);
|
|
1588
|
+
if (validMatch) {
|
|
1589
|
+
const id = validMatch[1];
|
|
1590
|
+
const priority = validMatch[2];
|
|
1591
|
+
const text = validMatch[3];
|
|
1592
|
+
if (!id || !priority || !text) continue;
|
|
1593
|
+
brs.push({
|
|
1594
|
+
id,
|
|
1595
|
+
priority,
|
|
1596
|
+
text: text.trim(),
|
|
1597
|
+
line: lineNumber
|
|
1598
|
+
});
|
|
1599
|
+
continue;
|
|
1391
1600
|
}
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1601
|
+
const anyPriorityMatch = lineText.match(BR_LINE_ANY_PRIORITY_RE);
|
|
1602
|
+
if (anyPriorityMatch) {
|
|
1603
|
+
const id = anyPriorityMatch[1];
|
|
1604
|
+
const priority = anyPriorityMatch[2];
|
|
1605
|
+
const text = anyPriorityMatch[3];
|
|
1606
|
+
if (!id || !priority || !text) continue;
|
|
1607
|
+
if (!VALID_PRIORITIES.has(priority)) {
|
|
1608
|
+
brsWithInvalidPriority.push({
|
|
1609
|
+
id,
|
|
1610
|
+
priority,
|
|
1611
|
+
text: text.trim(),
|
|
1612
|
+
line: lineNumber
|
|
1613
|
+
});
|
|
1400
1614
|
}
|
|
1615
|
+
continue;
|
|
1616
|
+
}
|
|
1617
|
+
const noPriorityMatch = lineText.match(BR_LINE_NO_PRIORITY_RE);
|
|
1618
|
+
if (noPriorityMatch) {
|
|
1619
|
+
const id = noPriorityMatch[1];
|
|
1620
|
+
const text = noPriorityMatch[2];
|
|
1621
|
+
if (!id || !text) continue;
|
|
1622
|
+
brsWithoutPriority.push({
|
|
1623
|
+
id,
|
|
1624
|
+
text: text.trim(),
|
|
1625
|
+
line: lineNumber
|
|
1626
|
+
});
|
|
1401
1627
|
}
|
|
1402
1628
|
}
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
name: scenario.name,
|
|
1410
|
-
kind,
|
|
1411
|
-
line: scenario.location?.line,
|
|
1412
|
-
tags,
|
|
1413
|
-
steps: scenario.steps
|
|
1629
|
+
const parsed = {
|
|
1630
|
+
file,
|
|
1631
|
+
sections: sectionNames,
|
|
1632
|
+
brs,
|
|
1633
|
+
brsWithoutPriority,
|
|
1634
|
+
brsWithInvalidPriority
|
|
1414
1635
|
};
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
return tags.map((tag) => tag.name.replace(/^@/, ""));
|
|
1418
|
-
}
|
|
1419
|
-
function collectStepTexts(step) {
|
|
1420
|
-
const texts = [];
|
|
1421
|
-
if (step.text) {
|
|
1422
|
-
texts.push(step.text);
|
|
1423
|
-
}
|
|
1424
|
-
if (step.docString?.content) {
|
|
1425
|
-
texts.push(step.docString.content);
|
|
1426
|
-
}
|
|
1427
|
-
if (step.dataTable?.rows) {
|
|
1428
|
-
for (const row of step.dataTable.rows) {
|
|
1429
|
-
for (const cell of row.cells) {
|
|
1430
|
-
texts.push(cell.value);
|
|
1431
|
-
}
|
|
1432
|
-
}
|
|
1636
|
+
if (specId) {
|
|
1637
|
+
parsed.specId = specId;
|
|
1433
1638
|
}
|
|
1434
|
-
return
|
|
1435
|
-
}
|
|
1436
|
-
function unique2(values) {
|
|
1437
|
-
return Array.from(new Set(values));
|
|
1639
|
+
return parsed;
|
|
1438
1640
|
}
|
|
1439
1641
|
|
|
1440
1642
|
// src/core/validators/ids.ts
|
|
1441
|
-
var
|
|
1643
|
+
var SC_TAG_RE3 = /^SC-\d{4}$/;
|
|
1442
1644
|
async function validateDefinedIds(root, config) {
|
|
1443
1645
|
const issues = [];
|
|
1444
1646
|
const specsRoot = resolvePath(root, config, "specsDir");
|
|
@@ -1472,7 +1674,7 @@ async function validateDefinedIds(root, config) {
|
|
|
1472
1674
|
}
|
|
1473
1675
|
async function collectSpecDefinitionIds(files, out) {
|
|
1474
1676
|
for (const file of files) {
|
|
1475
|
-
const text = await
|
|
1677
|
+
const text = await readFile7(file, "utf-8");
|
|
1476
1678
|
const parsed = parseSpec(text, file);
|
|
1477
1679
|
if (parsed.specId) {
|
|
1478
1680
|
recordId(out, parsed.specId, file);
|
|
@@ -1482,14 +1684,14 @@ async function collectSpecDefinitionIds(files, out) {
|
|
|
1482
1684
|
}
|
|
1483
1685
|
async function collectScenarioDefinitionIds(files, out) {
|
|
1484
1686
|
for (const file of files) {
|
|
1485
|
-
const text = await
|
|
1687
|
+
const text = await readFile7(file, "utf-8");
|
|
1486
1688
|
const { document, errors } = parseScenarioDocument(text, file);
|
|
1487
1689
|
if (!document || errors.length > 0) {
|
|
1488
1690
|
continue;
|
|
1489
1691
|
}
|
|
1490
1692
|
for (const scenario of document.scenarios) {
|
|
1491
1693
|
for (const tag of scenario.tags) {
|
|
1492
|
-
if (
|
|
1694
|
+
if (SC_TAG_RE3.test(tag)) {
|
|
1493
1695
|
recordId(out, tag, file);
|
|
1494
1696
|
}
|
|
1495
1697
|
}
|
|
@@ -1503,7 +1705,7 @@ function recordId(out, id, file) {
|
|
|
1503
1705
|
}
|
|
1504
1706
|
function formatFileList(files, root) {
|
|
1505
1707
|
return files.map((file) => {
|
|
1506
|
-
const relative =
|
|
1708
|
+
const relative = path13.relative(root, file);
|
|
1507
1709
|
return relative.length > 0 ? relative : file;
|
|
1508
1710
|
}).join(", ");
|
|
1509
1711
|
}
|
|
@@ -1526,13 +1728,12 @@ function issue3(code, message, severity, file, rule, refs) {
|
|
|
1526
1728
|
}
|
|
1527
1729
|
|
|
1528
1730
|
// src/core/validators/scenario.ts
|
|
1529
|
-
import { readFile as
|
|
1731
|
+
import { readFile as readFile8 } from "fs/promises";
|
|
1530
1732
|
var GIVEN_PATTERN = /\bGiven\b/;
|
|
1531
1733
|
var WHEN_PATTERN = /\bWhen\b/;
|
|
1532
1734
|
var THEN_PATTERN = /\bThen\b/;
|
|
1533
|
-
var
|
|
1735
|
+
var SC_TAG_RE4 = /^SC-\d{4}$/;
|
|
1534
1736
|
var SPEC_TAG_RE2 = /^SPEC-\d{4}$/;
|
|
1535
|
-
var BR_TAG_RE2 = /^BR-\d{4}$/;
|
|
1536
1737
|
async function validateScenarios(root, config) {
|
|
1537
1738
|
const specsRoot = resolvePath(root, config, "specsDir");
|
|
1538
1739
|
const entries = await collectSpecEntries(specsRoot);
|
|
@@ -1553,7 +1754,7 @@ async function validateScenarios(root, config) {
|
|
|
1553
1754
|
for (const entry of entries) {
|
|
1554
1755
|
let text;
|
|
1555
1756
|
try {
|
|
1556
|
-
text = await
|
|
1757
|
+
text = await readFile8(entry.scenarioPath, "utf-8");
|
|
1557
1758
|
} catch (error2) {
|
|
1558
1759
|
if (isMissingFileError3(error2)) {
|
|
1559
1760
|
issues.push(
|
|
@@ -1612,17 +1813,7 @@ function validateScenarioContent(text, file) {
|
|
|
1612
1813
|
const featureSpecTags = document.featureTags.filter(
|
|
1613
1814
|
(tag) => SPEC_TAG_RE2.test(tag)
|
|
1614
1815
|
);
|
|
1615
|
-
if (featureSpecTags.length
|
|
1616
|
-
issues.push(
|
|
1617
|
-
issue4(
|
|
1618
|
-
"QFAI-SC-009",
|
|
1619
|
-
"Feature \u30BF\u30B0\u306B SPEC \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002",
|
|
1620
|
-
"error",
|
|
1621
|
-
file,
|
|
1622
|
-
"scenario.featureSpec"
|
|
1623
|
-
)
|
|
1624
|
-
);
|
|
1625
|
-
} else if (featureSpecTags.length > 1) {
|
|
1816
|
+
if (featureSpecTags.length > 1) {
|
|
1626
1817
|
issues.push(
|
|
1627
1818
|
issue4(
|
|
1628
1819
|
"QFAI-SC-009",
|
|
@@ -1664,18 +1855,12 @@ function validateScenarioContent(text, file) {
|
|
|
1664
1855
|
continue;
|
|
1665
1856
|
}
|
|
1666
1857
|
const missingTags = [];
|
|
1667
|
-
const scTags = scenario.tags.filter((tag) =>
|
|
1858
|
+
const scTags = scenario.tags.filter((tag) => SC_TAG_RE4.test(tag));
|
|
1668
1859
|
if (scTags.length === 0) {
|
|
1669
1860
|
missingTags.push("SC(0\u4EF6)");
|
|
1670
1861
|
} else if (scTags.length > 1) {
|
|
1671
1862
|
missingTags.push(`SC(${scTags.length}\u4EF6/1\u4EF6\u5FC5\u9808)`);
|
|
1672
1863
|
}
|
|
1673
|
-
if (!scenario.tags.some((tag) => SPEC_TAG_RE2.test(tag))) {
|
|
1674
|
-
missingTags.push("SPEC");
|
|
1675
|
-
}
|
|
1676
|
-
if (!scenario.tags.some((tag) => BR_TAG_RE2.test(tag))) {
|
|
1677
|
-
missingTags.push("BR");
|
|
1678
|
-
}
|
|
1679
1864
|
if (missingTags.length > 0) {
|
|
1680
1865
|
issues.push(
|
|
1681
1866
|
issue4(
|
|
@@ -1739,7 +1924,7 @@ function isMissingFileError3(error2) {
|
|
|
1739
1924
|
}
|
|
1740
1925
|
|
|
1741
1926
|
// src/core/validators/spec.ts
|
|
1742
|
-
import { readFile as
|
|
1927
|
+
import { readFile as readFile9 } from "fs/promises";
|
|
1743
1928
|
async function validateSpecs(root, config) {
|
|
1744
1929
|
const specsRoot = resolvePath(root, config, "specsDir");
|
|
1745
1930
|
const entries = await collectSpecEntries(specsRoot);
|
|
@@ -1760,7 +1945,7 @@ async function validateSpecs(root, config) {
|
|
|
1760
1945
|
for (const entry of entries) {
|
|
1761
1946
|
let text;
|
|
1762
1947
|
try {
|
|
1763
|
-
text = await
|
|
1948
|
+
text = await readFile9(entry.specPath, "utf-8");
|
|
1764
1949
|
} catch (error2) {
|
|
1765
1950
|
if (isMissingFileError4(error2)) {
|
|
1766
1951
|
issues.push(
|
|
@@ -1909,10 +2094,9 @@ function isMissingFileError4(error2) {
|
|
|
1909
2094
|
}
|
|
1910
2095
|
|
|
1911
2096
|
// src/core/validators/traceability.ts
|
|
1912
|
-
import { readFile as
|
|
1913
|
-
var SC_TAG_RE4 = /^SC-\d{4}$/;
|
|
2097
|
+
import { readFile as readFile10 } from "fs/promises";
|
|
1914
2098
|
var SPEC_TAG_RE3 = /^SPEC-\d{4}$/;
|
|
1915
|
-
var
|
|
2099
|
+
var BR_TAG_RE2 = /^BR-\d{4}$/;
|
|
1916
2100
|
async function validateTraceability(root, config) {
|
|
1917
2101
|
const issues = [];
|
|
1918
2102
|
const specsRoot = resolvePath(root, config, "specsDir");
|
|
@@ -1931,7 +2115,7 @@ async function validateTraceability(root, config) {
|
|
|
1931
2115
|
const contractIndex = await buildContractIndex(root, config);
|
|
1932
2116
|
const contractIds = contractIndex.ids;
|
|
1933
2117
|
for (const file of specFiles) {
|
|
1934
|
-
const text = await
|
|
2118
|
+
const text = await readFile10(file, "utf-8");
|
|
1935
2119
|
extractAllIds(text).forEach((id) => upstreamIds.add(id));
|
|
1936
2120
|
const parsed = parseSpec(text, file);
|
|
1937
2121
|
if (parsed.specId) {
|
|
@@ -1939,28 +2123,6 @@ async function validateTraceability(root, config) {
|
|
|
1939
2123
|
}
|
|
1940
2124
|
const brIds = parsed.brs.map((br) => br.id);
|
|
1941
2125
|
brIds.forEach((id) => brIdsInSpecs.add(id));
|
|
1942
|
-
const referencedContractIds = /* @__PURE__ */ new Set([
|
|
1943
|
-
...extractIds(text, "UI"),
|
|
1944
|
-
...extractIds(text, "API"),
|
|
1945
|
-
...extractIds(text, "DATA")
|
|
1946
|
-
]);
|
|
1947
|
-
const unknownContractIds = Array.from(referencedContractIds).filter(
|
|
1948
|
-
(id) => !contractIds.has(id)
|
|
1949
|
-
);
|
|
1950
|
-
if (unknownContractIds.length > 0) {
|
|
1951
|
-
issues.push(
|
|
1952
|
-
issue6(
|
|
1953
|
-
"QFAI-TRACE-009",
|
|
1954
|
-
`Spec \u304C\u5B58\u5728\u3057\u306A\u3044\u5951\u7D04 ID \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownContractIds.join(
|
|
1955
|
-
", "
|
|
1956
|
-
)}`,
|
|
1957
|
-
"error",
|
|
1958
|
-
file,
|
|
1959
|
-
"traceability.specContractExists",
|
|
1960
|
-
unknownContractIds
|
|
1961
|
-
)
|
|
1962
|
-
);
|
|
1963
|
-
}
|
|
1964
2126
|
if (parsed.specId) {
|
|
1965
2127
|
const current = specToBrIds.get(parsed.specId) ?? /* @__PURE__ */ new Set();
|
|
1966
2128
|
brIds.forEach((id) => current.add(id));
|
|
@@ -1968,23 +2130,49 @@ async function validateTraceability(root, config) {
|
|
|
1968
2130
|
}
|
|
1969
2131
|
}
|
|
1970
2132
|
for (const file of scenarioFiles) {
|
|
1971
|
-
const text = await
|
|
2133
|
+
const text = await readFile10(file, "utf-8");
|
|
1972
2134
|
extractAllIds(text).forEach((id) => upstreamIds.add(id));
|
|
1973
2135
|
const { document, errors } = parseScenarioDocument(text, file);
|
|
1974
2136
|
if (!document || errors.length > 0) {
|
|
1975
2137
|
continue;
|
|
1976
2138
|
}
|
|
1977
2139
|
const atoms = buildScenarioAtoms(document);
|
|
2140
|
+
const scIdsInFile = /* @__PURE__ */ new Set();
|
|
1978
2141
|
for (const [index, scenario] of document.scenarios.entries()) {
|
|
1979
2142
|
const atom = atoms[index];
|
|
1980
2143
|
if (!atom) {
|
|
1981
2144
|
continue;
|
|
1982
2145
|
}
|
|
1983
2146
|
const specTags = scenario.tags.filter((tag) => SPEC_TAG_RE3.test(tag));
|
|
1984
|
-
const brTags = scenario.tags.filter((tag) =>
|
|
1985
|
-
const scTags = scenario.tags.filter((tag) =>
|
|
2147
|
+
const brTags = scenario.tags.filter((tag) => BR_TAG_RE2.test(tag));
|
|
2148
|
+
const scTags = scenario.tags.filter((tag) => SC_TAG_RE2.test(tag));
|
|
2149
|
+
if (specTags.length === 0) {
|
|
2150
|
+
issues.push(
|
|
2151
|
+
issue6(
|
|
2152
|
+
"QFAI-TRACE-014",
|
|
2153
|
+
`Scenario \u304C SPEC \u30BF\u30B0\u3092\u6301\u3063\u3066\u3044\u307E\u305B\u3093: ${scenario.name}`,
|
|
2154
|
+
"error",
|
|
2155
|
+
file,
|
|
2156
|
+
"traceability.scenarioSpecRequired"
|
|
2157
|
+
)
|
|
2158
|
+
);
|
|
2159
|
+
}
|
|
2160
|
+
if (brTags.length === 0) {
|
|
2161
|
+
issues.push(
|
|
2162
|
+
issue6(
|
|
2163
|
+
"QFAI-TRACE-015",
|
|
2164
|
+
`Scenario \u304C BR \u30BF\u30B0\u3092\u6301\u3063\u3066\u3044\u307E\u305B\u3093: ${scenario.name}`,
|
|
2165
|
+
"error",
|
|
2166
|
+
file,
|
|
2167
|
+
"traceability.scenarioBrRequired"
|
|
2168
|
+
)
|
|
2169
|
+
);
|
|
2170
|
+
}
|
|
1986
2171
|
brTags.forEach((id) => brIdsInScenarios.add(id));
|
|
1987
|
-
scTags.forEach((id) =>
|
|
2172
|
+
scTags.forEach((id) => {
|
|
2173
|
+
scIdsInScenarios.add(id);
|
|
2174
|
+
scIdsInFile.add(id);
|
|
2175
|
+
});
|
|
1988
2176
|
atom.contractIds.forEach((id) => scenarioContractIds.add(id));
|
|
1989
2177
|
if (atom.contractIds.length > 0) {
|
|
1990
2178
|
scTags.forEach((id) => scWithContracts.add(id));
|
|
@@ -2062,6 +2250,22 @@ async function validateTraceability(root, config) {
|
|
|
2062
2250
|
}
|
|
2063
2251
|
}
|
|
2064
2252
|
}
|
|
2253
|
+
if (scIdsInFile.size !== 1) {
|
|
2254
|
+
const invalidScIds = Array.from(scIdsInFile).sort(
|
|
2255
|
+
(a, b) => a.localeCompare(b)
|
|
2256
|
+
);
|
|
2257
|
+
const detail = invalidScIds.length === 0 ? "SC \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093" : `\u8907\u6570\u306E SC \u304C\u5B58\u5728\u3057\u307E\u3059: ${invalidScIds.join(", ")}`;
|
|
2258
|
+
issues.push(
|
|
2259
|
+
issue6(
|
|
2260
|
+
"QFAI-TRACE-012",
|
|
2261
|
+
`Spec entry \u304C Spec:SC=1:1 \u3092\u6E80\u305F\u3057\u3066\u3044\u307E\u305B\u3093: ${detail}`,
|
|
2262
|
+
"error",
|
|
2263
|
+
file,
|
|
2264
|
+
"traceability.specScOneToOne",
|
|
2265
|
+
invalidScIds
|
|
2266
|
+
)
|
|
2267
|
+
);
|
|
2268
|
+
}
|
|
2065
2269
|
}
|
|
2066
2270
|
if (upstreamIds.size === 0) {
|
|
2067
2271
|
return [
|
|
@@ -2110,6 +2314,66 @@ async function validateTraceability(root, config) {
|
|
|
2110
2314
|
);
|
|
2111
2315
|
}
|
|
2112
2316
|
}
|
|
2317
|
+
const scRefsResult = await collectScTestReferences(
|
|
2318
|
+
root,
|
|
2319
|
+
config.validation.traceability.testFileGlobs,
|
|
2320
|
+
config.validation.traceability.testFileExcludeGlobs
|
|
2321
|
+
);
|
|
2322
|
+
const scTestRefs = scRefsResult.refs;
|
|
2323
|
+
const testFileScan = scRefsResult.scan;
|
|
2324
|
+
const hasScenarios = scIdsInScenarios.size > 0;
|
|
2325
|
+
const hasGlobConfig = testFileScan.globs.length > 0;
|
|
2326
|
+
const hasMatchedTests = testFileScan.matchedFileCount > 0;
|
|
2327
|
+
if (hasScenarios && (!hasGlobConfig || !hasMatchedTests || scRefsResult.error)) {
|
|
2328
|
+
const detail = scRefsResult.error ? `\uFF08\u8A73\u7D30: ${scRefsResult.error}\uFF09` : "";
|
|
2329
|
+
issues.push(
|
|
2330
|
+
issue6(
|
|
2331
|
+
"QFAI-TRACE-013",
|
|
2332
|
+
`\u30C6\u30B9\u30C8\u63A2\u7D22 glob \u304C\u672A\u8A2D\u5B9A/\u4E0D\u6B63/\u4E00\u81F4\u30D5\u30A1\u30A4\u30EB0\u306E\u305F\u3081 SC\u2192Test \u3092\u5224\u5B9A\u3067\u304D\u307E\u305B\u3093\u3002${detail}`,
|
|
2333
|
+
"error",
|
|
2334
|
+
testsRoot,
|
|
2335
|
+
"traceability.testFileGlobs"
|
|
2336
|
+
)
|
|
2337
|
+
);
|
|
2338
|
+
} else {
|
|
2339
|
+
if (config.validation.traceability.scMustHaveTest && scIdsInScenarios.size) {
|
|
2340
|
+
const scWithoutTests = Array.from(scIdsInScenarios).filter((id) => {
|
|
2341
|
+
const refs = scTestRefs.get(id);
|
|
2342
|
+
return !refs || refs.size === 0;
|
|
2343
|
+
});
|
|
2344
|
+
if (scWithoutTests.length > 0) {
|
|
2345
|
+
issues.push(
|
|
2346
|
+
issue6(
|
|
2347
|
+
"QFAI-TRACE-010",
|
|
2348
|
+
`SC \u304C\u30C6\u30B9\u30C8\u3067\u53C2\u7167\u3055\u308C\u3066\u3044\u307E\u305B\u3093: ${scWithoutTests.join(
|
|
2349
|
+
", "
|
|
2350
|
+
)}\u3002testFileGlobs \u306B\u4E00\u81F4\u3059\u308B\u30C6\u30B9\u30C8\u30D5\u30A1\u30A4\u30EB\u3078 QFAI:SC-xxxx \u3092\u8A18\u8F09\u3057\u3066\u304F\u3060\u3055\u3044\u3002`,
|
|
2351
|
+
config.validation.traceability.scNoTestSeverity,
|
|
2352
|
+
testsRoot,
|
|
2353
|
+
"traceability.scMustHaveTest",
|
|
2354
|
+
scWithoutTests
|
|
2355
|
+
)
|
|
2356
|
+
);
|
|
2357
|
+
}
|
|
2358
|
+
}
|
|
2359
|
+
const unknownScIds = Array.from(scTestRefs.keys()).filter(
|
|
2360
|
+
(id) => !scIdsInScenarios.has(id)
|
|
2361
|
+
);
|
|
2362
|
+
if (unknownScIds.length > 0) {
|
|
2363
|
+
issues.push(
|
|
2364
|
+
issue6(
|
|
2365
|
+
"QFAI-TRACE-011",
|
|
2366
|
+
`\u30C6\u30B9\u30C8\u304C\u672A\u77E5\u306E SC \u3092\u30A2\u30CE\u30C6\u30FC\u30B7\u30E7\u30F3\u53C2\u7167\u3057\u3066\u3044\u307E\u3059: ${unknownScIds.join(
|
|
2367
|
+
", "
|
|
2368
|
+
)}`,
|
|
2369
|
+
"error",
|
|
2370
|
+
testsRoot,
|
|
2371
|
+
"traceability.scUnknownInTests",
|
|
2372
|
+
unknownScIds
|
|
2373
|
+
)
|
|
2374
|
+
);
|
|
2375
|
+
}
|
|
2376
|
+
}
|
|
2113
2377
|
if (!config.validation.traceability.allowOrphanContracts) {
|
|
2114
2378
|
if (contractIds.size > 0) {
|
|
2115
2379
|
const orphanContracts = Array.from(contractIds).filter(
|
|
@@ -2158,7 +2422,7 @@ async function validateCodeReferences(upstreamIds, srcRoot, testsRoot) {
|
|
|
2158
2422
|
const pattern = buildIdPattern(Array.from(upstreamIds));
|
|
2159
2423
|
let found = false;
|
|
2160
2424
|
for (const file of targetFiles) {
|
|
2161
|
-
const text = await
|
|
2425
|
+
const text = await readFile10(file, "utf-8");
|
|
2162
2426
|
if (pattern.test(text)) {
|
|
2163
2427
|
found = true;
|
|
2164
2428
|
break;
|
|
@@ -2168,8 +2432,8 @@ async function validateCodeReferences(upstreamIds, srcRoot, testsRoot) {
|
|
|
2168
2432
|
issues.push(
|
|
2169
2433
|
issue6(
|
|
2170
2434
|
"QFAI-TRACE-002",
|
|
2171
|
-
"\u4E0A\u6D41 ID \u304C\u30B3\u30FC\u30C9/\u30C6\u30B9\u30C8\u306B\u53C2\u7167\u3055\u308C\u3066\u3044\u307E\u305B\u3093\u3002",
|
|
2172
|
-
"
|
|
2435
|
+
"\u4E0A\u6D41 ID \u304C\u30B3\u30FC\u30C9/\u30C6\u30B9\u30C8\u306B\u53C2\u7167\u3055\u308C\u3066\u3044\u307E\u305B\u3093\uFF08\u53C2\u8003\u60C5\u5831\uFF09\u3002",
|
|
2436
|
+
"info",
|
|
2173
2437
|
srcRoot,
|
|
2174
2438
|
"traceability.codeReferences"
|
|
2175
2439
|
)
|
|
@@ -2212,12 +2476,24 @@ async function validateProject(root, configResult) {
|
|
|
2212
2476
|
...await validateDefinedIds(root, config),
|
|
2213
2477
|
...await validateTraceability(root, config)
|
|
2214
2478
|
];
|
|
2479
|
+
const specsRoot = resolvePath(root, config, "specsDir");
|
|
2480
|
+
const scenarioFiles = await collectScenarioFiles(specsRoot);
|
|
2481
|
+
const scIds = await collectScIdsFromScenarioFiles(scenarioFiles);
|
|
2482
|
+
const { refs: scTestRefs, scan: testFiles } = await collectScTestReferences(
|
|
2483
|
+
root,
|
|
2484
|
+
config.validation.traceability.testFileGlobs,
|
|
2485
|
+
config.validation.traceability.testFileExcludeGlobs
|
|
2486
|
+
);
|
|
2487
|
+
const scCoverage = buildScCoverage(scIds, scTestRefs);
|
|
2215
2488
|
const toolVersion = await resolveToolVersion();
|
|
2216
2489
|
return {
|
|
2217
|
-
schemaVersion: VALIDATION_SCHEMA_VERSION,
|
|
2218
2490
|
toolVersion,
|
|
2219
2491
|
issues,
|
|
2220
|
-
counts: countIssues(issues)
|
|
2492
|
+
counts: countIssues(issues),
|
|
2493
|
+
traceability: {
|
|
2494
|
+
sc: scCoverage,
|
|
2495
|
+
testFiles
|
|
2496
|
+
}
|
|
2221
2497
|
};
|
|
2222
2498
|
}
|
|
2223
2499
|
function countIssues(issues) {
|
|
@@ -2238,9 +2514,9 @@ async function createReportData(root, validation, configResult) {
|
|
|
2238
2514
|
const configPath = resolved.configPath;
|
|
2239
2515
|
const specsRoot = resolvePath(root, config, "specsDir");
|
|
2240
2516
|
const contractsRoot = resolvePath(root, config, "contractsDir");
|
|
2241
|
-
const apiRoot =
|
|
2242
|
-
const uiRoot =
|
|
2243
|
-
const dbRoot =
|
|
2517
|
+
const apiRoot = path14.join(contractsRoot, "api");
|
|
2518
|
+
const uiRoot = path14.join(contractsRoot, "ui");
|
|
2519
|
+
const dbRoot = path14.join(contractsRoot, "db");
|
|
2244
2520
|
const srcRoot = resolvePath(root, config, "srcDir");
|
|
2245
2521
|
const testsRoot = resolvePath(root, config, "testsDir");
|
|
2246
2522
|
const specFiles = await collectSpecFiles(specsRoot);
|
|
@@ -2266,6 +2542,16 @@ async function createReportData(root, validation, configResult) {
|
|
|
2266
2542
|
srcRoot,
|
|
2267
2543
|
testsRoot
|
|
2268
2544
|
);
|
|
2545
|
+
const scIds = await collectScIdsFromScenarioFiles(scenarioFiles);
|
|
2546
|
+
const scRefsResult = await collectScTestReferences(
|
|
2547
|
+
root,
|
|
2548
|
+
config.validation.traceability.testFileGlobs,
|
|
2549
|
+
config.validation.traceability.testFileExcludeGlobs
|
|
2550
|
+
);
|
|
2551
|
+
const scCoverage = validation?.traceability?.sc ?? buildScCoverage(scIds, scRefsResult.refs);
|
|
2552
|
+
const testFiles = validation?.traceability?.testFiles ?? scRefsResult.scan;
|
|
2553
|
+
const scSources = await collectScIdSourcesFromScenarioFiles(scenarioFiles);
|
|
2554
|
+
const scSourceRecord = mapToSortedRecord(scSources);
|
|
2269
2555
|
const resolvedValidation = validation ?? await validateProject(root, resolved);
|
|
2270
2556
|
const version = await resolveToolVersion();
|
|
2271
2557
|
return {
|
|
@@ -2294,7 +2580,10 @@ async function createReportData(root, validation, configResult) {
|
|
|
2294
2580
|
},
|
|
2295
2581
|
traceability: {
|
|
2296
2582
|
upstreamIdsFound: upstreamIds.size,
|
|
2297
|
-
referencedInCodeOrTests: traceability
|
|
2583
|
+
referencedInCodeOrTests: traceability,
|
|
2584
|
+
sc: scCoverage,
|
|
2585
|
+
scSources: scSourceRecord,
|
|
2586
|
+
testFiles
|
|
2298
2587
|
},
|
|
2299
2588
|
issues: resolvedValidation.issues
|
|
2300
2589
|
};
|
|
@@ -2331,6 +2620,65 @@ function formatReportMarkdown(data) {
|
|
|
2331
2620
|
`- \u30B3\u30FC\u30C9/\u30C6\u30B9\u30C8\u53C2\u7167: ${data.traceability.referencedInCodeOrTests ? "\u3042\u308A" : "\u306A\u3057"}`
|
|
2332
2621
|
);
|
|
2333
2622
|
lines.push("");
|
|
2623
|
+
lines.push("## SC\u30AB\u30D0\u30EC\u30C3\u30B8");
|
|
2624
|
+
lines.push(`- total: ${data.traceability.sc.total}`);
|
|
2625
|
+
lines.push(`- covered: ${data.traceability.sc.covered}`);
|
|
2626
|
+
lines.push(`- missing: ${data.traceability.sc.missing}`);
|
|
2627
|
+
lines.push(
|
|
2628
|
+
`- testFileGlobs: ${formatList(data.traceability.testFiles.globs)}`
|
|
2629
|
+
);
|
|
2630
|
+
lines.push(
|
|
2631
|
+
`- testFileExcludeGlobs: ${formatList(
|
|
2632
|
+
data.traceability.testFiles.excludeGlobs
|
|
2633
|
+
)}`
|
|
2634
|
+
);
|
|
2635
|
+
lines.push(
|
|
2636
|
+
`- testFileCount: ${data.traceability.testFiles.matchedFileCount}`
|
|
2637
|
+
);
|
|
2638
|
+
if (data.traceability.sc.missingIds.length === 0) {
|
|
2639
|
+
lines.push("- missingIds: (none)");
|
|
2640
|
+
} else {
|
|
2641
|
+
const sources = data.traceability.scSources;
|
|
2642
|
+
const missingWithSources = data.traceability.sc.missingIds.map((id) => {
|
|
2643
|
+
const files = sources[id] ?? [];
|
|
2644
|
+
if (files.length === 0) {
|
|
2645
|
+
return id;
|
|
2646
|
+
}
|
|
2647
|
+
return `${id} (${files.join(", ")})`;
|
|
2648
|
+
});
|
|
2649
|
+
lines.push(`- missingIds: ${missingWithSources.join(", ")}`);
|
|
2650
|
+
}
|
|
2651
|
+
lines.push("");
|
|
2652
|
+
lines.push("## SC\u2192\u53C2\u7167\u30C6\u30B9\u30C8");
|
|
2653
|
+
const scRefs = data.traceability.sc.refs;
|
|
2654
|
+
const scIds = Object.keys(scRefs).sort((a, b) => a.localeCompare(b));
|
|
2655
|
+
if (scIds.length === 0) {
|
|
2656
|
+
lines.push("- (none)");
|
|
2657
|
+
} else {
|
|
2658
|
+
for (const scId of scIds) {
|
|
2659
|
+
const refs = scRefs[scId] ?? [];
|
|
2660
|
+
if (refs.length === 0) {
|
|
2661
|
+
lines.push(`- ${scId}: (none)`);
|
|
2662
|
+
} else {
|
|
2663
|
+
lines.push(`- ${scId}: ${refs.join(", ")}`);
|
|
2664
|
+
}
|
|
2665
|
+
}
|
|
2666
|
+
}
|
|
2667
|
+
lines.push("");
|
|
2668
|
+
lines.push("## Spec:SC=1:1 \u9055\u53CD");
|
|
2669
|
+
const specScIssues = data.issues.filter(
|
|
2670
|
+
(item) => item.code === "QFAI-TRACE-012"
|
|
2671
|
+
);
|
|
2672
|
+
if (specScIssues.length === 0) {
|
|
2673
|
+
lines.push("- (none)");
|
|
2674
|
+
} else {
|
|
2675
|
+
for (const item of specScIssues) {
|
|
2676
|
+
const location = item.file ?? "(unknown)";
|
|
2677
|
+
const refs = item.refs && item.refs.length > 0 ? item.refs.join(", ") : item.message;
|
|
2678
|
+
lines.push(`- ${location}: ${refs}`);
|
|
2679
|
+
}
|
|
2680
|
+
}
|
|
2681
|
+
lines.push("");
|
|
2334
2682
|
lines.push("## Hotspots");
|
|
2335
2683
|
const hotspots = buildHotspots(data.issues);
|
|
2336
2684
|
if (hotspots.length === 0) {
|
|
@@ -2385,25 +2733,25 @@ async function collectIds(files) {
|
|
|
2385
2733
|
DATA: /* @__PURE__ */ new Set()
|
|
2386
2734
|
};
|
|
2387
2735
|
for (const file of files) {
|
|
2388
|
-
const text = await
|
|
2736
|
+
const text = await readFile11(file, "utf-8");
|
|
2389
2737
|
for (const prefix of ID_PREFIXES2) {
|
|
2390
2738
|
const ids = extractIds(text, prefix);
|
|
2391
2739
|
ids.forEach((id) => result[prefix].add(id));
|
|
2392
2740
|
}
|
|
2393
2741
|
}
|
|
2394
2742
|
return {
|
|
2395
|
-
SPEC:
|
|
2396
|
-
BR:
|
|
2397
|
-
SC:
|
|
2398
|
-
UI:
|
|
2399
|
-
API:
|
|
2400
|
-
DATA:
|
|
2743
|
+
SPEC: toSortedArray2(result.SPEC),
|
|
2744
|
+
BR: toSortedArray2(result.BR),
|
|
2745
|
+
SC: toSortedArray2(result.SC),
|
|
2746
|
+
UI: toSortedArray2(result.UI),
|
|
2747
|
+
API: toSortedArray2(result.API),
|
|
2748
|
+
DATA: toSortedArray2(result.DATA)
|
|
2401
2749
|
};
|
|
2402
2750
|
}
|
|
2403
2751
|
async function collectUpstreamIds(files) {
|
|
2404
2752
|
const ids = /* @__PURE__ */ new Set();
|
|
2405
2753
|
for (const file of files) {
|
|
2406
|
-
const text = await
|
|
2754
|
+
const text = await readFile11(file, "utf-8");
|
|
2407
2755
|
extractAllIds(text).forEach((id) => ids.add(id));
|
|
2408
2756
|
}
|
|
2409
2757
|
return ids;
|
|
@@ -2424,7 +2772,7 @@ async function evaluateTraceability(upstreamIds, srcRoot, testsRoot) {
|
|
|
2424
2772
|
}
|
|
2425
2773
|
const pattern = buildIdPattern2(Array.from(upstreamIds));
|
|
2426
2774
|
for (const file of targetFiles) {
|
|
2427
|
-
const text = await
|
|
2775
|
+
const text = await readFile11(file, "utf-8");
|
|
2428
2776
|
if (pattern.test(text)) {
|
|
2429
2777
|
return true;
|
|
2430
2778
|
}
|
|
@@ -2441,9 +2789,22 @@ function formatIdLine(label, values) {
|
|
|
2441
2789
|
}
|
|
2442
2790
|
return `- ${label}: ${values.join(", ")}`;
|
|
2443
2791
|
}
|
|
2444
|
-
function
|
|
2792
|
+
function formatList(values) {
|
|
2793
|
+
if (values.length === 0) {
|
|
2794
|
+
return "(none)";
|
|
2795
|
+
}
|
|
2796
|
+
return values.join(", ");
|
|
2797
|
+
}
|
|
2798
|
+
function toSortedArray2(values) {
|
|
2445
2799
|
return Array.from(values).sort((a, b) => a.localeCompare(b));
|
|
2446
2800
|
}
|
|
2801
|
+
function mapToSortedRecord(values) {
|
|
2802
|
+
const record2 = {};
|
|
2803
|
+
for (const [key, files] of values.entries()) {
|
|
2804
|
+
record2[key] = Array.from(files).sort((a, b) => a.localeCompare(b));
|
|
2805
|
+
}
|
|
2806
|
+
return record2;
|
|
2807
|
+
}
|
|
2447
2808
|
function buildHotspots(issues) {
|
|
2448
2809
|
const map = /* @__PURE__ */ new Map();
|
|
2449
2810
|
for (const issue7 of issues) {
|
|
@@ -2468,10 +2829,10 @@ function buildHotspots(issues) {
|
|
|
2468
2829
|
|
|
2469
2830
|
// src/cli/commands/report.ts
|
|
2470
2831
|
async function runReport(options) {
|
|
2471
|
-
const root =
|
|
2832
|
+
const root = path15.resolve(options.root);
|
|
2472
2833
|
const configResult = await loadConfig(root);
|
|
2473
2834
|
const input = configResult.config.output.validateJsonPath;
|
|
2474
|
-
const inputPath =
|
|
2835
|
+
const inputPath = path15.isAbsolute(input) ? input : path15.resolve(root, input);
|
|
2475
2836
|
let validation;
|
|
2476
2837
|
try {
|
|
2477
2838
|
validation = await readValidationResult(inputPath);
|
|
@@ -2496,10 +2857,10 @@ async function runReport(options) {
|
|
|
2496
2857
|
const data = await createReportData(root, validation, configResult);
|
|
2497
2858
|
const output = options.format === "json" ? formatReportJson(data) : formatReportMarkdown(data);
|
|
2498
2859
|
const outRoot = resolvePath(root, configResult.config, "outDir");
|
|
2499
|
-
const defaultOut = options.format === "json" ?
|
|
2860
|
+
const defaultOut = options.format === "json" ? path15.join(outRoot, "report.json") : path15.join(outRoot, "report.md");
|
|
2500
2861
|
const out = options.outPath ?? defaultOut;
|
|
2501
|
-
const outPath =
|
|
2502
|
-
await mkdir2(
|
|
2862
|
+
const outPath = path15.isAbsolute(out) ? out : path15.resolve(root, out);
|
|
2863
|
+
await mkdir2(path15.dirname(outPath), { recursive: true });
|
|
2503
2864
|
await writeFile(outPath, `${output}
|
|
2504
2865
|
`, "utf-8");
|
|
2505
2866
|
info(
|
|
@@ -2508,16 +2869,11 @@ async function runReport(options) {
|
|
|
2508
2869
|
info(`wrote report: ${outPath}`);
|
|
2509
2870
|
}
|
|
2510
2871
|
async function readValidationResult(inputPath) {
|
|
2511
|
-
const raw = await
|
|
2872
|
+
const raw = await readFile12(inputPath, "utf-8");
|
|
2512
2873
|
const parsed = JSON.parse(raw);
|
|
2513
2874
|
if (!isValidationResult(parsed)) {
|
|
2514
2875
|
throw new Error(`validate.json \u306E\u5F62\u5F0F\u304C\u4E0D\u6B63\u3067\u3059: ${inputPath}`);
|
|
2515
2876
|
}
|
|
2516
|
-
if (parsed.schemaVersion !== VALIDATION_SCHEMA_VERSION) {
|
|
2517
|
-
throw new Error(
|
|
2518
|
-
`validate.json \u306E schemaVersion \u304C\u4E0D\u4E00\u81F4\u3067\u3059: expected ${VALIDATION_SCHEMA_VERSION}, actual ${parsed.schemaVersion}`
|
|
2519
|
-
);
|
|
2520
|
-
}
|
|
2521
2877
|
return parsed;
|
|
2522
2878
|
}
|
|
2523
2879
|
function isValidationResult(value) {
|
|
@@ -2525,9 +2881,6 @@ function isValidationResult(value) {
|
|
|
2525
2881
|
return false;
|
|
2526
2882
|
}
|
|
2527
2883
|
const record2 = value;
|
|
2528
|
-
if (typeof record2.schemaVersion !== "string") {
|
|
2529
|
-
return false;
|
|
2530
|
-
}
|
|
2531
2884
|
if (typeof record2.toolVersion !== "string") {
|
|
2532
2885
|
return false;
|
|
2533
2886
|
}
|
|
@@ -2550,7 +2903,7 @@ function isMissingFileError5(error2) {
|
|
|
2550
2903
|
|
|
2551
2904
|
// src/cli/commands/validate.ts
|
|
2552
2905
|
import { mkdir as mkdir3, writeFile as writeFile2 } from "fs/promises";
|
|
2553
|
-
import
|
|
2906
|
+
import path16 from "path";
|
|
2554
2907
|
|
|
2555
2908
|
// src/cli/lib/failOn.ts
|
|
2556
2909
|
function shouldFail(result, failOn) {
|
|
@@ -2565,7 +2918,7 @@ function shouldFail(result, failOn) {
|
|
|
2565
2918
|
|
|
2566
2919
|
// src/cli/commands/validate.ts
|
|
2567
2920
|
async function runValidate(options) {
|
|
2568
|
-
const root =
|
|
2921
|
+
const root = path16.resolve(options.root);
|
|
2569
2922
|
const configResult = await loadConfig(root);
|
|
2570
2923
|
const result = await validateProject(root, configResult);
|
|
2571
2924
|
const format = options.format ?? "text";
|
|
@@ -2614,8 +2967,8 @@ function emitGitHub(issue7) {
|
|
|
2614
2967
|
);
|
|
2615
2968
|
}
|
|
2616
2969
|
async function emitJson(result, root, jsonPath) {
|
|
2617
|
-
const abs =
|
|
2618
|
-
await mkdir3(
|
|
2970
|
+
const abs = path16.isAbsolute(jsonPath) ? jsonPath : path16.resolve(root, jsonPath);
|
|
2971
|
+
await mkdir3(path16.dirname(abs), { recursive: true });
|
|
2619
2972
|
await writeFile2(abs, `${JSON.stringify(result, null, 2)}
|
|
2620
2973
|
`, "utf-8");
|
|
2621
2974
|
}
|