artifact-graph 0.9.0 → 0.9.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/CHANGELOG.md +30 -0
- package/dist/cli.js +111 -51
- package/dist/index.cjs +111 -51
- package/dist/index.js +111 -51
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,35 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.9.2
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- Scenario explicit relation field recognition is tightened to whole-line field syntax matching:
|
|
8
|
+
plain prose mentions of `关联功能` / `关联决策` / `关联实体` no longer produce edges or
|
|
9
|
+
spurious `FORMAT_ERROR` diagnostics.
|
|
10
|
+
- An invalid explicit relation reference is no longer reported twice across the scan and
|
|
11
|
+
validation stages: `validateGraph` now returns exactly one `FORMAT_ERROR` carrying the file,
|
|
12
|
+
line number, node, invalid value, and the expected rule.
|
|
13
|
+
- Behavior tightening: Markdown list-item forms such as `- **关联决策**: ADR-0001` are no longer
|
|
14
|
+
recognized as explicit relation fields (0.9.1 misidentified them via substring matching);
|
|
15
|
+
standard line-start field syntax is unaffected.
|
|
16
|
+
|
|
17
|
+
## 0.9.1
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
|
|
21
|
+
- Scenario explicit relation fields (`关联决策`, `关联功能`, `关联实体`) and design-document
|
|
22
|
+
frontmatter `related_decisions` now validate each reference against the project's configured
|
|
23
|
+
`idPatterns`, so custom identifier schemes such as `ADR-0002` or `FEAT-12` produce graph edges.
|
|
24
|
+
Invalid identifiers emit diagnostics with file and line numbers instead of being silently
|
|
25
|
+
dropped, while explicit empty markers such as `无` produce neither edges nor diagnostics.
|
|
26
|
+
- Design documents now read `related_decisions` as a structured frontmatter field (edge source
|
|
27
|
+
`frontmatter`), scan only the body for prose references, and never emit duplicate edges for
|
|
28
|
+
the same relation.
|
|
29
|
+
- `query --from` and `render --from` now resolve a bare identifier to its unique graph node
|
|
30
|
+
regardless of the configured `idPatterns`, and report an ambiguity diagnostic asking for a
|
|
31
|
+
full `type:id` when multiple artifact types share the same bare identifier.
|
|
32
|
+
|
|
3
33
|
## 0.9.0
|
|
4
34
|
|
|
5
35
|
### Changed
|
package/dist/cli.js
CHANGED
|
@@ -5072,6 +5072,7 @@ function validateScenarioPrdLinks(graph, schema = DEFAULT_SCHEMA) {
|
|
|
5072
5072
|
}
|
|
5073
5073
|
pushDuplicateIssues(issues, scenarioUid, validRefs, "feature");
|
|
5074
5074
|
for (const ref of refs) {
|
|
5075
|
+
if (ref.valid === false) continue;
|
|
5075
5076
|
if (!featurePattern.test(ref.target)) {
|
|
5076
5077
|
issues.push(issue("FORMAT_ERROR", `scenario ${scenarioUid} has invalid feature reference ${ref.target}`, ref.path, ref.line, { node: scenarioUid }));
|
|
5077
5078
|
}
|
|
@@ -5268,7 +5269,7 @@ function validateCodeCommentScenarioFeatureConsistency(graph) {
|
|
|
5268
5269
|
}
|
|
5269
5270
|
function queryGraph(graph, options) {
|
|
5270
5271
|
const depth = options.depth ?? 1;
|
|
5271
|
-
const start =
|
|
5272
|
+
const start = resolveQueryStartUid(graph, options.from ?? options.to ?? "");
|
|
5272
5273
|
const reverse = Boolean(options.to && !options.from);
|
|
5273
5274
|
const selected = /* @__PURE__ */ new Set([start]);
|
|
5274
5275
|
let frontier = /* @__PURE__ */ new Set([start]);
|
|
@@ -5370,7 +5371,7 @@ function parseFile(type, path, raw, schema = DEFAULT_SCHEMA) {
|
|
|
5370
5371
|
return { ...parseFeature(path, raw), diagnostics: [] };
|
|
5371
5372
|
}
|
|
5372
5373
|
if (type === "scenario") {
|
|
5373
|
-
return
|
|
5374
|
+
return parseScenarios(path, raw, schema);
|
|
5374
5375
|
}
|
|
5375
5376
|
if (type === "entity") {
|
|
5376
5377
|
return { ...parseEntityRegistry(path, raw), diagnostics: [] };
|
|
@@ -5382,7 +5383,7 @@ function parseFile(type, path, raw, schema = DEFAULT_SCHEMA) {
|
|
|
5382
5383
|
return { ...parseTest(path, raw, schema), diagnostics: [] };
|
|
5383
5384
|
}
|
|
5384
5385
|
if (type === "design") {
|
|
5385
|
-
return
|
|
5386
|
+
return parseDesign(path, raw, schema);
|
|
5386
5387
|
}
|
|
5387
5388
|
if (type === "e2e_test") {
|
|
5388
5389
|
return { ...parseE2eTest(path, raw), diagnostics: [] };
|
|
@@ -5584,13 +5585,14 @@ function parseScenarios(path, raw, schema = DEFAULT_SCHEMA) {
|
|
|
5584
5585
|
});
|
|
5585
5586
|
const nodes = [];
|
|
5586
5587
|
const edges = [];
|
|
5588
|
+
const diagnostics = [];
|
|
5587
5589
|
for (let i = 0; i < starts.length; i += 1) {
|
|
5588
5590
|
const start = starts[i];
|
|
5589
5591
|
const end = starts[i + 1]?.index ?? lines.length;
|
|
5590
5592
|
const block = lines.slice(start.index, end);
|
|
5591
|
-
const featureRefs = markdownRelationOccurrences(path, start, block, "\u5173\u8054\u529F\u80FD", "feature");
|
|
5592
|
-
const decisionRefs = markdownRelationOccurrences(path, start, block, "\u5173\u8054\u51B3\u7B56", "decision");
|
|
5593
|
-
const entityRefs = markdownRelationOccurrences(path, start, block, "\u5173\u8054\u5B9E\u4F53", "entity");
|
|
5593
|
+
const featureRefs = markdownRelationOccurrences(path, start, block, "\u5173\u8054\u529F\u80FD", "feature", schema);
|
|
5594
|
+
const decisionRefs = markdownRelationOccurrences(path, start, block, "\u5173\u8054\u51B3\u7B56", "decision", schema);
|
|
5595
|
+
const entityRefs = markdownRelationOccurrences(path, start, block, "\u5173\u8054\u5B9E\u4F53", "entity", schema);
|
|
5594
5596
|
nodes.push({
|
|
5595
5597
|
type: "scenario",
|
|
5596
5598
|
code: start.code,
|
|
@@ -5608,8 +5610,19 @@ function parseScenarios(path, raw, schema = DEFAULT_SCHEMA) {
|
|
|
5608
5610
|
edges.push(...featureRefs.filter(isValidRelationOccurrence).map((ref) => edge(toUid("scenario", start.code), toUid("feature", ref.target), "references", "markdown", path, ref.line)));
|
|
5609
5611
|
edges.push(...decisionRefs.filter(isValidRelationOccurrence).map((ref) => edge(toUid("scenario", start.code), toUid("decision", ref.target), "references", "markdown", path, ref.line)));
|
|
5610
5612
|
edges.push(...entityRefs.filter(isValidRelationOccurrence).map((ref) => edge(toUid("scenario", start.code), toUid("entity", ref.target), "references", "markdown", path, ref.line)));
|
|
5613
|
+
for (const ref of [...featureRefs, ...decisionRefs, ...entityRefs]) {
|
|
5614
|
+
if (ref.valid === false) {
|
|
5615
|
+
diagnostics.push(issue(
|
|
5616
|
+
"FORMAT_ERROR",
|
|
5617
|
+
`scenario:${start.code} has invalid ${ref.targetType} reference "${ref.target}" in field ${ref.field} (expected ${schema.idPatterns[ref.targetType] ?? DEFAULT_SCHEMA.idPatterns[ref.targetType]})`,
|
|
5618
|
+
ref.path,
|
|
5619
|
+
ref.line,
|
|
5620
|
+
{ node: toUid("scenario", start.code) }
|
|
5621
|
+
));
|
|
5622
|
+
}
|
|
5623
|
+
}
|
|
5611
5624
|
}
|
|
5612
|
-
return { nodes, edges };
|
|
5625
|
+
return { nodes, edges, diagnostics };
|
|
5613
5626
|
}
|
|
5614
5627
|
function parseEntityRegistry(path, raw) {
|
|
5615
5628
|
const nodes = [];
|
|
@@ -5921,7 +5934,7 @@ function containsTraceabilityTag(value, schema = DEFAULT_SCHEMA) {
|
|
|
5921
5934
|
const alternatives = [...tokens].sort((a, b) => b.length - a.length).map((token) => token.replace(/[-\/\\^$*+?.()|[\]{}]/g, "\\$&")).join("|");
|
|
5922
5935
|
return new RegExp(`(?:^|\\s)@(?:${alternatives})(?=\\s|$)`).test(value);
|
|
5923
5936
|
}
|
|
5924
|
-
function parseDesign(path, raw) {
|
|
5937
|
+
function parseDesign(path, raw, schema = DEFAULT_SCHEMA) {
|
|
5925
5938
|
const parsed = matter(raw);
|
|
5926
5939
|
const data = parsed.data;
|
|
5927
5940
|
const code = normalizeDesignCode(path);
|
|
@@ -5931,15 +5944,45 @@ function parseDesign(path, raw) {
|
|
|
5931
5944
|
...designFrontmatterEdges(path, code, data.related_features, "feature", "references"),
|
|
5932
5945
|
...designFrontmatterEdges(path, code, data.related_scenarios, "scenario", "references")
|
|
5933
5946
|
];
|
|
5934
|
-
|
|
5947
|
+
const diagnostics = [];
|
|
5948
|
+
const decisionPattern = relationIdPattern("decision", schema);
|
|
5949
|
+
const frontmatterDecisions = /* @__PURE__ */ new Set();
|
|
5950
|
+
for (const value of toArray(data.related_decisions)) {
|
|
5951
|
+
const candidate = String(value).trim();
|
|
5952
|
+
if (!candidate || EMPTY_RELATION_FIELD_VALUES.has(candidate.toLowerCase())) {
|
|
5953
|
+
continue;
|
|
5954
|
+
}
|
|
5955
|
+
if (!decisionPattern.test(candidate)) {
|
|
5956
|
+
diagnostics.push(issue(
|
|
5957
|
+
"FORMAT_ERROR",
|
|
5958
|
+
`design:${code} has invalid decision reference "${candidate}" in related_decisions (expected ${schema.idPatterns.decision ?? DEFAULT_SCHEMA.idPatterns.decision})`,
|
|
5959
|
+
path,
|
|
5960
|
+
1,
|
|
5961
|
+
{ node: toUid("design", code) }
|
|
5962
|
+
));
|
|
5963
|
+
continue;
|
|
5964
|
+
}
|
|
5965
|
+
if (frontmatterDecisions.has(candidate)) {
|
|
5966
|
+
continue;
|
|
5967
|
+
}
|
|
5968
|
+
frontmatterDecisions.add(candidate);
|
|
5969
|
+
edges.push(edge(toUid("design", code), toUid("decision", candidate), "references", "frontmatter", path, 1));
|
|
5970
|
+
}
|
|
5971
|
+
const hasFrontmatter = /^---\r?\n/.test(raw);
|
|
5972
|
+
const contentStart = hasFrontmatter && parsed.content ? Math.max(raw.indexOf(parsed.content), 0) : 0;
|
|
5973
|
+
const lineOffset = contentStart > 0 ? raw.slice(0, contentStart).split(/\r?\n/).length - 1 : 0;
|
|
5974
|
+
parsed.content.split(/\r?\n/).forEach((line, index) => {
|
|
5935
5975
|
for (const codeValue of extractCodes(line, "entity")) {
|
|
5936
|
-
edges.push(edge(toUid("design", code), toUid("entity", codeValue), "references", "markdown", path, index + 1));
|
|
5976
|
+
edges.push(edge(toUid("design", code), toUid("entity", codeValue), "references", "markdown", path, lineOffset + index + 1));
|
|
5937
5977
|
}
|
|
5938
5978
|
for (const codeValue of extractCodes(line, "decision")) {
|
|
5939
|
-
|
|
5979
|
+
if (frontmatterDecisions.has(codeValue)) {
|
|
5980
|
+
continue;
|
|
5981
|
+
}
|
|
5982
|
+
edges.push(edge(toUid("design", code), toUid("decision", codeValue), "references", "markdown", path, lineOffset + index + 1));
|
|
5940
5983
|
}
|
|
5941
5984
|
});
|
|
5942
|
-
return { nodes, edges };
|
|
5985
|
+
return { nodes, edges, diagnostics };
|
|
5943
5986
|
}
|
|
5944
5987
|
function parseE2eTest(path, raw) {
|
|
5945
5988
|
const parsed = matter(raw);
|
|
@@ -6784,20 +6827,25 @@ function decisionFrontmatterEdges(path, decisionCode, value, targetType, kind) {
|
|
|
6784
6827
|
return [edge(toUid("decision", decisionCode), toUid(targetType, code), kind, "frontmatter", path, 1)];
|
|
6785
6828
|
});
|
|
6786
6829
|
}
|
|
6787
|
-
function
|
|
6830
|
+
function matchExplicitRelationFieldLine(line, label) {
|
|
6831
|
+
const escaped = escapeRegExp(label);
|
|
6832
|
+
const match = new RegExp(`^\\s*(?:\\*\\*${escaped}\\*\\*|${escaped})[:\uFF1A]\\s*(.*?)\\s*$`).exec(line);
|
|
6833
|
+
return match?.[1] ?? null;
|
|
6834
|
+
}
|
|
6835
|
+
function markdownRelationOccurrences(path, scenario, block, label, targetType, schema = DEFAULT_SCHEMA) {
|
|
6788
6836
|
const result = [];
|
|
6789
6837
|
block.forEach((line, index) => {
|
|
6790
|
-
|
|
6838
|
+
const relationText = matchExplicitRelationFieldLine(line, label);
|
|
6839
|
+
if (!relationText) {
|
|
6791
6840
|
return;
|
|
6792
6841
|
}
|
|
6793
|
-
const relationText = line.split(/[::]/).slice(1).join(":").split(/\s+[—-]\s+/)[0] ?? line;
|
|
6794
6842
|
const lineNumber = scenario.index + index + 1;
|
|
6795
|
-
const
|
|
6796
|
-
for (const code of
|
|
6797
|
-
result.push({ field: label, targetType, target: code, path, line: lineNumber, raw: relationText
|
|
6843
|
+
const { valid, invalid } = parseExplicitRelationField(relationText, targetType, schema);
|
|
6844
|
+
for (const code of valid) {
|
|
6845
|
+
result.push({ field: label, targetType, target: code, path, line: lineNumber, raw: relationText, valid: true });
|
|
6798
6846
|
}
|
|
6799
|
-
for (const
|
|
6800
|
-
result.push({ field: label, targetType, target:
|
|
6847
|
+
for (const code of invalid) {
|
|
6848
|
+
result.push({ field: label, targetType, target: code, path, line: lineNumber, raw: relationText, valid: false });
|
|
6801
6849
|
}
|
|
6802
6850
|
});
|
|
6803
6851
|
return result;
|
|
@@ -6820,7 +6868,8 @@ function relationOccurrences(node, field, targetType) {
|
|
|
6820
6868
|
target,
|
|
6821
6869
|
path: typeof record.path === "string" ? record.path : node.path,
|
|
6822
6870
|
line: typeof record.line === "number" ? record.line : node.line,
|
|
6823
|
-
raw: typeof record.raw === "string" ? record.raw : void 0
|
|
6871
|
+
raw: typeof record.raw === "string" ? record.raw : void 0,
|
|
6872
|
+
valid: typeof record.valid === "boolean" ? record.valid : void 0
|
|
6824
6873
|
}];
|
|
6825
6874
|
});
|
|
6826
6875
|
}
|
|
@@ -6835,28 +6884,40 @@ function pushDuplicateIssues(issues, nodeUid, refs, targetType) {
|
|
|
6835
6884
|
}
|
|
6836
6885
|
}
|
|
6837
6886
|
function isValidRelationOccurrence(ref) {
|
|
6887
|
+
if (ref.valid !== void 0) {
|
|
6888
|
+
return ref.valid;
|
|
6889
|
+
}
|
|
6838
6890
|
return relationTargetPattern(ref.targetType).test(ref.target);
|
|
6839
6891
|
}
|
|
6840
|
-
function
|
|
6841
|
-
|
|
6842
|
-
const candidates = text.split(/[\s,,、;;()[\]()]+/).map((token) => token.trim().replace(/^["'`]+|["'`.::]+$/g, "")).filter(Boolean);
|
|
6843
|
-
const pattern = relationTargetPattern(targetType);
|
|
6844
|
-
return [...new Set(candidates.filter((candidate) => looksLikeRelationId(candidate, targetType) && !valid.has(candidate) && !pattern.test(candidate)))];
|
|
6892
|
+
function relationIdPattern(targetType, schema) {
|
|
6893
|
+
return new RegExp(schema.idPatterns[targetType] ?? DEFAULT_SCHEMA.idPatterns[targetType] ?? "^.+$");
|
|
6845
6894
|
}
|
|
6846
|
-
function
|
|
6847
|
-
|
|
6848
|
-
|
|
6849
|
-
|
|
6850
|
-
|
|
6851
|
-
|
|
6852
|
-
|
|
6853
|
-
if (targetType === "decision") {
|
|
6854
|
-
return /^D[-A-Z0-9]+$/.test(candidate);
|
|
6895
|
+
function stripRelationItemAnnotation(item) {
|
|
6896
|
+
let result = item.trim();
|
|
6897
|
+
let previous = "";
|
|
6898
|
+
while (result !== previous) {
|
|
6899
|
+
previous = result;
|
|
6900
|
+
result = result.replace(/\s*[((][^()()]*[))]\s*$/, "").trim();
|
|
6901
|
+
result = result.replace(/\s+[—–-]\s+.*$/, "").trim();
|
|
6855
6902
|
}
|
|
6856
|
-
|
|
6857
|
-
|
|
6903
|
+
return result;
|
|
6904
|
+
}
|
|
6905
|
+
function parseExplicitRelationField(text, targetType, schema) {
|
|
6906
|
+
const pattern = relationIdPattern(targetType, schema);
|
|
6907
|
+
const valid = [];
|
|
6908
|
+
const invalid = [];
|
|
6909
|
+
for (const item of text.split(/[,,、;;]/)) {
|
|
6910
|
+
const candidate = stripRelationItemAnnotation(item);
|
|
6911
|
+
if (!candidate || EMPTY_RELATION_FIELD_VALUES.has(candidate.toLowerCase())) {
|
|
6912
|
+
continue;
|
|
6913
|
+
}
|
|
6914
|
+
if (pattern.test(candidate)) {
|
|
6915
|
+
valid.push(candidate);
|
|
6916
|
+
} else if (!invalid.includes(candidate)) {
|
|
6917
|
+
invalid.push(candidate);
|
|
6918
|
+
}
|
|
6858
6919
|
}
|
|
6859
|
-
return
|
|
6920
|
+
return { valid, invalid };
|
|
6860
6921
|
}
|
|
6861
6922
|
function relationTargetPattern(targetType) {
|
|
6862
6923
|
const patterns = {
|
|
@@ -7979,21 +8040,19 @@ function globToRegExp(pattern) {
|
|
|
7979
8040
|
}
|
|
7980
8041
|
return new RegExp(`${source}$`);
|
|
7981
8042
|
}
|
|
7982
|
-
function
|
|
8043
|
+
function resolveQueryStartUid(graph, value) {
|
|
7983
8044
|
if (value.includes(":")) {
|
|
7984
8045
|
return value;
|
|
7985
8046
|
}
|
|
7986
|
-
|
|
7987
|
-
|
|
8047
|
+
const matches = graph.nodes.filter((node) => node.code === value);
|
|
8048
|
+
if (matches.length === 1) {
|
|
8049
|
+
return matches[0].uid;
|
|
7988
8050
|
}
|
|
7989
|
-
if (
|
|
7990
|
-
|
|
7991
|
-
|
|
7992
|
-
|
|
7993
|
-
|
|
7994
|
-
}
|
|
7995
|
-
if (/^[A-Z]{1,4}\d+$/.test(value)) {
|
|
7996
|
-
return toUid("feature", value);
|
|
8051
|
+
if (matches.length > 1) {
|
|
8052
|
+
const candidates = matches.map((node) => node.uid).sort();
|
|
8053
|
+
throw new Error(
|
|
8054
|
+
`Ambiguous artifact id "${value}": matches ${candidates.join(", ")}. Use a fully qualified uid (type:id), for example ${candidates[0]}`
|
|
8055
|
+
);
|
|
7997
8056
|
}
|
|
7998
8057
|
return value;
|
|
7999
8058
|
}
|
|
@@ -8497,7 +8556,7 @@ function formatContextMarkdown(manifest) {
|
|
|
8497
8556
|
}
|
|
8498
8557
|
return lines.join("\n");
|
|
8499
8558
|
}
|
|
8500
|
-
var TARGET_ARTIFACT_TYPES, NON_TARGET_ROLES, DEFAULT_SCHEMA, VALID_TC_STATUSES, VALID_CHAIN_TYPES, DEPRECATED_CHAIN_TYPE_ALIASES, CONTEXT_CATEGORIES, TIER_ORDER;
|
|
8559
|
+
var TARGET_ARTIFACT_TYPES, NON_TARGET_ROLES, DEFAULT_SCHEMA, EMPTY_RELATION_FIELD_VALUES, VALID_TC_STATUSES, VALID_CHAIN_TYPES, DEPRECATED_CHAIN_TYPE_ALIASES, CONTEXT_CATEGORIES, TIER_ORDER;
|
|
8501
8560
|
var init_index = __esm({
|
|
8502
8561
|
"src/index.ts"() {
|
|
8503
8562
|
"use strict";
|
|
@@ -8551,7 +8610,7 @@ var init_index = __esm({
|
|
|
8551
8610
|
relationFields: {
|
|
8552
8611
|
feature: ["scenarios", "decisions", "depends_on", "design_docs"],
|
|
8553
8612
|
scenario: ["\u5173\u8054\u529F\u80FD", "\u5173\u8054\u51B3\u7B56"],
|
|
8554
|
-
design: ["related_features", "related_scenarios"],
|
|
8613
|
+
design: ["related_features", "related_scenarios", "related_decisions"],
|
|
8555
8614
|
test: ["@scenario", "@feature", "@entity", "@decision"],
|
|
8556
8615
|
e2e_test: ["test_batch", "scope", "ac_coverage", "related_scenarios", "related_decisions", "related_entities", "\u8986\u76D6\u573A\u666F", "\u8986\u76D6\u529F\u80FD"],
|
|
8557
8616
|
e2e_registry: ["batches"]
|
|
@@ -8566,6 +8625,7 @@ var init_index = __esm({
|
|
|
8566
8625
|
runners: []
|
|
8567
8626
|
}
|
|
8568
8627
|
};
|
|
8628
|
+
EMPTY_RELATION_FIELD_VALUES = /* @__PURE__ */ new Set(["\u65E0", "none", "n/a", "-"]);
|
|
8569
8629
|
VALID_TC_STATUSES = /* @__PURE__ */ new Set(["created", "automated", "verified", "waived"]);
|
|
8570
8630
|
VALID_CHAIN_TYPES = /* @__PURE__ */ new Set([
|
|
8571
8631
|
"desktop_chain",
|
package/dist/index.cjs
CHANGED
|
@@ -5050,6 +5050,7 @@ function validateScenarioPrdLinks(graph, schema = DEFAULT_SCHEMA) {
|
|
|
5050
5050
|
}
|
|
5051
5051
|
pushDuplicateIssues(issues, scenarioUid, validRefs, "feature");
|
|
5052
5052
|
for (const ref of refs) {
|
|
5053
|
+
if (ref.valid === false) continue;
|
|
5053
5054
|
if (!featurePattern.test(ref.target)) {
|
|
5054
5055
|
issues.push(issue("FORMAT_ERROR", `scenario ${scenarioUid} has invalid feature reference ${ref.target}`, ref.path, ref.line, { node: scenarioUid }));
|
|
5055
5056
|
}
|
|
@@ -5246,7 +5247,7 @@ function validateCodeCommentScenarioFeatureConsistency(graph) {
|
|
|
5246
5247
|
}
|
|
5247
5248
|
function queryGraph(graph, options) {
|
|
5248
5249
|
const depth = options.depth ?? 1;
|
|
5249
|
-
const start =
|
|
5250
|
+
const start = resolveQueryStartUid(graph, options.from ?? options.to ?? "");
|
|
5250
5251
|
const reverse = Boolean(options.to && !options.from);
|
|
5251
5252
|
const selected = /* @__PURE__ */ new Set([start]);
|
|
5252
5253
|
let frontier = /* @__PURE__ */ new Set([start]);
|
|
@@ -5348,7 +5349,7 @@ function parseFile(type, path, raw, schema = DEFAULT_SCHEMA) {
|
|
|
5348
5349
|
return { ...parseFeature(path, raw), diagnostics: [] };
|
|
5349
5350
|
}
|
|
5350
5351
|
if (type === "scenario") {
|
|
5351
|
-
return
|
|
5352
|
+
return parseScenarios(path, raw, schema);
|
|
5352
5353
|
}
|
|
5353
5354
|
if (type === "entity") {
|
|
5354
5355
|
return { ...parseEntityRegistry(path, raw), diagnostics: [] };
|
|
@@ -5360,7 +5361,7 @@ function parseFile(type, path, raw, schema = DEFAULT_SCHEMA) {
|
|
|
5360
5361
|
return { ...parseTest(path, raw, schema), diagnostics: [] };
|
|
5361
5362
|
}
|
|
5362
5363
|
if (type === "design") {
|
|
5363
|
-
return
|
|
5364
|
+
return parseDesign(path, raw, schema);
|
|
5364
5365
|
}
|
|
5365
5366
|
if (type === "e2e_test") {
|
|
5366
5367
|
return { ...parseE2eTest(path, raw), diagnostics: [] };
|
|
@@ -5562,13 +5563,14 @@ function parseScenarios(path, raw, schema = DEFAULT_SCHEMA) {
|
|
|
5562
5563
|
});
|
|
5563
5564
|
const nodes = [];
|
|
5564
5565
|
const edges = [];
|
|
5566
|
+
const diagnostics = [];
|
|
5565
5567
|
for (let i = 0; i < starts.length; i += 1) {
|
|
5566
5568
|
const start = starts[i];
|
|
5567
5569
|
const end = starts[i + 1]?.index ?? lines.length;
|
|
5568
5570
|
const block = lines.slice(start.index, end);
|
|
5569
|
-
const featureRefs = markdownRelationOccurrences(path, start, block, "\u5173\u8054\u529F\u80FD", "feature");
|
|
5570
|
-
const decisionRefs = markdownRelationOccurrences(path, start, block, "\u5173\u8054\u51B3\u7B56", "decision");
|
|
5571
|
-
const entityRefs = markdownRelationOccurrences(path, start, block, "\u5173\u8054\u5B9E\u4F53", "entity");
|
|
5571
|
+
const featureRefs = markdownRelationOccurrences(path, start, block, "\u5173\u8054\u529F\u80FD", "feature", schema);
|
|
5572
|
+
const decisionRefs = markdownRelationOccurrences(path, start, block, "\u5173\u8054\u51B3\u7B56", "decision", schema);
|
|
5573
|
+
const entityRefs = markdownRelationOccurrences(path, start, block, "\u5173\u8054\u5B9E\u4F53", "entity", schema);
|
|
5572
5574
|
nodes.push({
|
|
5573
5575
|
type: "scenario",
|
|
5574
5576
|
code: start.code,
|
|
@@ -5586,8 +5588,19 @@ function parseScenarios(path, raw, schema = DEFAULT_SCHEMA) {
|
|
|
5586
5588
|
edges.push(...featureRefs.filter(isValidRelationOccurrence).map((ref) => edge(toUid("scenario", start.code), toUid("feature", ref.target), "references", "markdown", path, ref.line)));
|
|
5587
5589
|
edges.push(...decisionRefs.filter(isValidRelationOccurrence).map((ref) => edge(toUid("scenario", start.code), toUid("decision", ref.target), "references", "markdown", path, ref.line)));
|
|
5588
5590
|
edges.push(...entityRefs.filter(isValidRelationOccurrence).map((ref) => edge(toUid("scenario", start.code), toUid("entity", ref.target), "references", "markdown", path, ref.line)));
|
|
5591
|
+
for (const ref of [...featureRefs, ...decisionRefs, ...entityRefs]) {
|
|
5592
|
+
if (ref.valid === false) {
|
|
5593
|
+
diagnostics.push(issue(
|
|
5594
|
+
"FORMAT_ERROR",
|
|
5595
|
+
`scenario:${start.code} has invalid ${ref.targetType} reference "${ref.target}" in field ${ref.field} (expected ${schema.idPatterns[ref.targetType] ?? DEFAULT_SCHEMA.idPatterns[ref.targetType]})`,
|
|
5596
|
+
ref.path,
|
|
5597
|
+
ref.line,
|
|
5598
|
+
{ node: toUid("scenario", start.code) }
|
|
5599
|
+
));
|
|
5600
|
+
}
|
|
5601
|
+
}
|
|
5589
5602
|
}
|
|
5590
|
-
return { nodes, edges };
|
|
5603
|
+
return { nodes, edges, diagnostics };
|
|
5591
5604
|
}
|
|
5592
5605
|
function parseEntityRegistry(path, raw) {
|
|
5593
5606
|
const nodes = [];
|
|
@@ -5899,7 +5912,7 @@ function containsTraceabilityTag(value, schema = DEFAULT_SCHEMA) {
|
|
|
5899
5912
|
const alternatives = [...tokens].sort((a, b) => b.length - a.length).map((token) => token.replace(/[-\/\\^$*+?.()|[\]{}]/g, "\\$&")).join("|");
|
|
5900
5913
|
return new RegExp(`(?:^|\\s)@(?:${alternatives})(?=\\s|$)`).test(value);
|
|
5901
5914
|
}
|
|
5902
|
-
function parseDesign(path, raw) {
|
|
5915
|
+
function parseDesign(path, raw, schema = DEFAULT_SCHEMA) {
|
|
5903
5916
|
const parsed = (0, import_gray_matter.default)(raw);
|
|
5904
5917
|
const data = parsed.data;
|
|
5905
5918
|
const code = normalizeDesignCode(path);
|
|
@@ -5909,15 +5922,45 @@ function parseDesign(path, raw) {
|
|
|
5909
5922
|
...designFrontmatterEdges(path, code, data.related_features, "feature", "references"),
|
|
5910
5923
|
...designFrontmatterEdges(path, code, data.related_scenarios, "scenario", "references")
|
|
5911
5924
|
];
|
|
5912
|
-
|
|
5925
|
+
const diagnostics = [];
|
|
5926
|
+
const decisionPattern = relationIdPattern("decision", schema);
|
|
5927
|
+
const frontmatterDecisions = /* @__PURE__ */ new Set();
|
|
5928
|
+
for (const value of toArray(data.related_decisions)) {
|
|
5929
|
+
const candidate = String(value).trim();
|
|
5930
|
+
if (!candidate || EMPTY_RELATION_FIELD_VALUES.has(candidate.toLowerCase())) {
|
|
5931
|
+
continue;
|
|
5932
|
+
}
|
|
5933
|
+
if (!decisionPattern.test(candidate)) {
|
|
5934
|
+
diagnostics.push(issue(
|
|
5935
|
+
"FORMAT_ERROR",
|
|
5936
|
+
`design:${code} has invalid decision reference "${candidate}" in related_decisions (expected ${schema.idPatterns.decision ?? DEFAULT_SCHEMA.idPatterns.decision})`,
|
|
5937
|
+
path,
|
|
5938
|
+
1,
|
|
5939
|
+
{ node: toUid("design", code) }
|
|
5940
|
+
));
|
|
5941
|
+
continue;
|
|
5942
|
+
}
|
|
5943
|
+
if (frontmatterDecisions.has(candidate)) {
|
|
5944
|
+
continue;
|
|
5945
|
+
}
|
|
5946
|
+
frontmatterDecisions.add(candidate);
|
|
5947
|
+
edges.push(edge(toUid("design", code), toUid("decision", candidate), "references", "frontmatter", path, 1));
|
|
5948
|
+
}
|
|
5949
|
+
const hasFrontmatter = /^---\r?\n/.test(raw);
|
|
5950
|
+
const contentStart = hasFrontmatter && parsed.content ? Math.max(raw.indexOf(parsed.content), 0) : 0;
|
|
5951
|
+
const lineOffset = contentStart > 0 ? raw.slice(0, contentStart).split(/\r?\n/).length - 1 : 0;
|
|
5952
|
+
parsed.content.split(/\r?\n/).forEach((line, index) => {
|
|
5913
5953
|
for (const codeValue of extractCodes(line, "entity")) {
|
|
5914
|
-
edges.push(edge(toUid("design", code), toUid("entity", codeValue), "references", "markdown", path, index + 1));
|
|
5954
|
+
edges.push(edge(toUid("design", code), toUid("entity", codeValue), "references", "markdown", path, lineOffset + index + 1));
|
|
5915
5955
|
}
|
|
5916
5956
|
for (const codeValue of extractCodes(line, "decision")) {
|
|
5917
|
-
|
|
5957
|
+
if (frontmatterDecisions.has(codeValue)) {
|
|
5958
|
+
continue;
|
|
5959
|
+
}
|
|
5960
|
+
edges.push(edge(toUid("design", code), toUid("decision", codeValue), "references", "markdown", path, lineOffset + index + 1));
|
|
5918
5961
|
}
|
|
5919
5962
|
});
|
|
5920
|
-
return { nodes, edges };
|
|
5963
|
+
return { nodes, edges, diagnostics };
|
|
5921
5964
|
}
|
|
5922
5965
|
function parseE2eTest(path, raw) {
|
|
5923
5966
|
const parsed = (0, import_gray_matter.default)(raw);
|
|
@@ -6762,20 +6805,25 @@ function decisionFrontmatterEdges(path, decisionCode, value, targetType, kind) {
|
|
|
6762
6805
|
return [edge(toUid("decision", decisionCode), toUid(targetType, code), kind, "frontmatter", path, 1)];
|
|
6763
6806
|
});
|
|
6764
6807
|
}
|
|
6765
|
-
function
|
|
6808
|
+
function matchExplicitRelationFieldLine(line, label) {
|
|
6809
|
+
const escaped = escapeRegExp(label);
|
|
6810
|
+
const match = new RegExp(`^\\s*(?:\\*\\*${escaped}\\*\\*|${escaped})[:\uFF1A]\\s*(.*?)\\s*$`).exec(line);
|
|
6811
|
+
return match?.[1] ?? null;
|
|
6812
|
+
}
|
|
6813
|
+
function markdownRelationOccurrences(path, scenario, block, label, targetType, schema = DEFAULT_SCHEMA) {
|
|
6766
6814
|
const result = [];
|
|
6767
6815
|
block.forEach((line, index) => {
|
|
6768
|
-
|
|
6816
|
+
const relationText = matchExplicitRelationFieldLine(line, label);
|
|
6817
|
+
if (!relationText) {
|
|
6769
6818
|
return;
|
|
6770
6819
|
}
|
|
6771
|
-
const relationText = line.split(/[::]/).slice(1).join(":").split(/\s+[—-]\s+/)[0] ?? line;
|
|
6772
6820
|
const lineNumber = scenario.index + index + 1;
|
|
6773
|
-
const
|
|
6774
|
-
for (const code of
|
|
6775
|
-
result.push({ field: label, targetType, target: code, path, line: lineNumber, raw: relationText
|
|
6821
|
+
const { valid, invalid } = parseExplicitRelationField(relationText, targetType, schema);
|
|
6822
|
+
for (const code of valid) {
|
|
6823
|
+
result.push({ field: label, targetType, target: code, path, line: lineNumber, raw: relationText, valid: true });
|
|
6776
6824
|
}
|
|
6777
|
-
for (const
|
|
6778
|
-
result.push({ field: label, targetType, target:
|
|
6825
|
+
for (const code of invalid) {
|
|
6826
|
+
result.push({ field: label, targetType, target: code, path, line: lineNumber, raw: relationText, valid: false });
|
|
6779
6827
|
}
|
|
6780
6828
|
});
|
|
6781
6829
|
return result;
|
|
@@ -6798,7 +6846,8 @@ function relationOccurrences(node, field, targetType) {
|
|
|
6798
6846
|
target,
|
|
6799
6847
|
path: typeof record.path === "string" ? record.path : node.path,
|
|
6800
6848
|
line: typeof record.line === "number" ? record.line : node.line,
|
|
6801
|
-
raw: typeof record.raw === "string" ? record.raw : void 0
|
|
6849
|
+
raw: typeof record.raw === "string" ? record.raw : void 0,
|
|
6850
|
+
valid: typeof record.valid === "boolean" ? record.valid : void 0
|
|
6802
6851
|
}];
|
|
6803
6852
|
});
|
|
6804
6853
|
}
|
|
@@ -6813,28 +6862,40 @@ function pushDuplicateIssues(issues, nodeUid, refs, targetType) {
|
|
|
6813
6862
|
}
|
|
6814
6863
|
}
|
|
6815
6864
|
function isValidRelationOccurrence(ref) {
|
|
6865
|
+
if (ref.valid !== void 0) {
|
|
6866
|
+
return ref.valid;
|
|
6867
|
+
}
|
|
6816
6868
|
return relationTargetPattern(ref.targetType).test(ref.target);
|
|
6817
6869
|
}
|
|
6818
|
-
function
|
|
6819
|
-
|
|
6820
|
-
const candidates = text.split(/[\s,,、;;()[\]()]+/).map((token) => token.trim().replace(/^["'`]+|["'`.::]+$/g, "")).filter(Boolean);
|
|
6821
|
-
const pattern = relationTargetPattern(targetType);
|
|
6822
|
-
return [...new Set(candidates.filter((candidate) => looksLikeRelationId(candidate, targetType) && !valid.has(candidate) && !pattern.test(candidate)))];
|
|
6870
|
+
function relationIdPattern(targetType, schema) {
|
|
6871
|
+
return new RegExp(schema.idPatterns[targetType] ?? DEFAULT_SCHEMA.idPatterns[targetType] ?? "^.+$");
|
|
6823
6872
|
}
|
|
6824
|
-
function
|
|
6825
|
-
|
|
6826
|
-
|
|
6827
|
-
|
|
6828
|
-
|
|
6829
|
-
|
|
6830
|
-
|
|
6831
|
-
if (targetType === "decision") {
|
|
6832
|
-
return /^D[-A-Z0-9]+$/.test(candidate);
|
|
6873
|
+
function stripRelationItemAnnotation(item) {
|
|
6874
|
+
let result = item.trim();
|
|
6875
|
+
let previous = "";
|
|
6876
|
+
while (result !== previous) {
|
|
6877
|
+
previous = result;
|
|
6878
|
+
result = result.replace(/\s*[((][^()()]*[))]\s*$/, "").trim();
|
|
6879
|
+
result = result.replace(/\s+[—–-]\s+.*$/, "").trim();
|
|
6833
6880
|
}
|
|
6834
|
-
|
|
6835
|
-
|
|
6881
|
+
return result;
|
|
6882
|
+
}
|
|
6883
|
+
function parseExplicitRelationField(text, targetType, schema) {
|
|
6884
|
+
const pattern = relationIdPattern(targetType, schema);
|
|
6885
|
+
const valid = [];
|
|
6886
|
+
const invalid = [];
|
|
6887
|
+
for (const item of text.split(/[,,、;;]/)) {
|
|
6888
|
+
const candidate = stripRelationItemAnnotation(item);
|
|
6889
|
+
if (!candidate || EMPTY_RELATION_FIELD_VALUES.has(candidate.toLowerCase())) {
|
|
6890
|
+
continue;
|
|
6891
|
+
}
|
|
6892
|
+
if (pattern.test(candidate)) {
|
|
6893
|
+
valid.push(candidate);
|
|
6894
|
+
} else if (!invalid.includes(candidate)) {
|
|
6895
|
+
invalid.push(candidate);
|
|
6896
|
+
}
|
|
6836
6897
|
}
|
|
6837
|
-
return
|
|
6898
|
+
return { valid, invalid };
|
|
6838
6899
|
}
|
|
6839
6900
|
function relationTargetPattern(targetType) {
|
|
6840
6901
|
const patterns = {
|
|
@@ -7957,21 +8018,19 @@ function globToRegExp(pattern) {
|
|
|
7957
8018
|
}
|
|
7958
8019
|
return new RegExp(`${source}$`);
|
|
7959
8020
|
}
|
|
7960
|
-
function
|
|
8021
|
+
function resolveQueryStartUid(graph, value) {
|
|
7961
8022
|
if (value.includes(":")) {
|
|
7962
8023
|
return value;
|
|
7963
8024
|
}
|
|
7964
|
-
|
|
7965
|
-
|
|
8025
|
+
const matches = graph.nodes.filter((node) => node.code === value);
|
|
8026
|
+
if (matches.length === 1) {
|
|
8027
|
+
return matches[0].uid;
|
|
7966
8028
|
}
|
|
7967
|
-
if (
|
|
7968
|
-
|
|
7969
|
-
|
|
7970
|
-
|
|
7971
|
-
|
|
7972
|
-
}
|
|
7973
|
-
if (/^[A-Z]{1,4}\d+$/.test(value)) {
|
|
7974
|
-
return toUid("feature", value);
|
|
8029
|
+
if (matches.length > 1) {
|
|
8030
|
+
const candidates = matches.map((node) => node.uid).sort();
|
|
8031
|
+
throw new Error(
|
|
8032
|
+
`Ambiguous artifact id "${value}": matches ${candidates.join(", ")}. Use a fully qualified uid (type:id), for example ${candidates[0]}`
|
|
8033
|
+
);
|
|
7975
8034
|
}
|
|
7976
8035
|
return value;
|
|
7977
8036
|
}
|
|
@@ -8475,7 +8534,7 @@ function formatContextMarkdown(manifest) {
|
|
|
8475
8534
|
}
|
|
8476
8535
|
return lines.join("\n");
|
|
8477
8536
|
}
|
|
8478
|
-
var import_better_sqlite3, import_gray_matter, import_js_yaml, import_node_fs4, import_promises7, import_node_path8, TARGET_ARTIFACT_TYPES, NON_TARGET_ROLES, DEFAULT_SCHEMA, VALID_TC_STATUSES, VALID_CHAIN_TYPES, DEPRECATED_CHAIN_TYPE_ALIASES, CONTEXT_CATEGORIES, TIER_ORDER;
|
|
8537
|
+
var import_better_sqlite3, import_gray_matter, import_js_yaml, import_node_fs4, import_promises7, import_node_path8, TARGET_ARTIFACT_TYPES, NON_TARGET_ROLES, DEFAULT_SCHEMA, EMPTY_RELATION_FIELD_VALUES, VALID_TC_STATUSES, VALID_CHAIN_TYPES, DEPRECATED_CHAIN_TYPE_ALIASES, CONTEXT_CATEGORIES, TIER_ORDER;
|
|
8479
8538
|
var init_index = __esm({
|
|
8480
8539
|
"src/index.ts"() {
|
|
8481
8540
|
import_better_sqlite3 = __toESM(require("better-sqlite3"), 1);
|
|
@@ -8534,7 +8593,7 @@ var init_index = __esm({
|
|
|
8534
8593
|
relationFields: {
|
|
8535
8594
|
feature: ["scenarios", "decisions", "depends_on", "design_docs"],
|
|
8536
8595
|
scenario: ["\u5173\u8054\u529F\u80FD", "\u5173\u8054\u51B3\u7B56"],
|
|
8537
|
-
design: ["related_features", "related_scenarios"],
|
|
8596
|
+
design: ["related_features", "related_scenarios", "related_decisions"],
|
|
8538
8597
|
test: ["@scenario", "@feature", "@entity", "@decision"],
|
|
8539
8598
|
e2e_test: ["test_batch", "scope", "ac_coverage", "related_scenarios", "related_decisions", "related_entities", "\u8986\u76D6\u573A\u666F", "\u8986\u76D6\u529F\u80FD"],
|
|
8540
8599
|
e2e_registry: ["batches"]
|
|
@@ -8549,6 +8608,7 @@ var init_index = __esm({
|
|
|
8549
8608
|
runners: []
|
|
8550
8609
|
}
|
|
8551
8610
|
};
|
|
8611
|
+
EMPTY_RELATION_FIELD_VALUES = /* @__PURE__ */ new Set(["\u65E0", "none", "n/a", "-"]);
|
|
8552
8612
|
VALID_TC_STATUSES = /* @__PURE__ */ new Set(["created", "automated", "verified", "waived"]);
|
|
8553
8613
|
VALID_CHAIN_TYPES = /* @__PURE__ */ new Set([
|
|
8554
8614
|
"desktop_chain",
|
package/dist/index.js
CHANGED
|
@@ -4940,6 +4940,7 @@ function validateScenarioPrdLinks(graph, schema = DEFAULT_SCHEMA) {
|
|
|
4940
4940
|
}
|
|
4941
4941
|
pushDuplicateIssues(issues, scenarioUid, validRefs, "feature");
|
|
4942
4942
|
for (const ref of refs) {
|
|
4943
|
+
if (ref.valid === false) continue;
|
|
4943
4944
|
if (!featurePattern.test(ref.target)) {
|
|
4944
4945
|
issues.push(issue("FORMAT_ERROR", `scenario ${scenarioUid} has invalid feature reference ${ref.target}`, ref.path, ref.line, { node: scenarioUid }));
|
|
4945
4946
|
}
|
|
@@ -5136,7 +5137,7 @@ function validateCodeCommentScenarioFeatureConsistency(graph) {
|
|
|
5136
5137
|
}
|
|
5137
5138
|
function queryGraph(graph, options) {
|
|
5138
5139
|
const depth = options.depth ?? 1;
|
|
5139
|
-
const start =
|
|
5140
|
+
const start = resolveQueryStartUid(graph, options.from ?? options.to ?? "");
|
|
5140
5141
|
const reverse = Boolean(options.to && !options.from);
|
|
5141
5142
|
const selected = /* @__PURE__ */ new Set([start]);
|
|
5142
5143
|
let frontier = /* @__PURE__ */ new Set([start]);
|
|
@@ -5238,7 +5239,7 @@ function parseFile(type, path, raw, schema = DEFAULT_SCHEMA) {
|
|
|
5238
5239
|
return { ...parseFeature(path, raw), diagnostics: [] };
|
|
5239
5240
|
}
|
|
5240
5241
|
if (type === "scenario") {
|
|
5241
|
-
return
|
|
5242
|
+
return parseScenarios(path, raw, schema);
|
|
5242
5243
|
}
|
|
5243
5244
|
if (type === "entity") {
|
|
5244
5245
|
return { ...parseEntityRegistry(path, raw), diagnostics: [] };
|
|
@@ -5250,7 +5251,7 @@ function parseFile(type, path, raw, schema = DEFAULT_SCHEMA) {
|
|
|
5250
5251
|
return { ...parseTest(path, raw, schema), diagnostics: [] };
|
|
5251
5252
|
}
|
|
5252
5253
|
if (type === "design") {
|
|
5253
|
-
return
|
|
5254
|
+
return parseDesign(path, raw, schema);
|
|
5254
5255
|
}
|
|
5255
5256
|
if (type === "e2e_test") {
|
|
5256
5257
|
return { ...parseE2eTest(path, raw), diagnostics: [] };
|
|
@@ -5452,13 +5453,14 @@ function parseScenarios(path, raw, schema = DEFAULT_SCHEMA) {
|
|
|
5452
5453
|
});
|
|
5453
5454
|
const nodes = [];
|
|
5454
5455
|
const edges = [];
|
|
5456
|
+
const diagnostics = [];
|
|
5455
5457
|
for (let i = 0; i < starts.length; i += 1) {
|
|
5456
5458
|
const start = starts[i];
|
|
5457
5459
|
const end = starts[i + 1]?.index ?? lines.length;
|
|
5458
5460
|
const block = lines.slice(start.index, end);
|
|
5459
|
-
const featureRefs = markdownRelationOccurrences(path, start, block, "\u5173\u8054\u529F\u80FD", "feature");
|
|
5460
|
-
const decisionRefs = markdownRelationOccurrences(path, start, block, "\u5173\u8054\u51B3\u7B56", "decision");
|
|
5461
|
-
const entityRefs = markdownRelationOccurrences(path, start, block, "\u5173\u8054\u5B9E\u4F53", "entity");
|
|
5461
|
+
const featureRefs = markdownRelationOccurrences(path, start, block, "\u5173\u8054\u529F\u80FD", "feature", schema);
|
|
5462
|
+
const decisionRefs = markdownRelationOccurrences(path, start, block, "\u5173\u8054\u51B3\u7B56", "decision", schema);
|
|
5463
|
+
const entityRefs = markdownRelationOccurrences(path, start, block, "\u5173\u8054\u5B9E\u4F53", "entity", schema);
|
|
5462
5464
|
nodes.push({
|
|
5463
5465
|
type: "scenario",
|
|
5464
5466
|
code: start.code,
|
|
@@ -5476,8 +5478,19 @@ function parseScenarios(path, raw, schema = DEFAULT_SCHEMA) {
|
|
|
5476
5478
|
edges.push(...featureRefs.filter(isValidRelationOccurrence).map((ref) => edge(toUid("scenario", start.code), toUid("feature", ref.target), "references", "markdown", path, ref.line)));
|
|
5477
5479
|
edges.push(...decisionRefs.filter(isValidRelationOccurrence).map((ref) => edge(toUid("scenario", start.code), toUid("decision", ref.target), "references", "markdown", path, ref.line)));
|
|
5478
5480
|
edges.push(...entityRefs.filter(isValidRelationOccurrence).map((ref) => edge(toUid("scenario", start.code), toUid("entity", ref.target), "references", "markdown", path, ref.line)));
|
|
5481
|
+
for (const ref of [...featureRefs, ...decisionRefs, ...entityRefs]) {
|
|
5482
|
+
if (ref.valid === false) {
|
|
5483
|
+
diagnostics.push(issue(
|
|
5484
|
+
"FORMAT_ERROR",
|
|
5485
|
+
`scenario:${start.code} has invalid ${ref.targetType} reference "${ref.target}" in field ${ref.field} (expected ${schema.idPatterns[ref.targetType] ?? DEFAULT_SCHEMA.idPatterns[ref.targetType]})`,
|
|
5486
|
+
ref.path,
|
|
5487
|
+
ref.line,
|
|
5488
|
+
{ node: toUid("scenario", start.code) }
|
|
5489
|
+
));
|
|
5490
|
+
}
|
|
5491
|
+
}
|
|
5479
5492
|
}
|
|
5480
|
-
return { nodes, edges };
|
|
5493
|
+
return { nodes, edges, diagnostics };
|
|
5481
5494
|
}
|
|
5482
5495
|
function parseEntityRegistry(path, raw) {
|
|
5483
5496
|
const nodes = [];
|
|
@@ -5789,7 +5802,7 @@ function containsTraceabilityTag(value, schema = DEFAULT_SCHEMA) {
|
|
|
5789
5802
|
const alternatives = [...tokens].sort((a, b) => b.length - a.length).map((token) => token.replace(/[-\/\\^$*+?.()|[\]{}]/g, "\\$&")).join("|");
|
|
5790
5803
|
return new RegExp(`(?:^|\\s)@(?:${alternatives})(?=\\s|$)`).test(value);
|
|
5791
5804
|
}
|
|
5792
|
-
function parseDesign(path, raw) {
|
|
5805
|
+
function parseDesign(path, raw, schema = DEFAULT_SCHEMA) {
|
|
5793
5806
|
const parsed = matter(raw);
|
|
5794
5807
|
const data = parsed.data;
|
|
5795
5808
|
const code = normalizeDesignCode(path);
|
|
@@ -5799,15 +5812,45 @@ function parseDesign(path, raw) {
|
|
|
5799
5812
|
...designFrontmatterEdges(path, code, data.related_features, "feature", "references"),
|
|
5800
5813
|
...designFrontmatterEdges(path, code, data.related_scenarios, "scenario", "references")
|
|
5801
5814
|
];
|
|
5802
|
-
|
|
5815
|
+
const diagnostics = [];
|
|
5816
|
+
const decisionPattern = relationIdPattern("decision", schema);
|
|
5817
|
+
const frontmatterDecisions = /* @__PURE__ */ new Set();
|
|
5818
|
+
for (const value of toArray(data.related_decisions)) {
|
|
5819
|
+
const candidate = String(value).trim();
|
|
5820
|
+
if (!candidate || EMPTY_RELATION_FIELD_VALUES.has(candidate.toLowerCase())) {
|
|
5821
|
+
continue;
|
|
5822
|
+
}
|
|
5823
|
+
if (!decisionPattern.test(candidate)) {
|
|
5824
|
+
diagnostics.push(issue(
|
|
5825
|
+
"FORMAT_ERROR",
|
|
5826
|
+
`design:${code} has invalid decision reference "${candidate}" in related_decisions (expected ${schema.idPatterns.decision ?? DEFAULT_SCHEMA.idPatterns.decision})`,
|
|
5827
|
+
path,
|
|
5828
|
+
1,
|
|
5829
|
+
{ node: toUid("design", code) }
|
|
5830
|
+
));
|
|
5831
|
+
continue;
|
|
5832
|
+
}
|
|
5833
|
+
if (frontmatterDecisions.has(candidate)) {
|
|
5834
|
+
continue;
|
|
5835
|
+
}
|
|
5836
|
+
frontmatterDecisions.add(candidate);
|
|
5837
|
+
edges.push(edge(toUid("design", code), toUid("decision", candidate), "references", "frontmatter", path, 1));
|
|
5838
|
+
}
|
|
5839
|
+
const hasFrontmatter = /^---\r?\n/.test(raw);
|
|
5840
|
+
const contentStart = hasFrontmatter && parsed.content ? Math.max(raw.indexOf(parsed.content), 0) : 0;
|
|
5841
|
+
const lineOffset = contentStart > 0 ? raw.slice(0, contentStart).split(/\r?\n/).length - 1 : 0;
|
|
5842
|
+
parsed.content.split(/\r?\n/).forEach((line, index) => {
|
|
5803
5843
|
for (const codeValue of extractCodes(line, "entity")) {
|
|
5804
|
-
edges.push(edge(toUid("design", code), toUid("entity", codeValue), "references", "markdown", path, index + 1));
|
|
5844
|
+
edges.push(edge(toUid("design", code), toUid("entity", codeValue), "references", "markdown", path, lineOffset + index + 1));
|
|
5805
5845
|
}
|
|
5806
5846
|
for (const codeValue of extractCodes(line, "decision")) {
|
|
5807
|
-
|
|
5847
|
+
if (frontmatterDecisions.has(codeValue)) {
|
|
5848
|
+
continue;
|
|
5849
|
+
}
|
|
5850
|
+
edges.push(edge(toUid("design", code), toUid("decision", codeValue), "references", "markdown", path, lineOffset + index + 1));
|
|
5808
5851
|
}
|
|
5809
5852
|
});
|
|
5810
|
-
return { nodes, edges };
|
|
5853
|
+
return { nodes, edges, diagnostics };
|
|
5811
5854
|
}
|
|
5812
5855
|
function parseE2eTest(path, raw) {
|
|
5813
5856
|
const parsed = matter(raw);
|
|
@@ -6652,20 +6695,25 @@ function decisionFrontmatterEdges(path, decisionCode, value, targetType, kind) {
|
|
|
6652
6695
|
return [edge(toUid("decision", decisionCode), toUid(targetType, code), kind, "frontmatter", path, 1)];
|
|
6653
6696
|
});
|
|
6654
6697
|
}
|
|
6655
|
-
function
|
|
6698
|
+
function matchExplicitRelationFieldLine(line, label) {
|
|
6699
|
+
const escaped = escapeRegExp(label);
|
|
6700
|
+
const match = new RegExp(`^\\s*(?:\\*\\*${escaped}\\*\\*|${escaped})[:\uFF1A]\\s*(.*?)\\s*$`).exec(line);
|
|
6701
|
+
return match?.[1] ?? null;
|
|
6702
|
+
}
|
|
6703
|
+
function markdownRelationOccurrences(path, scenario, block, label, targetType, schema = DEFAULT_SCHEMA) {
|
|
6656
6704
|
const result = [];
|
|
6657
6705
|
block.forEach((line, index) => {
|
|
6658
|
-
|
|
6706
|
+
const relationText = matchExplicitRelationFieldLine(line, label);
|
|
6707
|
+
if (!relationText) {
|
|
6659
6708
|
return;
|
|
6660
6709
|
}
|
|
6661
|
-
const relationText = line.split(/[::]/).slice(1).join(":").split(/\s+[—-]\s+/)[0] ?? line;
|
|
6662
6710
|
const lineNumber = scenario.index + index + 1;
|
|
6663
|
-
const
|
|
6664
|
-
for (const code of
|
|
6665
|
-
result.push({ field: label, targetType, target: code, path, line: lineNumber, raw: relationText
|
|
6711
|
+
const { valid, invalid } = parseExplicitRelationField(relationText, targetType, schema);
|
|
6712
|
+
for (const code of valid) {
|
|
6713
|
+
result.push({ field: label, targetType, target: code, path, line: lineNumber, raw: relationText, valid: true });
|
|
6666
6714
|
}
|
|
6667
|
-
for (const
|
|
6668
|
-
result.push({ field: label, targetType, target:
|
|
6715
|
+
for (const code of invalid) {
|
|
6716
|
+
result.push({ field: label, targetType, target: code, path, line: lineNumber, raw: relationText, valid: false });
|
|
6669
6717
|
}
|
|
6670
6718
|
});
|
|
6671
6719
|
return result;
|
|
@@ -6688,7 +6736,8 @@ function relationOccurrences(node, field, targetType) {
|
|
|
6688
6736
|
target,
|
|
6689
6737
|
path: typeof record.path === "string" ? record.path : node.path,
|
|
6690
6738
|
line: typeof record.line === "number" ? record.line : node.line,
|
|
6691
|
-
raw: typeof record.raw === "string" ? record.raw : void 0
|
|
6739
|
+
raw: typeof record.raw === "string" ? record.raw : void 0,
|
|
6740
|
+
valid: typeof record.valid === "boolean" ? record.valid : void 0
|
|
6692
6741
|
}];
|
|
6693
6742
|
});
|
|
6694
6743
|
}
|
|
@@ -6703,28 +6752,40 @@ function pushDuplicateIssues(issues, nodeUid, refs, targetType) {
|
|
|
6703
6752
|
}
|
|
6704
6753
|
}
|
|
6705
6754
|
function isValidRelationOccurrence(ref) {
|
|
6755
|
+
if (ref.valid !== void 0) {
|
|
6756
|
+
return ref.valid;
|
|
6757
|
+
}
|
|
6706
6758
|
return relationTargetPattern(ref.targetType).test(ref.target);
|
|
6707
6759
|
}
|
|
6708
|
-
function
|
|
6709
|
-
|
|
6710
|
-
const candidates = text.split(/[\s,,、;;()[\]()]+/).map((token) => token.trim().replace(/^["'`]+|["'`.::]+$/g, "")).filter(Boolean);
|
|
6711
|
-
const pattern = relationTargetPattern(targetType);
|
|
6712
|
-
return [...new Set(candidates.filter((candidate) => looksLikeRelationId(candidate, targetType) && !valid.has(candidate) && !pattern.test(candidate)))];
|
|
6760
|
+
function relationIdPattern(targetType, schema) {
|
|
6761
|
+
return new RegExp(schema.idPatterns[targetType] ?? DEFAULT_SCHEMA.idPatterns[targetType] ?? "^.+$");
|
|
6713
6762
|
}
|
|
6714
|
-
function
|
|
6715
|
-
|
|
6716
|
-
|
|
6717
|
-
|
|
6718
|
-
|
|
6719
|
-
|
|
6720
|
-
|
|
6721
|
-
if (targetType === "decision") {
|
|
6722
|
-
return /^D[-A-Z0-9]+$/.test(candidate);
|
|
6763
|
+
function stripRelationItemAnnotation(item) {
|
|
6764
|
+
let result = item.trim();
|
|
6765
|
+
let previous = "";
|
|
6766
|
+
while (result !== previous) {
|
|
6767
|
+
previous = result;
|
|
6768
|
+
result = result.replace(/\s*[((][^()()]*[))]\s*$/, "").trim();
|
|
6769
|
+
result = result.replace(/\s+[—–-]\s+.*$/, "").trim();
|
|
6723
6770
|
}
|
|
6724
|
-
|
|
6725
|
-
|
|
6771
|
+
return result;
|
|
6772
|
+
}
|
|
6773
|
+
function parseExplicitRelationField(text, targetType, schema) {
|
|
6774
|
+
const pattern = relationIdPattern(targetType, schema);
|
|
6775
|
+
const valid = [];
|
|
6776
|
+
const invalid = [];
|
|
6777
|
+
for (const item of text.split(/[,,、;;]/)) {
|
|
6778
|
+
const candidate = stripRelationItemAnnotation(item);
|
|
6779
|
+
if (!candidate || EMPTY_RELATION_FIELD_VALUES.has(candidate.toLowerCase())) {
|
|
6780
|
+
continue;
|
|
6781
|
+
}
|
|
6782
|
+
if (pattern.test(candidate)) {
|
|
6783
|
+
valid.push(candidate);
|
|
6784
|
+
} else if (!invalid.includes(candidate)) {
|
|
6785
|
+
invalid.push(candidate);
|
|
6786
|
+
}
|
|
6726
6787
|
}
|
|
6727
|
-
return
|
|
6788
|
+
return { valid, invalid };
|
|
6728
6789
|
}
|
|
6729
6790
|
function relationTargetPattern(targetType) {
|
|
6730
6791
|
const patterns = {
|
|
@@ -7847,21 +7908,19 @@ function globToRegExp(pattern) {
|
|
|
7847
7908
|
}
|
|
7848
7909
|
return new RegExp(`${source}$`);
|
|
7849
7910
|
}
|
|
7850
|
-
function
|
|
7911
|
+
function resolveQueryStartUid(graph, value) {
|
|
7851
7912
|
if (value.includes(":")) {
|
|
7852
7913
|
return value;
|
|
7853
7914
|
}
|
|
7854
|
-
|
|
7855
|
-
|
|
7915
|
+
const matches = graph.nodes.filter((node) => node.code === value);
|
|
7916
|
+
if (matches.length === 1) {
|
|
7917
|
+
return matches[0].uid;
|
|
7856
7918
|
}
|
|
7857
|
-
if (
|
|
7858
|
-
|
|
7859
|
-
|
|
7860
|
-
|
|
7861
|
-
|
|
7862
|
-
}
|
|
7863
|
-
if (/^[A-Z]{1,4}\d+$/.test(value)) {
|
|
7864
|
-
return toUid("feature", value);
|
|
7919
|
+
if (matches.length > 1) {
|
|
7920
|
+
const candidates = matches.map((node) => node.uid).sort();
|
|
7921
|
+
throw new Error(
|
|
7922
|
+
`Ambiguous artifact id "${value}": matches ${candidates.join(", ")}. Use a fully qualified uid (type:id), for example ${candidates[0]}`
|
|
7923
|
+
);
|
|
7865
7924
|
}
|
|
7866
7925
|
return value;
|
|
7867
7926
|
}
|
|
@@ -8365,7 +8424,7 @@ function formatContextMarkdown(manifest) {
|
|
|
8365
8424
|
}
|
|
8366
8425
|
return lines.join("\n");
|
|
8367
8426
|
}
|
|
8368
|
-
var TARGET_ARTIFACT_TYPES, NON_TARGET_ROLES, DEFAULT_SCHEMA, VALID_TC_STATUSES, VALID_CHAIN_TYPES, DEPRECATED_CHAIN_TYPE_ALIASES, CONTEXT_CATEGORIES, TIER_ORDER;
|
|
8427
|
+
var TARGET_ARTIFACT_TYPES, NON_TARGET_ROLES, DEFAULT_SCHEMA, EMPTY_RELATION_FIELD_VALUES, VALID_TC_STATUSES, VALID_CHAIN_TYPES, DEPRECATED_CHAIN_TYPE_ALIASES, CONTEXT_CATEGORIES, TIER_ORDER;
|
|
8369
8428
|
var init_index = __esm({
|
|
8370
8429
|
"src/index.ts"() {
|
|
8371
8430
|
init_packet_constants();
|
|
@@ -8418,7 +8477,7 @@ var init_index = __esm({
|
|
|
8418
8477
|
relationFields: {
|
|
8419
8478
|
feature: ["scenarios", "decisions", "depends_on", "design_docs"],
|
|
8420
8479
|
scenario: ["\u5173\u8054\u529F\u80FD", "\u5173\u8054\u51B3\u7B56"],
|
|
8421
|
-
design: ["related_features", "related_scenarios"],
|
|
8480
|
+
design: ["related_features", "related_scenarios", "related_decisions"],
|
|
8422
8481
|
test: ["@scenario", "@feature", "@entity", "@decision"],
|
|
8423
8482
|
e2e_test: ["test_batch", "scope", "ac_coverage", "related_scenarios", "related_decisions", "related_entities", "\u8986\u76D6\u573A\u666F", "\u8986\u76D6\u529F\u80FD"],
|
|
8424
8483
|
e2e_registry: ["batches"]
|
|
@@ -8433,6 +8492,7 @@ var init_index = __esm({
|
|
|
8433
8492
|
runners: []
|
|
8434
8493
|
}
|
|
8435
8494
|
};
|
|
8495
|
+
EMPTY_RELATION_FIELD_VALUES = /* @__PURE__ */ new Set(["\u65E0", "none", "n/a", "-"]);
|
|
8436
8496
|
VALID_TC_STATUSES = /* @__PURE__ */ new Set(["created", "automated", "verified", "waived"]);
|
|
8437
8497
|
VALID_CHAIN_TYPES = /* @__PURE__ */ new Set([
|
|
8438
8498
|
"desktop_chain",
|