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 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 = normalizeUid(options.from ?? options.to ?? "");
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 { ...parseScenarios(path, raw, schema), diagnostics: [] };
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 { ...parseDesign(path, raw), diagnostics: [] };
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
- raw.split(/\r?\n/).forEach((line, index) => {
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
- edges.push(edge(toUid("design", code), toUid("decision", codeValue), "references", "markdown", path, index + 1));
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(":").split(/\s+[—-]\s+/)[0] ?? line;
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 codes = extractCodes(relationText, targetType, schema);
6796
- for (const code of codes) {
6797
- result.push({ field: label, targetType, target: code, path, line: lineNumber, raw: relationText.trim() });
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 invalid of invalidRelationCandidates(relationText, targetType, codes)) {
6800
- result.push({ field: label, targetType, target: invalid, path, line: lineNumber, raw: relationText.trim() });
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 invalidRelationCandidates(text, targetType, validCodes) {
6841
- const valid = new Set(validCodes);
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 looksLikeRelationId(candidate, targetType) {
6847
- if (targetType === "feature") {
6848
- return /^[A-Z][A-Z0-9-]*$/.test(candidate) && (/\d/.test(candidate) || candidate.includes("-")) && !/^AC\d+$/.test(candidate);
6849
- }
6850
- if (targetType === "scenario") {
6851
- return /^S[-A-Z0-9]+[a-z]?$/.test(candidate);
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
- if (targetType === "decision") {
6854
- return /^D[-A-Z0-9]+$/.test(candidate);
6855
- }
6856
- if (targetType === "entity") {
6857
- return /^E[-0-9]+$/.test(candidate);
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 false;
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 normalizeUid(value) {
8039
+ function resolveQueryStartUid(graph, value) {
7983
8040
  if (value.includes(":")) {
7984
8041
  return value;
7985
8042
  }
7986
- if (/^S-\d+[a-z]?$/.test(value)) {
7987
- return toUid("scenario", value);
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 (/^[A-Z]{1,4}\d+$/.test(value)) {
7996
- return toUid("feature", value);
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 = normalizeUid(options.from ?? options.to ?? "");
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 { ...parseScenarios(path, raw, schema), diagnostics: [] };
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 { ...parseDesign(path, raw), diagnostics: [] };
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
- raw.split(/\r?\n/).forEach((line, index) => {
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
- edges.push(edge(toUid("design", code), toUid("decision", codeValue), "references", "markdown", path, index + 1));
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(":").split(/\s+[—-]\s+/)[0] ?? line;
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 codes = extractCodes(relationText, targetType, schema);
6774
- for (const code of codes) {
6775
- result.push({ field: label, targetType, target: code, path, line: lineNumber, raw: relationText.trim() });
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 invalid of invalidRelationCandidates(relationText, targetType, codes)) {
6778
- result.push({ field: label, targetType, target: invalid, path, line: lineNumber, raw: relationText.trim() });
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 invalidRelationCandidates(text, targetType, validCodes) {
6819
- const valid = new Set(validCodes);
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 looksLikeRelationId(candidate, targetType) {
6825
- if (targetType === "feature") {
6826
- return /^[A-Z][A-Z0-9-]*$/.test(candidate) && (/\d/.test(candidate) || candidate.includes("-")) && !/^AC\d+$/.test(candidate);
6827
- }
6828
- if (targetType === "scenario") {
6829
- return /^S[-A-Z0-9]+[a-z]?$/.test(candidate);
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
- if (targetType === "decision") {
6832
- return /^D[-A-Z0-9]+$/.test(candidate);
6833
- }
6834
- if (targetType === "entity") {
6835
- return /^E[-0-9]+$/.test(candidate);
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 false;
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 normalizeUid(value) {
8017
+ function resolveQueryStartUid(graph, value) {
7961
8018
  if (value.includes(":")) {
7962
8019
  return value;
7963
8020
  }
7964
- if (/^S-\d+[a-z]?$/.test(value)) {
7965
- return toUid("scenario", value);
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 (/^[A-Z]{1,4}\d+$/.test(value)) {
7974
- return toUid("feature", value);
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 = normalizeUid(options.from ?? options.to ?? "");
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 { ...parseScenarios(path, raw, schema), diagnostics: [] };
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 { ...parseDesign(path, raw), diagnostics: [] };
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
- raw.split(/\r?\n/).forEach((line, index) => {
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
- edges.push(edge(toUid("design", code), toUid("decision", codeValue), "references", "markdown", path, index + 1));
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(":").split(/\s+[—-]\s+/)[0] ?? line;
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 codes = extractCodes(relationText, targetType, schema);
6664
- for (const code of codes) {
6665
- result.push({ field: label, targetType, target: code, path, line: lineNumber, raw: relationText.trim() });
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 invalid of invalidRelationCandidates(relationText, targetType, codes)) {
6668
- result.push({ field: label, targetType, target: invalid, path, line: lineNumber, raw: relationText.trim() });
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 invalidRelationCandidates(text, targetType, validCodes) {
6709
- const valid = new Set(validCodes);
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 looksLikeRelationId(candidate, targetType) {
6715
- if (targetType === "feature") {
6716
- return /^[A-Z][A-Z0-9-]*$/.test(candidate) && (/\d/.test(candidate) || candidate.includes("-")) && !/^AC\d+$/.test(candidate);
6717
- }
6718
- if (targetType === "scenario") {
6719
- return /^S[-A-Z0-9]+[a-z]?$/.test(candidate);
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
- if (targetType === "decision") {
6722
- return /^D[-A-Z0-9]+$/.test(candidate);
6723
- }
6724
- if (targetType === "entity") {
6725
- return /^E[-0-9]+$/.test(candidate);
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 false;
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 normalizeUid(value) {
7907
+ function resolveQueryStartUid(graph, value) {
7851
7908
  if (value.includes(":")) {
7852
7909
  return value;
7853
7910
  }
7854
- if (/^S-\d+[a-z]?$/.test(value)) {
7855
- return toUid("scenario", value);
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 (/^[A-Z]{1,4}\d+$/.test(value)) {
7864
- return toUid("feature", value);
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",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "artifact-graph",
3
- "version": "0.9.0",
3
+ "version": "0.9.1",
4
4
  "description": "Git-native Markdown artifact graph scanner and validator",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",