artifact-graph 0.9.2 → 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,24 @@
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
+
3
22
  ## 0.9.2
4
23
 
5
24
  ### 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,7 +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
+ if (ref.valid === false && !includeScanDiagnosedInvalid) continue;
5076
5076
  if (!featurePattern.test(ref.target)) {
5077
5077
  issues.push(issue("FORMAT_ERROR", `scenario ${scenarioUid} has invalid feature reference ${ref.target}`, ref.path, ref.line, { node: scenarioUid }));
5078
5078
  }
@@ -5576,8 +5576,12 @@ function parseFeature(path, raw) {
5576
5576
  }
5577
5577
  function parseScenarios(path, raw, schema = DEFAULT_SCHEMA) {
5578
5578
  const lines = raw.split(/\r?\n/);
5579
+ const codeLines = markdownCodeLineMask(lines);
5579
5580
  const starts = [];
5580
5581
  lines.forEach((line, index) => {
5582
+ if (codeLines[index]) {
5583
+ return;
5584
+ }
5581
5585
  const match = /^#{2,3}\s+(S-\d+[a-z]?)\s*[::]\s*(.+?)\s*$/.exec(line);
5582
5586
  if (match) {
5583
5587
  starts.push({ code: match[1], title: match[2], line: index + 1, index });
@@ -5590,9 +5594,9 @@ function parseScenarios(path, raw, schema = DEFAULT_SCHEMA) {
5590
5594
  const start = starts[i];
5591
5595
  const end = starts[i + 1]?.index ?? lines.length;
5592
5596
  const block = lines.slice(start.index, end);
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);
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);
5596
5600
  nodes.push({
5597
5601
  type: "scenario",
5598
5602
  code: start.code,
@@ -6829,12 +6833,39 @@ function decisionFrontmatterEdges(path, decisionCode, value, targetType, kind) {
6829
6833
  }
6830
6834
  function matchExplicitRelationFieldLine(line, label) {
6831
6835
  const escaped = escapeRegExp(label);
6832
- const match = new RegExp(`^\\s*(?:\\*\\*${escaped}\\*\\*|${escaped})[:\uFF1A]\\s*(.*?)\\s*$`).exec(line);
6836
+ const match = new RegExp(`^ {0,3}(?:\\*\\*${escaped}\\*\\*|${escaped})[:\uFF1A]\\s*(.*?)\\s*$`).exec(line);
6833
6837
  return match?.[1] ?? null;
6834
6838
  }
6835
- function markdownRelationOccurrences(path, scenario, block, label, targetType, schema = DEFAULT_SCHEMA) {
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) {
6836
6864
  const result = [];
6837
6865
  block.forEach((line, index) => {
6866
+ if (codeLines[scenario.index + index]) {
6867
+ return;
6868
+ }
6838
6869
  const relationText = matchExplicitRelationFieldLine(line, label);
6839
6870
  if (!relationText) {
6840
6871
  return;
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,7 +5053,7 @@ function validateScenarioPrdLinks(graph, schema = DEFAULT_SCHEMA) {
5050
5053
  }
5051
5054
  pushDuplicateIssues(issues, scenarioUid, validRefs, "feature");
5052
5055
  for (const ref of refs) {
5053
- if (ref.valid === false) continue;
5056
+ if (ref.valid === false && !includeScanDiagnosedInvalid) continue;
5054
5057
  if (!featurePattern.test(ref.target)) {
5055
5058
  issues.push(issue("FORMAT_ERROR", `scenario ${scenarioUid} has invalid feature reference ${ref.target}`, ref.path, ref.line, { node: scenarioUid }));
5056
5059
  }
@@ -5554,8 +5557,12 @@ function parseFeature(path, raw) {
5554
5557
  }
5555
5558
  function parseScenarios(path, raw, schema = DEFAULT_SCHEMA) {
5556
5559
  const lines = raw.split(/\r?\n/);
5560
+ const codeLines = markdownCodeLineMask(lines);
5557
5561
  const starts = [];
5558
5562
  lines.forEach((line, index) => {
5563
+ if (codeLines[index]) {
5564
+ return;
5565
+ }
5559
5566
  const match = /^#{2,3}\s+(S-\d+[a-z]?)\s*[::]\s*(.+?)\s*$/.exec(line);
5560
5567
  if (match) {
5561
5568
  starts.push({ code: match[1], title: match[2], line: index + 1, index });
@@ -5568,9 +5575,9 @@ function parseScenarios(path, raw, schema = DEFAULT_SCHEMA) {
5568
5575
  const start = starts[i];
5569
5576
  const end = starts[i + 1]?.index ?? lines.length;
5570
5577
  const block = lines.slice(start.index, end);
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);
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);
5574
5581
  nodes.push({
5575
5582
  type: "scenario",
5576
5583
  code: start.code,
@@ -6807,12 +6814,39 @@ function decisionFrontmatterEdges(path, decisionCode, value, targetType, kind) {
6807
6814
  }
6808
6815
  function matchExplicitRelationFieldLine(line, label) {
6809
6816
  const escaped = escapeRegExp(label);
6810
- const match = new RegExp(`^\\s*(?:\\*\\*${escaped}\\*\\*|${escaped})[:\uFF1A]\\s*(.*?)\\s*$`).exec(line);
6817
+ const match = new RegExp(`^ {0,3}(?:\\*\\*${escaped}\\*\\*|${escaped})[:\uFF1A]\\s*(.*?)\\s*$`).exec(line);
6811
6818
  return match?.[1] ?? null;
6812
6819
  }
6813
- function markdownRelationOccurrences(path, scenario, block, label, targetType, schema = DEFAULT_SCHEMA) {
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) {
6814
6845
  const result = [];
6815
6846
  block.forEach((line, index) => {
6847
+ if (codeLines[scenario.index + index]) {
6848
+ return;
6849
+ }
6816
6850
  const relationText = matchExplicitRelationFieldLine(line, label);
6817
6851
  if (!relationText) {
6818
6852
  return;
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,7 +4943,7 @@ function validateScenarioPrdLinks(graph, schema = DEFAULT_SCHEMA) {
4940
4943
  }
4941
4944
  pushDuplicateIssues(issues, scenarioUid, validRefs, "feature");
4942
4945
  for (const ref of refs) {
4943
- if (ref.valid === false) continue;
4946
+ if (ref.valid === false && !includeScanDiagnosedInvalid) continue;
4944
4947
  if (!featurePattern.test(ref.target)) {
4945
4948
  issues.push(issue("FORMAT_ERROR", `scenario ${scenarioUid} has invalid feature reference ${ref.target}`, ref.path, ref.line, { node: scenarioUid }));
4946
4949
  }
@@ -5444,8 +5447,12 @@ function parseFeature(path, raw) {
5444
5447
  }
5445
5448
  function parseScenarios(path, raw, schema = DEFAULT_SCHEMA) {
5446
5449
  const lines = raw.split(/\r?\n/);
5450
+ const codeLines = markdownCodeLineMask(lines);
5447
5451
  const starts = [];
5448
5452
  lines.forEach((line, index) => {
5453
+ if (codeLines[index]) {
5454
+ return;
5455
+ }
5449
5456
  const match = /^#{2,3}\s+(S-\d+[a-z]?)\s*[::]\s*(.+?)\s*$/.exec(line);
5450
5457
  if (match) {
5451
5458
  starts.push({ code: match[1], title: match[2], line: index + 1, index });
@@ -5458,9 +5465,9 @@ function parseScenarios(path, raw, schema = DEFAULT_SCHEMA) {
5458
5465
  const start = starts[i];
5459
5466
  const end = starts[i + 1]?.index ?? lines.length;
5460
5467
  const block = lines.slice(start.index, end);
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);
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);
5464
5471
  nodes.push({
5465
5472
  type: "scenario",
5466
5473
  code: start.code,
@@ -6697,12 +6704,39 @@ function decisionFrontmatterEdges(path, decisionCode, value, targetType, kind) {
6697
6704
  }
6698
6705
  function matchExplicitRelationFieldLine(line, label) {
6699
6706
  const escaped = escapeRegExp(label);
6700
- const match = new RegExp(`^\\s*(?:\\*\\*${escaped}\\*\\*|${escaped})[:\uFF1A]\\s*(.*?)\\s*$`).exec(line);
6707
+ const match = new RegExp(`^ {0,3}(?:\\*\\*${escaped}\\*\\*|${escaped})[:\uFF1A]\\s*(.*?)\\s*$`).exec(line);
6701
6708
  return match?.[1] ?? null;
6702
6709
  }
6703
- function markdownRelationOccurrences(path, scenario, block, label, targetType, schema = DEFAULT_SCHEMA) {
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) {
6704
6735
  const result = [];
6705
6736
  block.forEach((line, index) => {
6737
+ if (codeLines[scenario.index + index]) {
6738
+ return;
6739
+ }
6706
6740
  const relationText = matchExplicitRelationFieldLine(line, label);
6707
6741
  if (!relationText) {
6708
6742
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "artifact-graph",
3
- "version": "0.9.2",
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",