artifact-graph 0.9.1 → 0.9.3

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 CHANGED
@@ -1,5 +1,38 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.9.3
4
+
5
+ ### Fixed
6
+
7
+ - Scenario titles and explicit relation fields inside Markdown fenced code blocks
8
+ (```` ``` ```` or `~~~`) are no longer scanned: examples such as
9
+ `**关联决策**: ADR-0001` shown inside a fence produce neither scenario nodes nor graph
10
+ edges. A fence closes only on the same marker with a length at least as long as the
11
+ opening fence.
12
+ - Explicit relation fields indented by four or more spaces or a tab are now treated as
13
+ indented code and no longer produce edges. Outside code blocks, a real relation field
14
+ may be indented by at most 0—3 plain spaces. Behavior tightening: adopters who relied
15
+ on deeply indented relation fields must move them to line start (0—3 spaces).
16
+ - The public `validateScenarioPrdLinks` function once again returns the complete
17
+ diagnostics (including `FORMAT_ERROR` for invalid feature references) when called
18
+ directly on scan results, restoring the public API behavior regressed in 0.9.2.
19
+ - `validateGraph` still uses scan diagnostics as the single source for scanned invalid
20
+ relations, so the same invalid feature reference is reported exactly once.
21
+
22
+ ## 0.9.2
23
+
24
+ ### Fixed
25
+
26
+ - Scenario explicit relation field recognition is tightened to whole-line field syntax matching:
27
+ plain prose mentions of `关联功能` / `关联决策` / `关联实体` no longer produce edges or
28
+ spurious `FORMAT_ERROR` diagnostics.
29
+ - An invalid explicit relation reference is no longer reported twice across the scan and
30
+ validation stages: `validateGraph` now returns exactly one `FORMAT_ERROR` carrying the file,
31
+ line number, node, invalid value, and the expected rule.
32
+ - Behavior tightening: Markdown list-item forms such as `- **关联决策**: ADR-0001` are no longer
33
+ recognized as explicit relation fields (0.9.1 misidentified them via substring matching);
34
+ standard line-start field syntax is unaffected.
35
+
3
36
  ## 0.9.1
4
37
 
5
38
  ### Fixed
package/dist/cli.js CHANGED
@@ -4936,7 +4936,7 @@ function validateGraph(graph, schema = DEFAULT_SCHEMA) {
4936
4936
  }
4937
4937
  issues.push(...validateE2eTests(graph));
4938
4938
  issues.push(...validateE2eRegistry(graph));
4939
- issues.push(...validateScenarioPrdLinks(graph, schema));
4939
+ issues.push(...validateScenarioPrdLinksInternal(graph, schema, false));
4940
4940
  issues.push(...validateCodeCommentTraceabilityFormat(graph));
4941
4941
  issues.push(...validateCodeCommentScenarioFeatureConsistency(graph));
4942
4942
  for (const edge2 of graph.edges) {
@@ -5038,7 +5038,7 @@ function validateGraph(graph, schema = DEFAULT_SCHEMA) {
5038
5038
  issues.sort((left, right) => left.code.localeCompare(right.code) || left.path.localeCompare(right.path) || left.line - right.line);
5039
5039
  return issues;
5040
5040
  }
5041
- function validateScenarioPrdLinks(graph, schema = DEFAULT_SCHEMA) {
5041
+ function validateScenarioPrdLinksInternal(graph, schema, includeScanDiagnosedInvalid) {
5042
5042
  if (!schema.relationFields.scenario?.includes("\u5173\u8054\u529F\u80FD") || !schema.relationFields.feature?.includes("scenarios")) {
5043
5043
  return [];
5044
5044
  }
@@ -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 && !includeScanDiagnosedInvalid) 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
  }
@@ -5575,8 +5576,12 @@ function parseFeature(path, raw) {
5575
5576
  }
5576
5577
  function parseScenarios(path, raw, schema = DEFAULT_SCHEMA) {
5577
5578
  const lines = raw.split(/\r?\n/);
5579
+ const codeLines = markdownCodeLineMask(lines);
5578
5580
  const starts = [];
5579
5581
  lines.forEach((line, index) => {
5582
+ if (codeLines[index]) {
5583
+ return;
5584
+ }
5580
5585
  const match = /^#{2,3}\s+(S-\d+[a-z]?)\s*[::]\s*(.+?)\s*$/.exec(line);
5581
5586
  if (match) {
5582
5587
  starts.push({ code: match[1], title: match[2], line: index + 1, index });
@@ -5589,9 +5594,9 @@ function parseScenarios(path, raw, schema = DEFAULT_SCHEMA) {
5589
5594
  const start = starts[i];
5590
5595
  const end = starts[i + 1]?.index ?? lines.length;
5591
5596
  const block = lines.slice(start.index, end);
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);
5597
+ const featureRefs = markdownRelationOccurrences(path, start, block, codeLines, "\u5173\u8054\u529F\u80FD", "feature", schema);
5598
+ const decisionRefs = markdownRelationOccurrences(path, start, block, codeLines, "\u5173\u8054\u51B3\u7B56", "decision", schema);
5599
+ const entityRefs = markdownRelationOccurrences(path, start, block, codeLines, "\u5173\u8054\u5B9E\u4F53", "entity", schema);
5595
5600
  nodes.push({
5596
5601
  type: "scenario",
5597
5602
  code: start.code,
@@ -6826,13 +6831,42 @@ function decisionFrontmatterEdges(path, decisionCode, value, targetType, kind) {
6826
6831
  return [edge(toUid("decision", decisionCode), toUid(targetType, code), kind, "frontmatter", path, 1)];
6827
6832
  });
6828
6833
  }
6829
- function markdownRelationOccurrences(path, scenario, block, label, targetType, schema = DEFAULT_SCHEMA) {
6834
+ function matchExplicitRelationFieldLine(line, label) {
6835
+ const escaped = escapeRegExp(label);
6836
+ const match = new RegExp(`^ {0,3}(?:\\*\\*${escaped}\\*\\*|${escaped})[:\uFF1A]\\s*(.*?)\\s*$`).exec(line);
6837
+ return match?.[1] ?? null;
6838
+ }
6839
+ function markdownCodeLineMask(lines) {
6840
+ const result = new Array(lines.length).fill(false);
6841
+ let fence;
6842
+ lines.forEach((line, index) => {
6843
+ if (fence) {
6844
+ result[index] = true;
6845
+ const closing = /^ {0,3}(`+|~+)[ \t]*$/.exec(line);
6846
+ if (closing && closing[1][0] === fence.marker && closing[1].length >= fence.length) {
6847
+ fence = void 0;
6848
+ }
6849
+ return;
6850
+ }
6851
+ const opening = /^ {0,3}(`{3,}|~{3,})(.*)$/.exec(line);
6852
+ if (opening) {
6853
+ result[index] = true;
6854
+ fence = { marker: opening[1][0], length: opening[1].length };
6855
+ return;
6856
+ }
6857
+ if (/^(?: {4,}| {0,3}\t)/.test(line)) {
6858
+ result[index] = true;
6859
+ }
6860
+ });
6861
+ return result;
6862
+ }
6863
+ function markdownRelationOccurrences(path, scenario, block, codeLines, label, targetType, schema = DEFAULT_SCHEMA) {
6830
6864
  const result = [];
6831
6865
  block.forEach((line, index) => {
6832
- if (!line.includes(label)) {
6866
+ if (codeLines[scenario.index + index]) {
6833
6867
  return;
6834
6868
  }
6835
- const relationText = line.split(/[::]/).slice(1).join(":").trim();
6869
+ const relationText = matchExplicitRelationFieldLine(line, label);
6836
6870
  if (!relationText) {
6837
6871
  return;
6838
6872
  }
@@ -6865,7 +6899,8 @@ function relationOccurrences(node, field, targetType) {
6865
6899
  target,
6866
6900
  path: typeof record.path === "string" ? record.path : node.path,
6867
6901
  line: typeof record.line === "number" ? record.line : node.line,
6868
- raw: typeof record.raw === "string" ? record.raw : void 0
6902
+ raw: typeof record.raw === "string" ? record.raw : void 0,
6903
+ valid: typeof record.valid === "boolean" ? record.valid : void 0
6869
6904
  }];
6870
6905
  });
6871
6906
  }
package/dist/index.cjs CHANGED
@@ -4914,7 +4914,7 @@ function validateGraph(graph, schema = DEFAULT_SCHEMA) {
4914
4914
  }
4915
4915
  issues.push(...validateE2eTests(graph));
4916
4916
  issues.push(...validateE2eRegistry(graph));
4917
- issues.push(...validateScenarioPrdLinks(graph, schema));
4917
+ issues.push(...validateScenarioPrdLinksInternal(graph, schema, false));
4918
4918
  issues.push(...validateCodeCommentTraceabilityFormat(graph));
4919
4919
  issues.push(...validateCodeCommentScenarioFeatureConsistency(graph));
4920
4920
  for (const edge2 of graph.edges) {
@@ -5017,6 +5017,9 @@ function validateGraph(graph, schema = DEFAULT_SCHEMA) {
5017
5017
  return issues;
5018
5018
  }
5019
5019
  function validateScenarioPrdLinks(graph, schema = DEFAULT_SCHEMA) {
5020
+ return validateScenarioPrdLinksInternal(graph, schema, true);
5021
+ }
5022
+ function validateScenarioPrdLinksInternal(graph, schema, includeScanDiagnosedInvalid) {
5020
5023
  if (!schema.relationFields.scenario?.includes("\u5173\u8054\u529F\u80FD") || !schema.relationFields.feature?.includes("scenarios")) {
5021
5024
  return [];
5022
5025
  }
@@ -5050,6 +5053,7 @@ function validateScenarioPrdLinks(graph, schema = DEFAULT_SCHEMA) {
5050
5053
  }
5051
5054
  pushDuplicateIssues(issues, scenarioUid, validRefs, "feature");
5052
5055
  for (const ref of refs) {
5056
+ if (ref.valid === false && !includeScanDiagnosedInvalid) continue;
5053
5057
  if (!featurePattern.test(ref.target)) {
5054
5058
  issues.push(issue("FORMAT_ERROR", `scenario ${scenarioUid} has invalid feature reference ${ref.target}`, ref.path, ref.line, { node: scenarioUid }));
5055
5059
  }
@@ -5553,8 +5557,12 @@ function parseFeature(path, raw) {
5553
5557
  }
5554
5558
  function parseScenarios(path, raw, schema = DEFAULT_SCHEMA) {
5555
5559
  const lines = raw.split(/\r?\n/);
5560
+ const codeLines = markdownCodeLineMask(lines);
5556
5561
  const starts = [];
5557
5562
  lines.forEach((line, index) => {
5563
+ if (codeLines[index]) {
5564
+ return;
5565
+ }
5558
5566
  const match = /^#{2,3}\s+(S-\d+[a-z]?)\s*[::]\s*(.+?)\s*$/.exec(line);
5559
5567
  if (match) {
5560
5568
  starts.push({ code: match[1], title: match[2], line: index + 1, index });
@@ -5567,9 +5575,9 @@ function parseScenarios(path, raw, schema = DEFAULT_SCHEMA) {
5567
5575
  const start = starts[i];
5568
5576
  const end = starts[i + 1]?.index ?? lines.length;
5569
5577
  const block = lines.slice(start.index, end);
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);
5578
+ const featureRefs = markdownRelationOccurrences(path, start, block, codeLines, "\u5173\u8054\u529F\u80FD", "feature", schema);
5579
+ const decisionRefs = markdownRelationOccurrences(path, start, block, codeLines, "\u5173\u8054\u51B3\u7B56", "decision", schema);
5580
+ const entityRefs = markdownRelationOccurrences(path, start, block, codeLines, "\u5173\u8054\u5B9E\u4F53", "entity", schema);
5573
5581
  nodes.push({
5574
5582
  type: "scenario",
5575
5583
  code: start.code,
@@ -6804,13 +6812,42 @@ function decisionFrontmatterEdges(path, decisionCode, value, targetType, kind) {
6804
6812
  return [edge(toUid("decision", decisionCode), toUid(targetType, code), kind, "frontmatter", path, 1)];
6805
6813
  });
6806
6814
  }
6807
- function markdownRelationOccurrences(path, scenario, block, label, targetType, schema = DEFAULT_SCHEMA) {
6815
+ function matchExplicitRelationFieldLine(line, label) {
6816
+ const escaped = escapeRegExp(label);
6817
+ const match = new RegExp(`^ {0,3}(?:\\*\\*${escaped}\\*\\*|${escaped})[:\uFF1A]\\s*(.*?)\\s*$`).exec(line);
6818
+ return match?.[1] ?? null;
6819
+ }
6820
+ function markdownCodeLineMask(lines) {
6821
+ const result = new Array(lines.length).fill(false);
6822
+ let fence;
6823
+ lines.forEach((line, index) => {
6824
+ if (fence) {
6825
+ result[index] = true;
6826
+ const closing = /^ {0,3}(`+|~+)[ \t]*$/.exec(line);
6827
+ if (closing && closing[1][0] === fence.marker && closing[1].length >= fence.length) {
6828
+ fence = void 0;
6829
+ }
6830
+ return;
6831
+ }
6832
+ const opening = /^ {0,3}(`{3,}|~{3,})(.*)$/.exec(line);
6833
+ if (opening) {
6834
+ result[index] = true;
6835
+ fence = { marker: opening[1][0], length: opening[1].length };
6836
+ return;
6837
+ }
6838
+ if (/^(?: {4,}| {0,3}\t)/.test(line)) {
6839
+ result[index] = true;
6840
+ }
6841
+ });
6842
+ return result;
6843
+ }
6844
+ function markdownRelationOccurrences(path, scenario, block, codeLines, label, targetType, schema = DEFAULT_SCHEMA) {
6808
6845
  const result = [];
6809
6846
  block.forEach((line, index) => {
6810
- if (!line.includes(label)) {
6847
+ if (codeLines[scenario.index + index]) {
6811
6848
  return;
6812
6849
  }
6813
- const relationText = line.split(/[::]/).slice(1).join(":").trim();
6850
+ const relationText = matchExplicitRelationFieldLine(line, label);
6814
6851
  if (!relationText) {
6815
6852
  return;
6816
6853
  }
@@ -6843,7 +6880,8 @@ function relationOccurrences(node, field, targetType) {
6843
6880
  target,
6844
6881
  path: typeof record.path === "string" ? record.path : node.path,
6845
6882
  line: typeof record.line === "number" ? record.line : node.line,
6846
- raw: typeof record.raw === "string" ? record.raw : void 0
6883
+ raw: typeof record.raw === "string" ? record.raw : void 0,
6884
+ valid: typeof record.valid === "boolean" ? record.valid : void 0
6847
6885
  }];
6848
6886
  });
6849
6887
  }
package/dist/index.js CHANGED
@@ -4804,7 +4804,7 @@ function validateGraph(graph, schema = DEFAULT_SCHEMA) {
4804
4804
  }
4805
4805
  issues.push(...validateE2eTests(graph));
4806
4806
  issues.push(...validateE2eRegistry(graph));
4807
- issues.push(...validateScenarioPrdLinks(graph, schema));
4807
+ issues.push(...validateScenarioPrdLinksInternal(graph, schema, false));
4808
4808
  issues.push(...validateCodeCommentTraceabilityFormat(graph));
4809
4809
  issues.push(...validateCodeCommentScenarioFeatureConsistency(graph));
4810
4810
  for (const edge2 of graph.edges) {
@@ -4907,6 +4907,9 @@ function validateGraph(graph, schema = DEFAULT_SCHEMA) {
4907
4907
  return issues;
4908
4908
  }
4909
4909
  function validateScenarioPrdLinks(graph, schema = DEFAULT_SCHEMA) {
4910
+ return validateScenarioPrdLinksInternal(graph, schema, true);
4911
+ }
4912
+ function validateScenarioPrdLinksInternal(graph, schema, includeScanDiagnosedInvalid) {
4910
4913
  if (!schema.relationFields.scenario?.includes("\u5173\u8054\u529F\u80FD") || !schema.relationFields.feature?.includes("scenarios")) {
4911
4914
  return [];
4912
4915
  }
@@ -4940,6 +4943,7 @@ function validateScenarioPrdLinks(graph, schema = DEFAULT_SCHEMA) {
4940
4943
  }
4941
4944
  pushDuplicateIssues(issues, scenarioUid, validRefs, "feature");
4942
4945
  for (const ref of refs) {
4946
+ if (ref.valid === false && !includeScanDiagnosedInvalid) continue;
4943
4947
  if (!featurePattern.test(ref.target)) {
4944
4948
  issues.push(issue("FORMAT_ERROR", `scenario ${scenarioUid} has invalid feature reference ${ref.target}`, ref.path, ref.line, { node: scenarioUid }));
4945
4949
  }
@@ -5443,8 +5447,12 @@ function parseFeature(path, raw) {
5443
5447
  }
5444
5448
  function parseScenarios(path, raw, schema = DEFAULT_SCHEMA) {
5445
5449
  const lines = raw.split(/\r?\n/);
5450
+ const codeLines = markdownCodeLineMask(lines);
5446
5451
  const starts = [];
5447
5452
  lines.forEach((line, index) => {
5453
+ if (codeLines[index]) {
5454
+ return;
5455
+ }
5448
5456
  const match = /^#{2,3}\s+(S-\d+[a-z]?)\s*[::]\s*(.+?)\s*$/.exec(line);
5449
5457
  if (match) {
5450
5458
  starts.push({ code: match[1], title: match[2], line: index + 1, index });
@@ -5457,9 +5465,9 @@ function parseScenarios(path, raw, schema = DEFAULT_SCHEMA) {
5457
5465
  const start = starts[i];
5458
5466
  const end = starts[i + 1]?.index ?? lines.length;
5459
5467
  const block = lines.slice(start.index, end);
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);
5468
+ const featureRefs = markdownRelationOccurrences(path, start, block, codeLines, "\u5173\u8054\u529F\u80FD", "feature", schema);
5469
+ const decisionRefs = markdownRelationOccurrences(path, start, block, codeLines, "\u5173\u8054\u51B3\u7B56", "decision", schema);
5470
+ const entityRefs = markdownRelationOccurrences(path, start, block, codeLines, "\u5173\u8054\u5B9E\u4F53", "entity", schema);
5463
5471
  nodes.push({
5464
5472
  type: "scenario",
5465
5473
  code: start.code,
@@ -6694,13 +6702,42 @@ function decisionFrontmatterEdges(path, decisionCode, value, targetType, kind) {
6694
6702
  return [edge(toUid("decision", decisionCode), toUid(targetType, code), kind, "frontmatter", path, 1)];
6695
6703
  });
6696
6704
  }
6697
- function markdownRelationOccurrences(path, scenario, block, label, targetType, schema = DEFAULT_SCHEMA) {
6705
+ function matchExplicitRelationFieldLine(line, label) {
6706
+ const escaped = escapeRegExp(label);
6707
+ const match = new RegExp(`^ {0,3}(?:\\*\\*${escaped}\\*\\*|${escaped})[:\uFF1A]\\s*(.*?)\\s*$`).exec(line);
6708
+ return match?.[1] ?? null;
6709
+ }
6710
+ function markdownCodeLineMask(lines) {
6711
+ const result = new Array(lines.length).fill(false);
6712
+ let fence;
6713
+ lines.forEach((line, index) => {
6714
+ if (fence) {
6715
+ result[index] = true;
6716
+ const closing = /^ {0,3}(`+|~+)[ \t]*$/.exec(line);
6717
+ if (closing && closing[1][0] === fence.marker && closing[1].length >= fence.length) {
6718
+ fence = void 0;
6719
+ }
6720
+ return;
6721
+ }
6722
+ const opening = /^ {0,3}(`{3,}|~{3,})(.*)$/.exec(line);
6723
+ if (opening) {
6724
+ result[index] = true;
6725
+ fence = { marker: opening[1][0], length: opening[1].length };
6726
+ return;
6727
+ }
6728
+ if (/^(?: {4,}| {0,3}\t)/.test(line)) {
6729
+ result[index] = true;
6730
+ }
6731
+ });
6732
+ return result;
6733
+ }
6734
+ function markdownRelationOccurrences(path, scenario, block, codeLines, label, targetType, schema = DEFAULT_SCHEMA) {
6698
6735
  const result = [];
6699
6736
  block.forEach((line, index) => {
6700
- if (!line.includes(label)) {
6737
+ if (codeLines[scenario.index + index]) {
6701
6738
  return;
6702
6739
  }
6703
- const relationText = line.split(/[::]/).slice(1).join(":").trim();
6740
+ const relationText = matchExplicitRelationFieldLine(line, label);
6704
6741
  if (!relationText) {
6705
6742
  return;
6706
6743
  }
@@ -6733,7 +6770,8 @@ function relationOccurrences(node, field, targetType) {
6733
6770
  target,
6734
6771
  path: typeof record.path === "string" ? record.path : node.path,
6735
6772
  line: typeof record.line === "number" ? record.line : node.line,
6736
- raw: typeof record.raw === "string" ? record.raw : void 0
6773
+ raw: typeof record.raw === "string" ? record.raw : void 0,
6774
+ valid: typeof record.valid === "boolean" ? record.valid : void 0
6737
6775
  }];
6738
6776
  });
6739
6777
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "artifact-graph",
3
- "version": "0.9.1",
3
+ "version": "0.9.3",
4
4
  "description": "Git-native Markdown artifact graph scanner and validator",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",