guardlink 1.4.1 → 1.4.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.
Files changed (138) hide show
  1. package/CHANGELOG.md +111 -7
  2. package/README.md +53 -5
  3. package/dist/agents/config.d.ts +7 -0
  4. package/dist/agents/config.d.ts.map +1 -1
  5. package/dist/agents/config.js.map +1 -1
  6. package/dist/agents/index.d.ts +9 -1
  7. package/dist/agents/index.d.ts.map +1 -1
  8. package/dist/agents/index.js +36 -1
  9. package/dist/agents/index.js.map +1 -1
  10. package/dist/agents/launcher.d.ts.map +1 -1
  11. package/dist/agents/launcher.js +5 -0
  12. package/dist/agents/launcher.js.map +1 -1
  13. package/dist/agents/prompts.d.ts +16 -1
  14. package/dist/agents/prompts.d.ts.map +1 -1
  15. package/dist/agents/prompts.js +511 -16
  16. package/dist/agents/prompts.js.map +1 -1
  17. package/dist/analyze/format.d.ts +72 -0
  18. package/dist/analyze/format.d.ts.map +1 -0
  19. package/dist/analyze/format.js +176 -0
  20. package/dist/analyze/format.js.map +1 -0
  21. package/dist/analyze/index.d.ts +76 -0
  22. package/dist/analyze/index.d.ts.map +1 -1
  23. package/dist/analyze/index.js +165 -2
  24. package/dist/analyze/index.js.map +1 -1
  25. package/dist/analyze/prompts.d.ts +3 -2
  26. package/dist/analyze/prompts.d.ts.map +1 -1
  27. package/dist/analyze/prompts.js +17 -3
  28. package/dist/analyze/prompts.js.map +1 -1
  29. package/dist/analyzer/sarif.d.ts +3 -2
  30. package/dist/analyzer/sarif.d.ts.map +1 -1
  31. package/dist/analyzer/sarif.js +29 -3
  32. package/dist/analyzer/sarif.js.map +1 -1
  33. package/dist/cli/index.d.ts +2 -0
  34. package/dist/cli/index.d.ts.map +1 -1
  35. package/dist/cli/index.js +408 -37
  36. package/dist/cli/index.js.map +1 -1
  37. package/dist/dashboard/data.d.ts +11 -0
  38. package/dist/dashboard/data.d.ts.map +1 -1
  39. package/dist/dashboard/data.js +12 -0
  40. package/dist/dashboard/data.js.map +1 -1
  41. package/dist/dashboard/diagrams.d.ts +81 -12
  42. package/dist/dashboard/diagrams.d.ts.map +1 -1
  43. package/dist/dashboard/diagrams.js +750 -362
  44. package/dist/dashboard/diagrams.js.map +1 -1
  45. package/dist/dashboard/generate.d.ts +5 -2
  46. package/dist/dashboard/generate.d.ts.map +1 -1
  47. package/dist/dashboard/generate.js +2516 -244
  48. package/dist/dashboard/generate.js.map +1 -1
  49. package/dist/diff/engine.d.ts +2 -1
  50. package/dist/diff/engine.d.ts.map +1 -1
  51. package/dist/diff/engine.js +3 -2
  52. package/dist/diff/engine.js.map +1 -1
  53. package/dist/diff/git.js +3 -3
  54. package/dist/diff/git.js.map +1 -1
  55. package/dist/init/index.d.ts +7 -0
  56. package/dist/init/index.d.ts.map +1 -1
  57. package/dist/init/index.js +82 -27
  58. package/dist/init/index.js.map +1 -1
  59. package/dist/init/migrate.d.ts +39 -0
  60. package/dist/init/migrate.d.ts.map +1 -0
  61. package/dist/init/migrate.js +45 -0
  62. package/dist/init/migrate.js.map +1 -0
  63. package/dist/init/templates.d.ts +8 -0
  64. package/dist/init/templates.d.ts.map +1 -1
  65. package/dist/init/templates.js +68 -6
  66. package/dist/init/templates.js.map +1 -1
  67. package/dist/mcp/lookup.d.ts +1 -0
  68. package/dist/mcp/lookup.d.ts.map +1 -1
  69. package/dist/mcp/lookup.js +138 -10
  70. package/dist/mcp/lookup.js.map +1 -1
  71. package/dist/mcp/server.d.ts +2 -1
  72. package/dist/mcp/server.d.ts.map +1 -1
  73. package/dist/mcp/server.js +32 -15
  74. package/dist/mcp/server.js.map +1 -1
  75. package/dist/parser/clear.d.ts +2 -1
  76. package/dist/parser/clear.d.ts.map +1 -1
  77. package/dist/parser/clear.js +19 -29
  78. package/dist/parser/clear.js.map +1 -1
  79. package/dist/parser/comment-strip.d.ts +5 -0
  80. package/dist/parser/comment-strip.d.ts.map +1 -1
  81. package/dist/parser/comment-strip.js +8 -0
  82. package/dist/parser/comment-strip.js.map +1 -1
  83. package/dist/parser/feature-filter.d.ts +42 -0
  84. package/dist/parser/feature-filter.d.ts.map +1 -0
  85. package/dist/parser/feature-filter.js +109 -0
  86. package/dist/parser/feature-filter.js.map +1 -0
  87. package/dist/parser/format.d.ts +24 -0
  88. package/dist/parser/format.d.ts.map +1 -0
  89. package/dist/parser/format.js +29 -0
  90. package/dist/parser/format.js.map +1 -0
  91. package/dist/parser/index.d.ts +2 -0
  92. package/dist/parser/index.d.ts.map +1 -1
  93. package/dist/parser/index.js +1 -0
  94. package/dist/parser/index.js.map +1 -1
  95. package/dist/parser/parse-file.d.ts +1 -0
  96. package/dist/parser/parse-file.d.ts.map +1 -1
  97. package/dist/parser/parse-file.js +34 -9
  98. package/dist/parser/parse-file.js.map +1 -1
  99. package/dist/parser/parse-line.d.ts +9 -0
  100. package/dist/parser/parse-line.d.ts.map +1 -1
  101. package/dist/parser/parse-line.js +100 -26
  102. package/dist/parser/parse-line.js.map +1 -1
  103. package/dist/parser/parse-project.d.ts +1 -0
  104. package/dist/parser/parse-project.d.ts.map +1 -1
  105. package/dist/parser/parse-project.js +36 -2
  106. package/dist/parser/parse-project.js.map +1 -1
  107. package/dist/parser/validate.d.ts +3 -0
  108. package/dist/parser/validate.d.ts.map +1 -1
  109. package/dist/parser/validate.js +7 -0
  110. package/dist/parser/validate.js.map +1 -1
  111. package/dist/report/index.d.ts +1 -0
  112. package/dist/report/index.d.ts.map +1 -1
  113. package/dist/report/index.js +1 -0
  114. package/dist/report/index.js.map +1 -1
  115. package/dist/report/report.d.ts.map +1 -1
  116. package/dist/report/report.js +924 -24
  117. package/dist/report/report.js.map +1 -1
  118. package/dist/report/sequence.d.ts +11 -0
  119. package/dist/report/sequence.d.ts.map +1 -0
  120. package/dist/report/sequence.js +140 -0
  121. package/dist/report/sequence.js.map +1 -0
  122. package/dist/review/index.d.ts +3 -1
  123. package/dist/review/index.d.ts.map +1 -1
  124. package/dist/review/index.js +77 -35
  125. package/dist/review/index.js.map +1 -1
  126. package/dist/tui/commands.d.ts +1 -0
  127. package/dist/tui/commands.d.ts.map +1 -1
  128. package/dist/tui/commands.js +98 -12
  129. package/dist/tui/commands.js.map +1 -1
  130. package/dist/tui/index.d.ts.map +1 -1
  131. package/dist/tui/index.js +7 -2
  132. package/dist/tui/index.js.map +1 -1
  133. package/dist/types/index.d.ts +59 -3
  134. package/dist/types/index.d.ts.map +1 -1
  135. package/dist/workspace/merge.d.ts.map +1 -1
  136. package/dist/workspace/merge.js +6 -2
  137. package/dist/workspace/merge.js.map +1 -1
  138. package/package.json +1 -1
@@ -0,0 +1,109 @@
1
+ /**
2
+ * GuardLink — Feature-based filtering.
3
+ *
4
+ * Filters a ThreatModel to only include annotations that belong to
5
+ * specific features. Feature association is determined by file-level
6
+ * proximity: if a file contains @feature "X", all annotations in
7
+ * that file are considered part of feature "X".
8
+ *
9
+ * @comment -- "Pure filtering utility; no I/O"
10
+ */
11
+ /**
12
+ * Unique feature names found in the model, sorted alphabetically.
13
+ */
14
+ export function listFeatures(model) {
15
+ const names = new Set();
16
+ for (const f of model.features) {
17
+ names.add(f.feature);
18
+ }
19
+ return [...names].sort();
20
+ }
21
+ /**
22
+ * Build a map of file → Set<feature name> from feature annotations.
23
+ */
24
+ function buildFileFeatureMap(model) {
25
+ const map = new Map();
26
+ for (const f of model.features) {
27
+ const file = f.location.file;
28
+ if (!map.has(file))
29
+ map.set(file, new Set());
30
+ map.get(file).add(f.feature);
31
+ }
32
+ return map;
33
+ }
34
+ /**
35
+ * Filter a ThreatModel to only annotations in files tagged with
36
+ * one or more of the given feature names.
37
+ *
38
+ * Returns a new ThreatModel with only matching annotations.
39
+ * Feature matching is case-insensitive.
40
+ */
41
+ export function filterByFeature(model, featureNames) {
42
+ const wantedLower = new Set(featureNames.map(n => n.toLowerCase()));
43
+ const fileFeatureMap = buildFileFeatureMap(model);
44
+ // Determine which files match any of the requested features
45
+ const matchingFiles = new Set();
46
+ for (const [file, features] of fileFeatureMap) {
47
+ for (const f of features) {
48
+ if (wantedLower.has(f.toLowerCase())) {
49
+ matchingFiles.add(file);
50
+ break;
51
+ }
52
+ }
53
+ }
54
+ // Filter helper
55
+ const inFeature = (arr) => arr.filter(item => matchingFiles.has(item.location.file));
56
+ return {
57
+ ...model,
58
+ // Preserve metadata
59
+ annotations_parsed: model.annotations_parsed,
60
+ source_files: model.source_files,
61
+ annotated_files: model.annotated_files.filter(f => matchingFiles.has(f)),
62
+ unannotated_files: model.unannotated_files,
63
+ // Filter each category
64
+ assets: inFeature(model.assets),
65
+ threats: inFeature(model.threats),
66
+ controls: inFeature(model.controls),
67
+ mitigations: inFeature(model.mitigations),
68
+ exposures: inFeature(model.exposures),
69
+ confirmed: inFeature(model.confirmed),
70
+ acceptances: inFeature(model.acceptances),
71
+ transfers: inFeature(model.transfers),
72
+ flows: inFeature(model.flows),
73
+ boundaries: inFeature(model.boundaries),
74
+ validations: inFeature(model.validations),
75
+ audits: inFeature(model.audits),
76
+ ownership: inFeature(model.ownership),
77
+ data_handling: inFeature(model.data_handling),
78
+ assumptions: inFeature(model.assumptions),
79
+ shields: inFeature(model.shields),
80
+ features: inFeature(model.features),
81
+ comments: inFeature(model.comments),
82
+ };
83
+ }
84
+ /**
85
+ * Get summary stats for each feature in the model.
86
+ */
87
+ export function getFeatureSummaries(model) {
88
+ const featureNames = listFeatures(model);
89
+ return featureNames.map(name => {
90
+ const filtered = filterByFeature(model, [name]);
91
+ return {
92
+ name,
93
+ files: [...new Set(model.features.filter(f => f.feature.toLowerCase() === name.toLowerCase()).map(f => f.location.file))],
94
+ annotations: filtered.assets.length + filtered.threats.length + filtered.controls.length +
95
+ filtered.mitigations.length + filtered.exposures.length + filtered.confirmed.length +
96
+ filtered.acceptances.length + filtered.transfers.length + filtered.flows.length +
97
+ filtered.boundaries.length + filtered.validations.length + filtered.audits.length +
98
+ filtered.ownership.length + filtered.data_handling.length + filtered.assumptions.length +
99
+ filtered.features.length + filtered.comments.length + filtered.shields.length,
100
+ exposures: filtered.exposures.length,
101
+ mitigations: filtered.mitigations.length,
102
+ assets: filtered.assets.length,
103
+ threats: filtered.threats.length,
104
+ flows: filtered.flows.length,
105
+ confirmed: filtered.confirmed.length,
106
+ };
107
+ });
108
+ }
109
+ //# sourceMappingURL=feature-filter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"feature-filter.js","sourceRoot":"","sources":["../../src/parser/feature-filter.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,24 @@
1
+ /**
2
+ * GuardLink parser — Diagnostic formatting helpers.
3
+ *
4
+ * Tiny pure helpers shared by the CLI's `printDiagnostics` and the TUI's
5
+ * status-command printer so the level-to-icon mapping lives in one place.
6
+ * If a future iteration adds a new diagnostic tier, only this file needs
7
+ * to change for the visual treatment to stay consistent across surfaces.
8
+ */
9
+ import type { ParseDiagnostic } from '../types/index.js';
10
+ /**
11
+ * Returns the icon character for a diagnostic level. No color, no padding,
12
+ * no extra whitespace — callers that want those (e.g. the TUI's
13
+ * `C.error(' ' + diagnosticIcon(...))`) wrap the return value themselves.
14
+ *
15
+ * - `'warning'` → `⚠` (informational, never blocks)
16
+ * - `'error'` → `✗` (annotation skipped, model continues)
17
+ * - `'fatal'` → `✗✗` (model is unsafe to render; consumer must abort)
18
+ *
19
+ * The switch is exhaustive over `ParseDiagnostic['level']` — TypeScript
20
+ * will flag this function if a new level is added to the union without a
21
+ * matching case here.
22
+ */
23
+ export declare function diagnosticIcon(level: ParseDiagnostic['level']): string;
24
+ //# sourceMappingURL=format.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"format.d.ts","sourceRoot":"","sources":["../../src/parser/format.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAEzD;;;;;;;;;;;;GAYG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,eAAe,CAAC,OAAO,CAAC,GAAG,MAAM,CAMtE"}
@@ -0,0 +1,29 @@
1
+ /**
2
+ * GuardLink parser — Diagnostic formatting helpers.
3
+ *
4
+ * Tiny pure helpers shared by the CLI's `printDiagnostics` and the TUI's
5
+ * status-command printer so the level-to-icon mapping lives in one place.
6
+ * If a future iteration adds a new diagnostic tier, only this file needs
7
+ * to change for the visual treatment to stay consistent across surfaces.
8
+ */
9
+ /**
10
+ * Returns the icon character for a diagnostic level. No color, no padding,
11
+ * no extra whitespace — callers that want those (e.g. the TUI's
12
+ * `C.error(' ' + diagnosticIcon(...))`) wrap the return value themselves.
13
+ *
14
+ * - `'warning'` → `⚠` (informational, never blocks)
15
+ * - `'error'` → `✗` (annotation skipped, model continues)
16
+ * - `'fatal'` → `✗✗` (model is unsafe to render; consumer must abort)
17
+ *
18
+ * The switch is exhaustive over `ParseDiagnostic['level']` — TypeScript
19
+ * will flag this function if a new level is added to the union without a
20
+ * matching case here.
21
+ */
22
+ export function diagnosticIcon(level) {
23
+ switch (level) {
24
+ case 'fatal': return '✗✗';
25
+ case 'error': return '✗';
26
+ case 'warning': return '⚠';
27
+ }
28
+ }
29
+ //# sourceMappingURL=format.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"format.js","sourceRoot":"","sources":["../../src/parser/format.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,cAAc,CAAC,KAA+B;IAC5D,QAAQ,KAAK,EAAE,CAAC;QACd,KAAK,OAAO,CAAC,CAAG,OAAO,IAAI,CAAC;QAC5B,KAAK,OAAO,CAAC,CAAG,OAAO,GAAG,CAAC;QAC3B,KAAK,SAAS,CAAC,CAAC,OAAO,GAAG,CAAC;IAC7B,CAAC;AACH,CAAC"}
@@ -10,4 +10,6 @@ export { stripCommentPrefix, commentStyleForExt } from './comment-strip.js';
10
10
  export { findDanglingRefs, findUnmitigatedExposures, findAcceptedWithoutAudit, findAcceptedExposures } from './validate.js';
11
11
  export { clearAnnotations } from './clear.js';
12
12
  export type { ClearAnnotationsOptions, ClearAnnotationsResult } from './clear.js';
13
+ export { listFeatures, filterByFeature, getFeatureSummaries } from './feature-filter.js';
14
+ export type { FeatureSummary } from './feature-filter.js';
13
15
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/parser/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AACzD,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,YAAY,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAC9D,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAC5C,OAAO,EAAE,aAAa,EAAE,eAAe,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AACrF,OAAO,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AAC5E,OAAO,EAAE,gBAAgB,EAAE,wBAAwB,EAAE,wBAAwB,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAC;AAC5H,OAAO,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAC9C,YAAY,EAAE,uBAAuB,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/parser/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AACzD,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,YAAY,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAC9D,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAC5C,OAAO,EAAE,aAAa,EAAE,eAAe,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AACrF,OAAO,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AAC5E,OAAO,EAAE,gBAAgB,EAAE,wBAAwB,EAAE,wBAAwB,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAC;AAC5H,OAAO,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAC9C,YAAY,EAAE,uBAAuB,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAC;AAClF,OAAO,EAAE,YAAY,EAAE,eAAe,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AACzF,YAAY,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC"}
@@ -8,4 +8,5 @@ export { normalizeName, resolveSeverity, unescapeDescription } from './normalize
8
8
  export { stripCommentPrefix, commentStyleForExt } from './comment-strip.js';
9
9
  export { findDanglingRefs, findUnmitigatedExposures, findAcceptedWithoutAudit, findAcceptedExposures } from './validate.js';
10
10
  export { clearAnnotations } from './clear.js';
11
+ export { listFeatures, filterByFeature, getFeatureSummaries } from './feature-filter.js';
11
12
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/parser/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AACzD,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAElD,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAC5C,OAAO,EAAE,aAAa,EAAE,eAAe,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AACrF,OAAO,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AAC5E,OAAO,EAAE,gBAAgB,EAAE,wBAAwB,EAAE,wBAAwB,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAC;AAC5H,OAAO,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/parser/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AACzD,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAElD,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAC5C,OAAO,EAAE,aAAa,EAAE,eAAe,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AACrF,OAAO,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AAC5E,OAAO,EAAE,gBAAgB,EAAE,wBAAwB,EAAE,wBAAwB,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAC;AAC5H,OAAO,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAE9C,OAAO,EAAE,YAAY,EAAE,eAAe,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC"}
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * GuardLink — File-level parser.
3
3
  * Reads source files and extracts all GuardLink annotations.
4
+ * Standalone .gal files are treated as raw annotation text.
4
5
  *
5
6
  * @exposes #parser to #path-traversal [high] cwe:CWE-22 -- "File path from caller read via readFile; no validation here"
6
7
  * @exposes #parser to #dos [medium] cwe:CWE-400 -- "Large files loaded entirely into memory"
@@ -1 +1 @@
1
- {"version":3,"file":"parse-file.d.ts","sourceRoot":"","sources":["../../src/parser/parse-file.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAIH,OAAO,KAAK,EAA+B,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAKlF;;GAEG;AACH,wBAAsB,SAAS,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CAGtE;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,GAAE,MAAkB,GAAG,WAAW,CAuEtF"}
1
+ {"version":3,"file":"parse-file.d.ts","sourceRoot":"","sources":["../../src/parser/parse-file.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAGH,OAAO,KAAK,EAA+B,WAAW,EAAkB,MAAM,mBAAmB,CAAC;AAKlG;;GAEG;AACH,wBAAsB,SAAS,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CAGtE;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,GAAE,MAAkB,GAAG,WAAW,CA+FtF"}
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * GuardLink — File-level parser.
3
3
  * Reads source files and extracts all GuardLink annotations.
4
+ * Standalone .gal files are treated as raw annotation text.
4
5
  *
5
6
  * @exposes #parser to #path-traversal [high] cwe:CWE-22 -- "File path from caller read via readFile; no validation here"
6
7
  * @exposes #parser to #dos [medium] cwe:CWE-400 -- "Large files loaded entirely into memory"
@@ -9,7 +10,7 @@
9
10
  * @flows #parser -> Annotations via parseString -- "Parsed annotation output"
10
11
  */
11
12
  import { readFile } from 'node:fs/promises';
12
- import { stripCommentPrefix } from './comment-strip.js';
13
+ import { isStandaloneAnnotationFile, stripCommentPrefix } from './comment-strip.js';
13
14
  import { parseLine } from './parse-line.js';
14
15
  import { unescapeDescription } from './normalize.js';
15
16
  /**
@@ -29,20 +30,24 @@ export function parseString(content, filePath = '<input>') {
29
30
  const diagnostics = [];
30
31
  let lastAnnotation = null;
31
32
  let inShield = false;
33
+ const allowRawAnnotationLines = isStandaloneAnnotationFile(filePath);
34
+ let currentSource = null;
32
35
  for (let i = 0; i < lines.length; i++) {
33
36
  const lineNum = i + 1; // 1-indexed
34
37
  const rawLine = lines[i];
35
- // Strip comment prefix
36
- const inner = stripCommentPrefix(rawLine);
38
+ // Strip comment prefix unless this is a standalone .gal file, where
39
+ // annotations are stored as raw lines instead of host-language comments.
40
+ const inner = allowRawAnnotationLines ? rawLine : stripCommentPrefix(rawLine);
37
41
  if (inner === null) {
38
42
  lastAnnotation = null;
39
43
  continue;
40
44
  }
45
+ const text = inner.trimStart();
41
46
  // Check for shield block boundaries — always parse these even inside shields
42
- const trimmed = inner.trim();
47
+ const trimmed = text.trim();
43
48
  if (trimmed.startsWith('@shield:end')) {
44
49
  const location = { file: filePath, line: lineNum };
45
- const result = parseLine(inner, location);
50
+ const result = parseLine(text, location);
46
51
  if (result.annotation)
47
52
  annotations.push(result.annotation);
48
53
  inShield = false;
@@ -51,7 +56,7 @@ export function parseString(content, filePath = '<input>') {
51
56
  }
52
57
  if (trimmed.startsWith('@shield:begin')) {
53
58
  const location = { file: filePath, line: lineNum };
54
- const result = parseLine(inner, location);
59
+ const result = parseLine(text, location);
55
60
  if (result.annotation)
56
61
  annotations.push(result.annotation);
57
62
  inShield = true;
@@ -62,7 +67,7 @@ export function parseString(content, filePath = '<input>') {
62
67
  if (inShield)
63
68
  continue;
64
69
  // Check for continuation line: -- "..."
65
- const contMatch = inner.match(/^--\s*"((?:[^"\\]|\\.)*)"/);
70
+ const contMatch = text.match(/^--\s*"((?:[^"\\]|\\.)*)"/);
66
71
  if (contMatch && lastAnnotation) {
67
72
  // Append to last annotation's description
68
73
  const contDesc = unescapeDescription(contMatch[1]);
@@ -76,10 +81,30 @@ export function parseString(content, filePath = '<input>') {
76
81
  }
77
82
  // Try to parse as annotation
78
83
  const location = { file: filePath, line: lineNum };
79
- const result = parseLine(inner, location);
84
+ const result = parseLine(text, location);
85
+ if (result.sourceDirective) {
86
+ currentSource = {
87
+ file: result.sourceDirective.file,
88
+ line: result.sourceDirective.line,
89
+ parent_symbol: result.sourceDirective.symbol ?? null,
90
+ };
91
+ lastAnnotation = null;
92
+ continue;
93
+ }
80
94
  if (result.annotation) {
95
+ if (allowRawAnnotationLines && currentSource) {
96
+ result.annotation.location = {
97
+ file: currentSource.file,
98
+ line: currentSource.line,
99
+ parent_symbol: currentSource.parent_symbol ?? null,
100
+ origin_file: filePath,
101
+ origin_line: lineNum,
102
+ };
103
+ }
81
104
  annotations.push(result.annotation);
82
- lastAnnotation = result.annotation;
105
+ if (result.extraAnnotations)
106
+ annotations.push(...result.extraAnnotations);
107
+ lastAnnotation = annotations[annotations.length - 1];
83
108
  }
84
109
  else {
85
110
  if (result.diagnostic) {
@@ -1 +1 @@
1
- {"version":3,"file":"parse-file.js","sourceRoot":"","sources":["../../src/parser/parse-file.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"parse-file.js","sourceRoot":"","sources":["../../src/parser/parse-file.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAE5C,OAAO,EAAE,0BAA0B,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AACpF,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAC5C,OAAO,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AAErD;;GAEG;AACH,MAAM,CAAC,KAAK,UAAU,SAAS,CAAC,QAAgB;IAC9C,MAAM,OAAO,GAAG,MAAM,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;IAClD,OAAO,WAAW,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;AACxC,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,WAAW,CAAC,OAAe,EAAE,WAAmB,SAAS;IACvE,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAClC,MAAM,WAAW,GAAiB,EAAE,CAAC;IACrC,MAAM,WAAW,GAAsB,EAAE,CAAC;IAC1C,IAAI,cAAc,GAAsB,IAAI,CAAC;IAC7C,IAAI,QAAQ,GAAG,KAAK,CAAC;IACrB,MAAM,uBAAuB,GAAG,0BAA0B,CAAC,QAAQ,CAAC,CAAC;IACrE,IAAI,aAAa,GAA0B,IAAI,CAAC;IAEhD,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACtC,MAAM,OAAO,GAAG,CAAC,GAAG,CAAC,CAAC,CAAE,YAAY;QACpC,MAAM,OAAO,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;QAEzB,oEAAoE;QACpE,yEAAyE;QACzE,MAAM,KAAK,GAAG,uBAAuB,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,kBAAkB,CAAC,OAAO,CAAC,CAAC;QAC9E,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;YACnB,cAAc,GAAG,IAAI,CAAC;YACtB,SAAS;QACX,CAAC;QACD,MAAM,IAAI,GAAG,KAAK,CAAC,SAAS,EAAE,CAAC;QAE/B,6EAA6E;QAC7E,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;QAC5B,IAAI,OAAO,CAAC,UAAU,CAAC,aAAa,CAAC,EAAE,CAAC;YACtC,MAAM,QAAQ,GAAG,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC;YACnD,MAAM,MAAM,GAAG,SAAS,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;YACzC,IAAI,MAAM,CAAC,UAAU;gBAAE,WAAW,CAAC,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC;YAC3D,QAAQ,GAAG,KAAK,CAAC;YACjB,cAAc,GAAG,IAAI,CAAC;YACtB,SAAS;QACX,CAAC;QACD,IAAI,OAAO,CAAC,UAAU,CAAC,eAAe,CAAC,EAAE,CAAC;YACxC,MAAM,QAAQ,GAAG,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC;YACnD,MAAM,MAAM,GAAG,SAAS,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;YACzC,IAAI,MAAM,CAAC,UAAU;gBAAE,WAAW,CAAC,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC;YAC3D,QAAQ,GAAG,IAAI,CAAC;YAChB,cAAc,GAAG,IAAI,CAAC;YACtB,SAAS;QACX,CAAC;QAED,4EAA4E;QAC5E,IAAI,QAAQ;YAAE,SAAS;QAEvB,wCAAwC;QACxC,MAAM,SAAS,GAAG,IAAI,CAAC,KAAK,CAAC,2BAA2B,CAAC,CAAC;QAC1D,IAAI,SAAS,IAAI,cAAc,EAAE,CAAC;YAChC,0CAA0C;YAC1C,MAAM,QAAQ,GAAG,mBAAmB,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC;YACnD,IAAI,cAAc,CAAC,WAAW,EAAE,CAAC;gBAC/B,cAAc,CAAC,WAAW,IAAI,GAAG,GAAG,QAAQ,CAAC;YAC/C,CAAC;iBAAM,CAAC;gBACN,cAAc,CAAC,WAAW,GAAG,QAAQ,CAAC;YACxC,CAAC;YACD,SAAS;QACX,CAAC;QAED,6BAA6B;QAC7B,MAAM,QAAQ,GAAG,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC;QACnD,MAAM,MAAM,GAAG,SAAS,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;QAEzC,IAAI,MAAM,CAAC,eAAe,EAAE,CAAC;YAC3B,aAAa,GAAG;gBACd,IAAI,EAAE,MAAM,CAAC,eAAe,CAAC,IAAI;gBACjC,IAAI,EAAE,MAAM,CAAC,eAAe,CAAC,IAAI;gBACjC,aAAa,EAAE,MAAM,CAAC,eAAe,CAAC,MAAM,IAAI,IAAI;aACrD,CAAC;YACF,cAAc,GAAG,IAAI,CAAC;YACtB,SAAS;QACX,CAAC;QAED,IAAI,MAAM,CAAC,UAAU,EAAE,CAAC;YACtB,IAAI,uBAAuB,IAAI,aAAa,EAAE,CAAC;gBAC7C,MAAM,CAAC,UAAU,CAAC,QAAQ,GAAG;oBAC3B,IAAI,EAAE,aAAa,CAAC,IAAI;oBACxB,IAAI,EAAE,aAAa,CAAC,IAAI;oBACxB,aAAa,EAAE,aAAa,CAAC,aAAa,IAAI,IAAI;oBAClD,WAAW,EAAE,QAAQ;oBACrB,WAAW,EAAE,OAAO;iBACrB,CAAC;YACJ,CAAC;YACD,WAAW,CAAC,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC;YACpC,IAAI,MAAM,CAAC,gBAAgB;gBAAE,WAAW,CAAC,IAAI,CAAC,GAAG,MAAM,CAAC,gBAAgB,CAAC,CAAC;YAC1E,cAAc,GAAG,WAAW,CAAC,WAAW,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;QACvD,CAAC;aAAM,CAAC;YACN,IAAI,MAAM,CAAC,UAAU,EAAE,CAAC;gBACtB,WAAW,CAAC,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC;YACtC,CAAC;YACD,IAAI,CAAC,MAAM,CAAC,cAAc,EAAE,CAAC;gBAC3B,cAAc,GAAG,IAAI,CAAC;YACxB,CAAC;QACH,CAAC;IACH,CAAC;IAED,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,YAAY,EAAE,CAAC,EAAE,CAAC;AACvD,CAAC"}
@@ -7,10 +7,19 @@
7
7
  * @comment -- "Regex patterns designed with bounded quantifiers and explicit structure"
8
8
  */
9
9
  import type { Annotation, ParseDiagnostic, SourceLocation } from '../types/index.js';
10
+ export interface SourceDirective {
11
+ file: string;
12
+ line: number;
13
+ symbol?: string;
14
+ }
10
15
  export interface ParseLineResult {
11
16
  annotation: Annotation | null;
17
+ /** Additional annotations from the same line. Used by multi-hop @flows
18
+ * chains (`A -> B -> C`) to emit one pairwise flow per arrow. */
19
+ extraAnnotations?: Annotation[];
12
20
  diagnostic: ParseDiagnostic | null;
13
21
  isContinuation: boolean;
22
+ sourceDirective?: SourceDirective | null;
14
23
  }
15
24
  /**
16
25
  * Parse a single annotation line (after comment prefix has been stripped).
@@ -1 +1 @@
1
- {"version":3,"file":"parse-line.d.ts","sourceRoot":"","sources":["../../src/parser/parse-line.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EACV,UAAU,EACV,eAAe,EAAE,cAAc,EAChC,MAAM,mBAAmB,CAAC;AAsE3B,MAAM,WAAW,eAAe;IAC9B,UAAU,EAAE,UAAU,GAAG,IAAI,CAAC;IAC9B,UAAU,EAAE,eAAe,GAAG,IAAI,CAAC;IACnC,cAAc,EAAE,OAAO,CAAC;CACzB;AAED;;;;GAIG;AACH,wBAAgB,SAAS,CACvB,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,cAAc,GACvB,eAAe,CA8KjB"}
1
+ {"version":3,"file":"parse-line.d.ts","sourceRoot":"","sources":["../../src/parser/parse-line.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EACV,UAAU,EACV,eAAe,EAAE,cAAc,EAChC,MAAM,mBAAmB,CAAC;AA0F3B,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,eAAe;IAC9B,UAAU,EAAE,UAAU,GAAG,IAAI,CAAC;IAC9B;sEACkE;IAClE,gBAAgB,CAAC,EAAE,UAAU,EAAE,CAAC;IAChC,UAAU,EAAE,eAAe,GAAG,IAAI,CAAC;IACnC,cAAc,EAAE,OAAO,CAAC;IACxB,eAAe,CAAC,EAAE,eAAe,GAAG,IAAI,CAAC;CAC1C;AAED;;;;GAIG;AACH,wBAAgB,SAAS,CACvB,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,cAAc,GACvB,eAAe,CA0NjB"}
@@ -9,14 +9,20 @@
9
9
  import { normalizeName, resolveSeverity, unescapeDescription } from './normalize.js';
10
10
  // ─── Shared regex fragments ──────────────────────────────────────────
11
11
  const COMPONENT = String.raw `[A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*`;
12
- const ASSET_REF = String.raw `(?:#[a-zA-Z0-9_-]+|[A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*)`; // #id or Dotted.Path
12
+ // Quoted ref: any non-newline content between double quotes, with `\"` and
13
+ // `\\` escape support. Mirrors the DESC fragment's character class.
14
+ const QUOTED_REF = String.raw `"(?:[^"\\\n]|\\.)*"`;
15
+ const ASSET_REF = String.raw `(?:#[a-zA-Z0-9_-]+|${QUOTED_REF}|[A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*)`; // #id, "quoted", or Dotted.Path
13
16
  const NAME = String.raw `[A-Za-z]\w*(?:[_\- ][A-Za-z]\w*)*`;
14
17
  const ID_DEF = String.raw `\(#([a-zA-Z0-9_-]+)\)`;
15
18
  const ID_REF = String.raw `#([a-zA-Z0-9_-]+)`;
16
- const THREAT_REF = String.raw `(?:#[a-zA-Z0-9_-]+|[A-Za-z]\w*(?:[_\- ][A-Za-z]\w*)*)`;
19
+ const THREAT_REF = String.raw `(?:#[a-zA-Z0-9_-]+|${QUOTED_REF}|[A-Za-z]\w*(?:[_\- ][A-Za-z]\w*)*)`;
17
20
  const SEVERITY = String.raw `\[(P[0-3]|critical|high|medium|low)\]`;
18
21
  const EXT_REF = String.raw `([a-zA-Z]+:[A-Za-z0-9_:.\-]+)`;
19
22
  const DESC = String.raw `--\s*"((?:[^"\\]|\\.)*)"`;
23
+ const SOURCE_FILE = String.raw `\S+`;
24
+ const SOURCE_LINE = String.raw `[1-9]\d*`;
25
+ const SOURCE_SYMBOL = String.raw `\S+`;
20
26
  // Capture external refs (0 or more, space-separated)
21
27
  const EXT_REFS_OPT = String.raw `((?:\s+[a-zA-Z]+:[A-Za-z0-9_:.\-]+)*)`;
22
28
  // ─── Verb-specific patterns ──────────────────────────────────────────
@@ -29,10 +35,11 @@ const PATTERNS = {
29
35
  mitigates: new RegExp(String.raw `^@mitigates\s+(${ASSET_REF})\s+against\s+(${THREAT_REF})(?:\s+using\s+(${THREAT_REF}))?(?:\s+${DESC})?$`),
30
36
  mitigates_v1: new RegExp(String.raw `^@mitigates\s+(${ASSET_REF})\s+against\s+(${THREAT_REF})(?:\s+with\s+(${THREAT_REF}))?(?:\s+${DESC})?$`),
31
37
  exposes: new RegExp(String.raw `^@exposes\s+(${ASSET_REF})\s+to\s+(${THREAT_REF})(?:\s+${SEVERITY})?${EXT_REFS_OPT}(?:\s+${DESC})?$`),
38
+ confirmed: new RegExp(String.raw `^@confirmed\s+(${THREAT_REF})\s+on\s+(${ASSET_REF})(?:\s+${SEVERITY})?${EXT_REFS_OPT}(?:\s+${DESC})?$`),
32
39
  accepts: new RegExp(String.raw `^@accepts\s+(${THREAT_REF})\s+on\s+(${ASSET_REF})(?:\s+${DESC})?$`),
33
40
  accepts_v1: new RegExp(String.raw `^@accepts\s+(${THREAT_REF})\s+to\s+(${ASSET_REF})(?:\s+${DESC})?$`),
34
41
  transfers: new RegExp(String.raw `^@transfers\s+(${THREAT_REF})\s+from\s+(${ASSET_REF})\s+to\s+(${ASSET_REF})(?:\s+${DESC})?$`),
35
- flows: new RegExp(String.raw `^@flows\s+(${ASSET_REF})\s+->\s+(${ASSET_REF})(?:\s+via\s+((?:(?!\s+--\s*").)+?))?(?:\s+${DESC})?$`),
42
+ flows: new RegExp(String.raw `^@flows\s+(${ASSET_REF}(?:\s+->\s+${ASSET_REF})+)(?:\s+via\s+((?:(?!\s+--\s*").)+?))?(?:\s+${DESC})?$`),
36
43
  boundary: new RegExp(String.raw `^@boundary\s+(?:between\s+)?(${ASSET_REF})\s+and\s+(${ASSET_REF})(?:\s+${ID_DEF})?(?:\s+${DESC})?$`),
37
44
  boundary_pipe: new RegExp(String.raw `^@boundary\s+(${ASSET_REF})\s*\|\s*(${ASSET_REF})(?:\s+${ID_DEF})?(?:\s+${DESC})?$`),
38
45
  connects_v1: new RegExp(String.raw `^@connects\s+(${ASSET_REF})\s+to\s+(${ASSET_REF})(?:\s+${DESC})?$`),
@@ -43,8 +50,12 @@ const PATTERNS = {
43
50
  owns: new RegExp(String.raw `^@owns\s+([a-zA-Z0-9_-]+)\s+for\s+(${ASSET_REF})(?:\s+${DESC})?$`),
44
51
  handles: new RegExp(String.raw `^@handles\s+(pii|phi|financial|secrets|internal|public)\s+on\s+(${ASSET_REF})(?:\s+${DESC})?$`, 'i'),
45
52
  assumes: new RegExp(String.raw `^@assumes\s+(${ASSET_REF})(?:\s+${DESC})?$`),
53
+ // Metadata — feature tagging
54
+ feature: new RegExp(String.raw `^@feature\s+"((?:[^"\\]|\\.)*)"(?:\s+${DESC})?$`),
46
55
  // Comment — developer note, description only
47
56
  comment: new RegExp(String.raw `^@comment(?:\s+${DESC})?$`),
57
+ // Standalone .gal directive — sets logical source location for following annotations
58
+ source: new RegExp(String.raw `^@source\s+file:(${SOURCE_FILE})\s+line:(${SOURCE_LINE})(?:\s+symbol:(${SOURCE_SYMBOL}))?$`),
48
59
  // Special
49
60
  shield: new RegExp(String.raw `^@shield(?!:)(?:\s+${DESC})?$`),
50
61
  shield_begin: new RegExp(String.raw `^@shield:begin(?:\s+${DESC})?$`),
@@ -56,8 +67,15 @@ function extractExternalRefs(raw) {
56
67
  return [];
57
68
  return raw.trim().split(/\s+/).filter(r => /^[a-zA-Z]+:[A-Za-z0-9_:.\-]+$/.test(r));
58
69
  }
59
- // ─── Ref resolver: #id or Name → string ──────────────────────────────
70
+ // ─── Ref resolver: #id, "quoted", or Dotted.Pathcanonical string ───
71
+ /** Normalize a captured ASSET_REF or THREAT_REF for storage in the model.
72
+ * Strips surrounding double quotes and processes escape sequences (\", \\)
73
+ * when the user wrote a quoted ref like `"User Browser"` or `"/api/login"`.
74
+ * Pass-through for `#id` and `Dotted.Path` forms. */
60
75
  function resolveRef(ref) {
76
+ if (ref.length >= 2 && ref.charCodeAt(0) === 0x22 /* " */ && ref.charCodeAt(ref.length - 1) === 0x22) {
77
+ return unescapeDescription(ref.slice(1, -1));
78
+ }
61
79
  return ref;
62
80
  }
63
81
  /**
@@ -72,9 +90,9 @@ export function parseLine(text, location) {
72
90
  // Check for continuation line (-- "...")
73
91
  const contMatch = trimmed.match(new RegExp(String.raw `^${DESC}$`));
74
92
  if (contMatch) {
75
- return { annotation: null, diagnostic: null, isContinuation: true };
93
+ return { annotation: null, diagnostic: null, isContinuation: true, sourceDirective: null };
76
94
  }
77
- return { annotation: null, diagnostic: null, isContinuation: false };
95
+ return { annotation: null, diagnostic: null, isContinuation: false, sourceDirective: null };
78
96
  }
79
97
  const base = { location, raw: trimmed };
80
98
  let m;
@@ -102,7 +120,7 @@ export function parseLine(text, location) {
102
120
  // ── @mitigates ──
103
121
  if ((m = trimmed.match(PATTERNS.mitigates)) || (m = trimmed.match(PATTERNS.mitigates_v1))) {
104
122
  return ok({
105
- ...base, verb: 'mitigates', asset: m[1],
123
+ ...base, verb: 'mitigates', asset: resolveRef(m[1]),
106
124
  threat: resolveRef(m[2]), control: m[3] ? resolveRef(m[3]) : undefined,
107
125
  description: desc(m[4]),
108
126
  });
@@ -110,77 +128,118 @@ export function parseLine(text, location) {
110
128
  // ── @exposes ──
111
129
  if ((m = trimmed.match(PATTERNS.exposes))) {
112
130
  return ok({
113
- ...base, verb: 'exposes', asset: m[1], threat: resolveRef(m[2]),
131
+ ...base, verb: 'exposes', asset: resolveRef(m[1]), threat: resolveRef(m[2]),
132
+ severity: m[3] ? resolveSeverity(m[3]) : undefined,
133
+ external_refs: extractExternalRefs(m[4]), description: desc(m[5]),
134
+ });
135
+ }
136
+ // ── @confirmed ──
137
+ if ((m = trimmed.match(PATTERNS.confirmed))) {
138
+ return ok({
139
+ ...base, verb: 'confirmed', threat: resolveRef(m[1]), asset: resolveRef(m[2]),
114
140
  severity: m[3] ? resolveSeverity(m[3]) : undefined,
115
141
  external_refs: extractExternalRefs(m[4]), description: desc(m[5]),
116
142
  });
117
143
  }
118
144
  // ── @accepts ──
119
145
  if ((m = trimmed.match(PATTERNS.accepts)) || (m = trimmed.match(PATTERNS.accepts_v1))) {
120
- return ok({ ...base, verb: 'accepts', threat: resolveRef(m[1]), asset: m[2], description: desc(m[3]) });
146
+ return ok({ ...base, verb: 'accepts', threat: resolveRef(m[1]), asset: resolveRef(m[2]), description: desc(m[3]) });
121
147
  }
122
148
  // ── @transfers ──
123
149
  if ((m = trimmed.match(PATTERNS.transfers))) {
124
150
  return ok({
125
151
  ...base, verb: 'transfers', threat: resolveRef(m[1]),
126
- source: m[2], target: m[3], description: desc(m[4]),
152
+ source: resolveRef(m[2]), target: resolveRef(m[3]), description: desc(m[4]),
127
153
  });
128
154
  }
129
155
  // ── @flows ──
156
+ // Single-hop `A -> B` is a chain of length 2 producing one flow.
157
+ // Multi-hop `A -> B -> C -> D` is treated as syntactic sugar for N-1
158
+ // pairwise flows — each emitted flow shares the mechanism, description,
159
+ // and source location with every other hop in the chain.
130
160
  if ((m = trimmed.match(PATTERNS.flows))) {
131
- return ok({
132
- ...base, verb: 'flows', source: m[1], target: m[2],
133
- mechanism: m[3]?.trim(), description: desc(m[4]),
134
- });
161
+ // Use matchAll instead of split so quoted refs containing literal
162
+ // `->` sequences (e.g. `"step1 -> step2"`) aren't shredded by the
163
+ // arrow separator. The outer regex has already validated chain shape.
164
+ const participants = [...m[1].matchAll(new RegExp(ASSET_REF, 'g'))]
165
+ .map(mm => resolveRef(mm[0]));
166
+ const mechanism = m[2]?.trim();
167
+ const description = desc(m[3]);
168
+ const flows = [];
169
+ for (let i = 0; i < participants.length - 1; i++) {
170
+ flows.push({
171
+ ...base, verb: 'flows',
172
+ source: participants[i], target: participants[i + 1],
173
+ mechanism, description,
174
+ });
175
+ }
176
+ return okMulti(flows);
135
177
  }
136
178
  // ── @boundary ──
137
179
  if ((m = trimmed.match(PATTERNS.boundary))) {
138
180
  return ok({
139
- ...base, verb: 'boundary', asset_a: m[1], asset_b: m[2],
181
+ ...base, verb: 'boundary', asset_a: resolveRef(m[1]), asset_b: resolveRef(m[2]),
140
182
  id: m[3], description: desc(m[4]),
141
183
  });
142
184
  }
143
185
  // ── @boundary pipe shorthand: @boundary A | B ──
144
186
  if ((m = trimmed.match(PATTERNS.boundary_pipe))) {
145
187
  return ok({
146
- ...base, verb: 'boundary', asset_a: m[1], asset_b: m[2],
188
+ ...base, verb: 'boundary', asset_a: resolveRef(m[1]), asset_b: resolveRef(m[2]),
147
189
  id: m[3], description: desc(m[4]),
148
190
  });
149
191
  }
150
192
  // ── @connects (v1 → flows) ──
151
193
  if ((m = trimmed.match(PATTERNS.connects_v1))) {
152
194
  return ok({
153
- ...base, verb: 'flows', source: m[1], target: m[2], description: desc(m[3]),
195
+ ...base, verb: 'flows', source: resolveRef(m[1]), target: resolveRef(m[2]), description: desc(m[3]),
154
196
  });
155
197
  }
156
198
  // ── @validates ──
157
199
  if ((m = trimmed.match(PATTERNS.validates))) {
158
- return ok({ ...base, verb: 'validates', control: resolveRef(m[1]), asset: m[2], description: desc(m[3]) });
200
+ return ok({ ...base, verb: 'validates', control: resolveRef(m[1]), asset: resolveRef(m[2]), description: desc(m[3]) });
159
201
  }
160
202
  // ── @audit / @review (v1) ──
161
203
  if ((m = trimmed.match(PATTERNS.audit)) || (m = trimmed.match(PATTERNS.review_v1))) {
162
- return ok({ ...base, verb: 'audit', asset: m[1], description: desc(m[2]) });
204
+ return ok({ ...base, verb: 'audit', asset: resolveRef(m[1]), description: desc(m[2]) });
163
205
  }
164
206
  // ── @owns ──
165
207
  if ((m = trimmed.match(PATTERNS.owns))) {
166
- return ok({ ...base, verb: 'owns', owner: m[1], asset: m[2], description: desc(m[3]) });
208
+ return ok({ ...base, verb: 'owns', owner: m[1], asset: resolveRef(m[2]), description: desc(m[3]) });
167
209
  }
168
210
  // ── @handles ──
169
211
  if ((m = trimmed.match(PATTERNS.handles))) {
170
212
  return ok({
171
213
  ...base, verb: 'handles',
172
214
  classification: m[1].toLowerCase(),
173
- asset: m[2], description: desc(m[3]),
215
+ asset: resolveRef(m[2]), description: desc(m[3]),
174
216
  });
175
217
  }
176
218
  // ── @assumes ──
177
219
  if ((m = trimmed.match(PATTERNS.assumes))) {
178
- return ok({ ...base, verb: 'assumes', asset: m[1], description: desc(m[2]) });
220
+ return ok({ ...base, verb: 'assumes', asset: resolveRef(m[1]), description: desc(m[2]) });
221
+ }
222
+ // ── @feature ──
223
+ if ((m = trimmed.match(PATTERNS.feature))) {
224
+ return ok({ ...base, verb: 'feature', feature: unescapeDescription(m[1]), description: desc(m[2]) });
179
225
  }
180
226
  // ── @comment ──
181
227
  if ((m = trimmed.match(PATTERNS.comment))) {
182
228
  return ok({ ...base, verb: 'comment', description: desc(m[1]) });
183
229
  }
230
+ // ── @source ──
231
+ if ((m = trimmed.match(PATTERNS.source))) {
232
+ return {
233
+ annotation: null,
234
+ diagnostic: null,
235
+ isContinuation: false,
236
+ sourceDirective: {
237
+ file: m[1],
238
+ line: Number(m[2]),
239
+ symbol: m[3] || undefined,
240
+ },
241
+ };
242
+ }
184
243
  // ── @shield ──
185
244
  if ((m = trimmed.match(PATTERNS.shield_begin))) {
186
245
  return ok({ ...base, verb: 'shield:begin', description: desc(m[1]) });
@@ -195,9 +254,9 @@ export function parseLine(text, location) {
195
254
  const verbMatch = trimmed.match(/^@(\S+)/);
196
255
  if (verbMatch) {
197
256
  const knownVerbs = new Set([
198
- 'asset', 'threat', 'control', 'mitigates', 'exposes', 'accepts',
257
+ 'asset', 'threat', 'control', 'mitigates', 'exposes', 'confirmed', 'accepts',
199
258
  'transfers', 'flows', 'boundary', 'validates', 'audit', 'owns',
200
- 'handles', 'assumes', 'comment', 'shield', 'shield:begin', 'shield:end',
259
+ 'handles', 'assumes', 'feature', 'source', 'comment', 'shield', 'shield:begin', 'shield:end',
201
260
  // v1 compat
202
261
  'review', 'connects',
203
262
  ]);
@@ -216,11 +275,26 @@ export function parseLine(text, location) {
216
275
  }
217
276
  }
218
277
  // Not a GuardLink annotation (could be @param, @returns, etc.)
219
- return { annotation: null, diagnostic: null, isContinuation: false };
278
+ return { annotation: null, diagnostic: null, isContinuation: false, sourceDirective: null };
220
279
  }
221
280
  // ─── Helpers ─────────────────────────────────────────────────────────
222
281
  function ok(annotation) {
223
- return { annotation, diagnostic: null, isContinuation: false };
282
+ return { annotation, diagnostic: null, isContinuation: false, sourceDirective: null };
283
+ }
284
+ /** Like ok(), but for parser branches that emit multiple annotations from
285
+ * one line (currently only multi-hop @flows chains). The first annotation
286
+ * becomes the primary `annotation`; the remainder go in `extraAnnotations`
287
+ * so the call site can push them all and update lastAnnotation correctly. */
288
+ function okMulti(annotations) {
289
+ if (annotations.length === 0) {
290
+ return { annotation: null, diagnostic: null, isContinuation: false };
291
+ }
292
+ return {
293
+ annotation: annotations[0],
294
+ extraAnnotations: annotations.length > 1 ? annotations.slice(1) : undefined,
295
+ diagnostic: null,
296
+ isContinuation: false,
297
+ };
224
298
  }
225
299
  function desc(raw) {
226
300
  if (!raw)