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