@stacksjs/defaults 0.75.361 → 0.75.366

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 (210) hide show
  1. package/ai/skills/stacks-ai/SKILL.md +22 -0
  2. package/ai/skills/stacks-native/CATALOG.md +5 -5
  3. package/ai/skills/stacks-payments/SKILL.md +10 -0
  4. package/ai/skills/stacks-technical-diagrams/LICENSE +202 -22
  5. package/ai/skills/stacks-technical-diagrams/LICENSE-MIT +22 -0
  6. package/ai/skills/stacks-technical-diagrams/NOTICE +26 -0
  7. package/ai/skills/stacks-technical-diagrams/SKILL.md +106 -266
  8. package/ai/skills/stacks-technical-diagrams/THIRD_PARTY_NOTICES.md +11 -2
  9. package/ai/skills/stacks-technical-diagrams/UPSTREAM.json +12 -0
  10. package/ai/skills/stacks-technical-diagrams/agents/openai.yaml +2 -2
  11. package/ai/skills/stacks-technical-diagrams/assets/template.html +3655 -3798
  12. package/ai/skills/stacks-technical-diagrams/bin/archify.mjs +7290 -0
  13. package/ai/skills/stacks-technical-diagrams/bin/delivery-update.mjs +7 -0
  14. package/ai/skills/stacks-technical-diagrams/bin/diagrams +1 -0
  15. package/ai/skills/stacks-technical-diagrams/bin/finalize.mjs +1056 -0
  16. package/ai/skills/stacks-technical-diagrams/bin/open-artifact.mjs +72 -12
  17. package/ai/skills/stacks-technical-diagrams/bin/preview.mjs +348 -31
  18. package/ai/skills/stacks-technical-diagrams/bin/recover-output.mjs +41 -0
  19. package/ai/skills/stacks-technical-diagrams/bin/technical-diagrams.mjs +3 -2097
  20. package/ai/skills/stacks-technical-diagrams/bin/visual-check.mjs +2142 -157
  21. package/ai/skills/stacks-technical-diagrams/brand-marks/README.md +3 -3
  22. package/ai/skills/stacks-technical-diagrams/delta/architecture-delta.mjs +130 -33
  23. package/ai/skills/stacks-technical-diagrams/examples/agent-run.lifecycle.json +25 -30
  24. package/ai/skills/stacks-technical-diagrams/examples/agent-tool-call.workflow.json +0 -20
  25. package/ai/skills/stacks-technical-diagrams/examples/archify-dev-activity.timeline.json +176 -0
  26. package/ai/skills/stacks-technical-diagrams/examples/archify-repository.tree.json +197 -0
  27. package/ai/skills/stacks-technical-diagrams/examples/async-job-roundtrip.sequence.json +1 -6
  28. package/ai/skills/stacks-technical-diagrams/examples/brand-aware-delivery.architecture.json +3 -16
  29. package/ai/skills/stacks-technical-diagrams/examples/cache-miss-request.sequence.json +2 -7
  30. package/ai/skills/stacks-technical-diagrams/examples/checkout-platform.base.architecture.json +2 -1
  31. package/ai/skills/stacks-technical-diagrams/examples/checkout-platform.head.architecture.json +2 -1
  32. package/ai/skills/stacks-technical-diagrams/examples/checkout-request.waterfall.json +20 -0
  33. package/ai/skills/stacks-technical-diagrams/examples/class-payment-processors-rendered.html +14987 -0
  34. package/ai/skills/stacks-technical-diagrams/examples/class-payments-rendered.html +14787 -0
  35. package/ai/skills/stacks-technical-diagrams/examples/dataflow-product-analytics.html +14880 -0
  36. package/ai/skills/stacks-technical-diagrams/examples/delivery.explainer.json +155 -0
  37. package/ai/skills/stacks-technical-diagrams/examples/deployment-release.lifecycle.json +57 -34
  38. package/ai/skills/stacks-technical-diagrams/examples/erd-orders-rendered.html +15112 -0
  39. package/ai/skills/stacks-technical-diagrams/examples/event-stream.dataflow.json +1 -6
  40. package/ai/skills/stacks-technical-diagrams/examples/example-rebuild.waterfall.json +212 -0
  41. package/ai/skills/stacks-technical-diagrams/examples/holiday-planning.workflow.json +143 -0
  42. package/ai/skills/stacks-technical-diagrams/examples/incident-response.workflow.json +1 -6
  43. package/ai/skills/stacks-technical-diagrams/examples/lifecycle-agent-run.html +14892 -0
  44. package/ai/skills/stacks-technical-diagrams/examples/locales/de.json +463 -0
  45. package/ai/skills/stacks-technical-diagrams/examples/locales/fr.json +463 -0
  46. package/ai/skills/stacks-technical-diagrams/examples/locales/fr.partial.json +22 -0
  47. package/ai/skills/stacks-technical-diagrams/examples/locales/it.json +463 -0
  48. package/ai/skills/stacks-technical-diagrams/examples/locales/ja.json +463 -0
  49. package/ai/skills/stacks-technical-diagrams/examples/locales/pt.json +463 -0
  50. package/ai/skills/stacks-technical-diagrams/examples/locales/ru.json +463 -0
  51. package/ai/skills/stacks-technical-diagrams/examples/orders.erd.json +337 -0
  52. package/ai/skills/stacks-technical-diagrams/examples/payment-incident.timeline.json +24 -0
  53. package/ai/skills/stacks-technical-diagrams/examples/payment-platform.tree.json +22 -0
  54. package/ai/skills/stacks-technical-diagrams/examples/payment-processors.class.json +177 -0
  55. package/ai/skills/stacks-technical-diagrams/examples/payments.class.json +85 -0
  56. package/ai/skills/stacks-technical-diagrams/examples/product-analytics.dataflow.json +1 -6
  57. package/ai/skills/stacks-technical-diagrams/examples/production-deployment.architecture.json +3 -23
  58. package/ai/skills/stacks-technical-diagrams/examples/release-delivery.workflow.json +1 -6
  59. package/ai/skills/stacks-technical-diagrams/examples/sequence-cache-miss-request.html +14895 -0
  60. package/ai/skills/stacks-technical-diagrams/examples/subscription-billing-rendered.html +15369 -0
  61. package/ai/skills/stacks-technical-diagrams/examples/subscription-billing.erd.json +548 -0
  62. package/ai/skills/stacks-technical-diagrams/examples/timeline-archify-dev-activity-rendered.html +15002 -0
  63. package/ai/skills/stacks-technical-diagrams/examples/timeline-payment-incident-rendered.html +14772 -0
  64. package/ai/skills/stacks-technical-diagrams/examples/tree-archify-repository-rendered.html +15071 -0
  65. package/ai/skills/stacks-technical-diagrams/examples/tree-payment-platform-rendered.html +14810 -0
  66. package/ai/skills/stacks-technical-diagrams/examples/waterfall-checkout-request-rendered.html +14754 -0
  67. package/ai/skills/stacks-technical-diagrams/examples/waterfall-example-rebuild-rendered.html +14940 -0
  68. package/ai/skills/stacks-technical-diagrams/examples/web-app-rendered.html +14889 -0
  69. package/ai/skills/stacks-technical-diagrams/examples/web-app.architecture.json +1 -6
  70. package/ai/skills/stacks-technical-diagrams/examples/workflow-agent-tool-call-rendered.html +14907 -0
  71. package/ai/skills/stacks-technical-diagrams/explainer/browser-check.mjs +208 -0
  72. package/ai/skills/stacks-technical-diagrams/explainer/charts.mjs +70 -0
  73. package/ai/skills/stacks-technical-diagrams/explainer/check.mjs +168 -0
  74. package/ai/skills/stacks-technical-diagrams/explainer/diagrams.mjs +518 -0
  75. package/ai/skills/stacks-technical-diagrams/explainer/diff.mjs +124 -0
  76. package/ai/skills/stacks-technical-diagrams/explainer/inline.mjs +155 -0
  77. package/ai/skills/stacks-technical-diagrams/explainer/layout.mjs +35 -0
  78. package/ai/skills/stacks-technical-diagrams/explainer/page-bundle.mjs +16 -0
  79. package/ai/skills/stacks-technical-diagrams/explainer/parse.mjs +300 -0
  80. package/ai/skills/stacks-technical-diagrams/explainer/patch.mjs +117 -0
  81. package/ai/skills/stacks-technical-diagrams/explainer/prepare.mjs +131 -0
  82. package/ai/skills/stacks-technical-diagrams/explainer/reader.css +472 -0
  83. package/ai/skills/stacks-technical-diagrams/explainer/reader.js +667 -0
  84. package/ai/skills/stacks-technical-diagrams/explainer/render.mjs +224 -0
  85. package/ai/skills/stacks-technical-diagrams/explainer/revision.mjs +20 -0
  86. package/ai/skills/stacks-technical-diagrams/explainer/sources.mjs +318 -0
  87. package/ai/skills/stacks-technical-diagrams/explainer/validate.mjs +138 -0
  88. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/LICENSE +40 -0
  89. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/README.md +5 -0
  90. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/3DKXF73K.js +4 -0
  91. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/42UTT654.js +3 -0
  92. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/DKTLEL5E.js +1 -0
  93. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/EEIX5ZYH.js +2 -0
  94. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/EJ5KOXYD.js +25 -0
  95. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/GUJTEFZK.js +1 -0
  96. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/IZOKQ2GT.js +4 -0
  97. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/L4LY553T.js +3 -0
  98. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/MSKIJHB2.js +2 -0
  99. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/RVARI57O.js +1 -0
  100. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/UKSIWDDN.js +2 -0
  101. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/UPTXNAXN.js +2 -0
  102. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/XTDUIGUI.js +9 -0
  103. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/YAYTP6L5.js +9 -0
  104. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/core.js +1 -0
  105. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/diagrams/pie.js +1 -0
  106. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/diagrams/quadrantChart.js +1 -0
  107. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/diagrams/radar.js +1 -0
  108. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/diagrams/sankey.js +1 -0
  109. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/diagrams/venn.js +1 -0
  110. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/diagrams/xychart.js +1 -0
  111. package/ai/skills/stacks-technical-diagrams/explainer/writing.mjs +139 -0
  112. package/ai/skills/stacks-technical-diagrams/locales/README.md +40 -0
  113. package/ai/skills/stacks-technical-diagrams/locales/ar.json +488 -0
  114. package/ai/skills/stacks-technical-diagrams/locales/en.json +488 -0
  115. package/ai/skills/stacks-technical-diagrams/locales/es.json +488 -0
  116. package/ai/skills/stacks-technical-diagrams/locales/ko.json +488 -0
  117. package/ai/skills/stacks-technical-diagrams/locales/manifest.json +10 -0
  118. package/ai/skills/stacks-technical-diagrams/locales/zh-CN.json +488 -0
  119. package/ai/skills/stacks-technical-diagrams/locales/zh-TW.json +488 -0
  120. package/ai/skills/stacks-technical-diagrams/migrations/workflow-v2.mjs +1 -0
  121. package/ai/skills/stacks-technical-diagrams/recipes/scenarios.mjs +115 -51
  122. package/ai/skills/stacks-technical-diagrams/references/architecture-layout-repair.md +18 -0
  123. package/ai/skills/stacks-technical-diagrams/references/authoring-contract.md +453 -51
  124. package/ai/skills/stacks-technical-diagrams/references/authoring-defaults.md +57 -0
  125. package/ai/skills/stacks-technical-diagrams/references/brand-marks.md +57 -3
  126. package/ai/skills/stacks-technical-diagrams/references/delivery-contract.md +603 -58
  127. package/ai/skills/stacks-technical-diagrams/references/explainer-authoring.md +274 -0
  128. package/ai/skills/stacks-technical-diagrams/references/repository-authoring.md +126 -0
  129. package/ai/skills/stacks-technical-diagrams/references/update-awareness.md +13 -0
  130. package/ai/skills/stacks-technical-diagrams/references/viewer-runtime.md +18 -11
  131. package/ai/skills/stacks-technical-diagrams/renderers/architecture/grid.mjs +1 -1
  132. package/ai/skills/stacks-technical-diagrams/renderers/architecture/labels.mjs +2 -0
  133. package/ai/skills/stacks-technical-diagrams/renderers/architecture/render-architecture.mjs +550 -424
  134. package/ai/skills/stacks-technical-diagrams/renderers/architecture/routing.mjs +1469 -0
  135. package/ai/skills/stacks-technical-diagrams/renderers/class/README.md +129 -0
  136. package/ai/skills/stacks-technical-diagrams/renderers/class/render-class.mjs +1095 -0
  137. package/ai/skills/stacks-technical-diagrams/renderers/dataflow/README.md +89 -17
  138. package/ai/skills/stacks-technical-diagrams/renderers/dataflow/dataflow-compiler.mjs +1605 -0
  139. package/ai/skills/stacks-technical-diagrams/renderers/dataflow/joint-label-routing.mjs +165 -0
  140. package/ai/skills/stacks-technical-diagrams/renderers/dataflow/label-capacity.mjs +81 -0
  141. package/ai/skills/stacks-technical-diagrams/renderers/dataflow/render-dataflow.mjs +14 -474
  142. package/ai/skills/stacks-technical-diagrams/renderers/erd/README.md +246 -0
  143. package/ai/skills/stacks-technical-diagrams/renderers/erd/grid.mjs +150 -0
  144. package/ai/skills/stacks-technical-diagrams/renderers/erd/render-erd.mjs +1588 -0
  145. package/ai/skills/stacks-technical-diagrams/renderers/explainer/render-explainer.mjs +42 -0
  146. package/ai/skills/stacks-technical-diagrams/renderers/lifecycle/README.md +83 -94
  147. package/ai/skills/stacks-technical-diagrams/renderers/lifecycle/render-lifecycle.mjs +990 -417
  148. package/ai/skills/stacks-technical-diagrams/renderers/sequence/README.md +49 -17
  149. package/ai/skills/stacks-technical-diagrams/renderers/sequence/render-sequence.mjs +735 -94
  150. package/ai/skills/stacks-technical-diagrams/renderers/shared/atomic-output.mjs +1982 -0
  151. package/ai/skills/stacks-technical-diagrams/renderers/shared/automatic-labels.mjs +290 -0
  152. package/ai/skills/stacks-technical-diagrams/renderers/shared/brand-marks.mjs +169 -26
  153. package/ai/skills/stacks-technical-diagrams/renderers/shared/cli.mjs +266 -54
  154. package/ai/skills/stacks-technical-diagrams/renderers/shared/desktop-readability.mjs +106 -0
  155. package/ai/skills/stacks-technical-diagrams/renderers/shared/diagnostics.mjs +0 -0
  156. package/ai/skills/stacks-technical-diagrams/renderers/shared/generated-validators.mjs +268 -2
  157. package/ai/skills/stacks-technical-diagrams/renderers/shared/geometry.mjs +774 -124
  158. package/ai/skills/stacks-technical-diagrams/renderers/shared/i18n.mjs +289 -556
  159. package/ai/skills/stacks-technical-diagrams/renderers/shared/layout-report.mjs +18 -1
  160. package/ai/skills/stacks-technical-diagrams/renderers/shared/legend.mjs +16 -6
  161. package/ai/skills/stacks-technical-diagrams/renderers/shared/node-card-style.mjs +32 -0
  162. package/ai/skills/stacks-technical-diagrams/renderers/shared/output-path.mjs +329 -229
  163. package/ai/skills/stacks-technical-diagrams/renderers/shared/owned-temp-directory.mjs +82 -0
  164. package/ai/skills/stacks-technical-diagrams/renderers/shared/path-semantics.mjs +1017 -0
  165. package/ai/skills/stacks-technical-diagrams/renderers/shared/portable-path.mjs +408 -0
  166. package/ai/skills/stacks-technical-diagrams/renderers/shared/repository-evidence.mjs +235 -79
  167. package/ai/skills/stacks-technical-diagrams/renderers/shared/repository-location.mjs +17 -4
  168. package/ai/skills/stacks-technical-diagrams/renderers/shared/route-quality.mjs +670 -0
  169. package/ai/skills/stacks-technical-diagrams/renderers/shared/sidecar-path.mjs +38 -0
  170. package/ai/skills/stacks-technical-diagrams/renderers/shared/software-notices.mjs +6 -0
  171. package/ai/skills/stacks-technical-diagrams/renderers/shared/spatial-grid.mjs +66 -0
  172. package/ai/skills/stacks-technical-diagrams/renderers/shared/svg-path-analysis.mjs +135 -0
  173. package/ai/skills/stacks-technical-diagrams/renderers/shared/text-fit.mjs +59 -3
  174. package/ai/skills/stacks-technical-diagrams/renderers/shared/utils.mjs +63 -35
  175. package/ai/skills/stacks-technical-diagrams/renderers/shared/validator.mjs +2 -5
  176. package/ai/skills/stacks-technical-diagrams/renderers/timeline/README.md +76 -0
  177. package/ai/skills/stacks-technical-diagrams/renderers/timeline/render-timeline.mjs +576 -0
  178. package/ai/skills/stacks-technical-diagrams/renderers/tree/README.md +96 -0
  179. package/ai/skills/stacks-technical-diagrams/renderers/tree/render-tree.mjs +573 -0
  180. package/ai/skills/stacks-technical-diagrams/renderers/waterfall/README.md +73 -0
  181. package/ai/skills/stacks-technical-diagrams/renderers/waterfall/render-waterfall.mjs +413 -0
  182. package/ai/skills/stacks-technical-diagrams/renderers/workflow/README.md +137 -23
  183. package/ai/skills/stacks-technical-diagrams/renderers/workflow/render-workflow.mjs +18 -6
  184. package/ai/skills/stacks-technical-diagrams/renderers/workflow/workflow-compiler.mjs +2230 -710
  185. package/ai/skills/stacks-technical-diagrams/renderers/workflow/workflow-migration-geometry.mjs +17 -0
  186. package/ai/skills/stacks-technical-diagrams/renderers/workflow/workflow-placement-repair.mjs +94 -0
  187. package/ai/skills/stacks-technical-diagrams/schemas/README.md +87 -40
  188. package/ai/skills/stacks-technical-diagrams/schemas/architecture.schema.json +7 -33
  189. package/ai/skills/stacks-technical-diagrams/schemas/class.schema.json +345 -0
  190. package/ai/skills/stacks-technical-diagrams/schemas/common.schema.json +68 -2
  191. package/ai/skills/stacks-technical-diagrams/schemas/dataflow.schema.json +14 -3
  192. package/ai/skills/stacks-technical-diagrams/schemas/erd.schema.json +381 -0
  193. package/ai/skills/stacks-technical-diagrams/schemas/explainer.schema.json +927 -0
  194. package/ai/skills/stacks-technical-diagrams/schemas/lifecycle.schema.json +23 -110
  195. package/ai/skills/stacks-technical-diagrams/schemas/sequence.schema.json +15 -4
  196. package/ai/skills/stacks-technical-diagrams/schemas/timeline.schema.json +160 -0
  197. package/ai/skills/stacks-technical-diagrams/schemas/tree.schema.json +141 -0
  198. package/ai/skills/stacks-technical-diagrams/schemas/waterfall.schema.json +147 -0
  199. package/ai/skills/stacks-technical-diagrams/schemas/workflow.schema.json +14 -3
  200. package/ai/skills/stacks-technical-diagrams/scripts/check-render-output.mjs +876 -243
  201. package/ai/skills/stacks-technical-diagrams/scripts/check-update.mjs +1717 -0
  202. package/ai/skills/stacks-technical-diagrams/scripts/delivery-update-child.mjs +26 -0
  203. package/ai/skills/stacks-technical-diagrams/scripts/generate-brand-marks.mjs +141 -0
  204. package/ai/skills/stacks-technical-diagrams/scripts/generate-validators.mjs +105 -0
  205. package/ai/skills/stacks-technical-diagrams/scripts/render-examples.mjs +11 -1
  206. package/ai/skills/stacks-technical-diagrams/scripts/update-contract.mjs +187 -0
  207. package/ai/skills/stacks-technical-diagrams/skill-release.json +10 -0
  208. package/ai/skills/stacks-testing/SKILL.md +8 -1
  209. package/ide/vscode/package.json +1 -1
  210. package/package.json +2 -2
@@ -1,11 +1,13 @@
1
1
  import { spawnSync } from 'node:child_process';
2
2
  import fs from 'node:fs';
3
3
  import path from 'node:path';
4
- import { throwDiagnosticError } from './diagnostics.mjs';
5
- import { parseRepositoryRemote, redactRepositoryRemote, repositorySourceHref } from './repository-location.mjs';
4
+ import { throwDiagnosticError, withDiagnosticRecordingSuppressed } from './diagnostics.mjs';
5
+ import { sameEntry } from './path-semantics.mjs';
6
+ import { parseRepositoryRemote, redactRepositoryRemote, repositorySourceHref, repositoryTreeHref } from './repository-location.mjs';
6
7
 
7
8
  const FULL_SHA_RE = /^[a-f0-9]{40}$/i;
8
9
  const CONTROL_CHARACTER_RE = /[\u0000-\u001f\u007f]/;
10
+ const MAX_SOURCE_BYTES = 16 * 1024 * 1024;
9
11
 
10
12
  function evidenceFailure(code, message, { subject = {}, evidence = {}, supportedFixes = [] } = {}) {
11
13
  throwDiagnosticError(message, [{
@@ -19,9 +21,12 @@ function evidenceFailure(code, message, { subject = {}, evidence = {}, supported
19
21
  }
20
22
 
21
23
  function runGit(repoRoot, args) {
22
- const result = spawnSync('git', ['-C', repoRoot, ...args], {
24
+ // 固定 SHA 的来源必须读取原始对象,不能使用本地 replacement refs 的替换内容。
25
+ const result = spawnSync('git', ['--no-replace-objects', '-C', repoRoot, ...args], {
23
26
  encoding: 'utf8',
24
- maxBuffer: 16 * 1024 * 1024,
27
+ maxBuffer: MAX_SOURCE_BYTES,
28
+ // Older Git may ignore NO_LAZY_FETCH; no transport is needed for local reads.
29
+ env: { ...process.env, GIT_NO_LAZY_FETCH: '1', GIT_ALLOW_PROTOCOL: '' },
25
30
  });
26
31
  if (result.error) evidenceFailure('repository-evidence/git-unavailable', `Could not run Git: ${result.error.message}`, {
27
32
  evidence: { reason: result.error.message },
@@ -30,17 +35,70 @@ function runGit(repoRoot, args) {
30
35
  return result;
31
36
  }
32
37
 
38
+ // Check types in one session, then read only blobs whose cited lines need
39
+ // verification. Path-only references never require loading the file contents.
40
+ function prefetchBlobs(repoRoot, objectNeedsContent) {
41
+ const blobs = readBatchObjects(repoRoot, [...objectNeedsContent.keys()], false);
42
+ if (!blobs) return null;
43
+ const readable = [...objectNeedsContent].filter(([object, needsContent]) => {
44
+ const blob = blobs.get(object);
45
+ return needsContent && blob?.type === 'blob' && blob.size <= MAX_SOURCE_BYTES;
46
+ }).map(([object]) => object);
47
+ const contents = readBatchObjects(repoRoot, readable, true);
48
+ if (contents) for (const [object, blob] of contents) blobs.set(object, blob);
49
+ // Failed or oversized reads fall back in source order to the original
50
+ // per-file path, preserving its size limit and diagnostic behavior.
51
+ return blobs;
52
+ }
53
+
54
+ function readBatchObjects(repoRoot, objects, includeContent) {
55
+ if (!objects.length) return new Map();
56
+ const mode = includeContent ? '--batch' : '--batch-check';
57
+ const result = spawnSync('git', ['--no-replace-objects', '-C', repoRoot, 'cat-file', mode], {
58
+ input: objects.join('\n') + '\n',
59
+ maxBuffer: 64 * 1024 * 1024,
60
+ env: { ...process.env, GIT_NO_LAZY_FETCH: '1', GIT_ALLOW_PROTOCOL: '' },
61
+ });
62
+ if (result.error || result.status !== 0 || !Buffer.isBuffer(result.stdout)) return null;
63
+ const buffer = result.stdout;
64
+ const blobs = new Map();
65
+ let cursor = 0;
66
+ for (const object of objects) {
67
+ const newline = buffer.indexOf(0x0a, cursor);
68
+ if (newline < 0) return null;
69
+ const header = buffer.toString('utf8', cursor, newline);
70
+ cursor = newline + 1;
71
+ if (header.endsWith(' missing')) {
72
+ blobs.set(object, { missing: true });
73
+ continue;
74
+ }
75
+ const parts = header.split(' ');
76
+ const size = Number(parts[2]);
77
+ if (parts.length !== 3 || !Number.isSafeInteger(size) || size < 0) return null;
78
+ const blob = { type: parts[1], size };
79
+ if (includeContent) {
80
+ if (cursor + size >= buffer.length || buffer[cursor + size] !== 0x0a) return null;
81
+ blob.content = buffer.toString('utf8', cursor, cursor + size);
82
+ cursor += size + 1;
83
+ }
84
+ blobs.set(object, blob);
85
+ }
86
+ return blobs;
87
+ }
88
+
33
89
  function gitValue(repoRoot, args, failure) {
34
90
  const result = runGit(repoRoot, args);
35
91
  if (result.status !== 0) evidenceFailure('repository-evidence/git-command', failure, {
36
92
  evidence: { gitArgs: args, exitCode: result.status },
37
93
  supportedFixes: ['use the intended local Git repository and verify its origin and revision'],
38
94
  });
39
- return result.stdout.trim();
95
+ // Remove Git's record terminator without changing whitespace in a path.
96
+ return result.stdout.replace(/\n$/, '');
40
97
  }
41
98
 
42
99
  function verifiedSourcePath(value, where) {
43
100
  const sourcePath = String(value || '');
101
+ // path-contract-allow: git-path -- Git tree entries use repository-relative POSIX syntax.
44
102
  if (!sourcePath || sourcePath.startsWith('/') || sourcePath.includes('\\') || CONTROL_CHARACTER_RE.test(sourcePath)) {
45
103
  evidenceFailure('repository-evidence/path-invalid', `${where} must be a repo-relative POSIX path.`, {
46
104
  subject: { path: where },
@@ -65,23 +123,43 @@ function sourceLineCount(content) {
65
123
  return lines.length - (/(?:\r\n|\n|\r)$/.test(content) ? 1 : 0);
66
124
  }
67
125
 
126
+ // Every diagram type carries its nodes under a different property name, and
127
+ // source evidence is authored on those nodes. One table keeps the verification
128
+ // below identical for all five types instead of branching per type: the only
129
+ // per-type fact is which array to read and which JSON pointer to quote back.
130
+ const EVIDENCE_NODE_COLLECTIONS = {
131
+ architecture: 'components',
132
+ workflow: 'nodes',
133
+ sequence: 'participants',
134
+ dataflow: 'nodes',
135
+ lifecycle: 'states',
136
+ erd: 'entities',
137
+ tree: 'nodes',
138
+ class: 'types',
139
+ timeline: 'events',
140
+ waterfall: 'spans',
141
+ };
142
+
143
+ function evidenceNodes(diagramType, diagram) {
144
+ const collection = EVIDENCE_NODE_COLLECTIONS[diagramType];
145
+ if (!collection) return null;
146
+ return { collection, nodes: Array.isArray(diagram?.[collection]) ? diagram[collection] : [] };
147
+ }
148
+
68
149
  export function hasRepositoryEvidence(diagramType, diagram) {
69
- if (diagramType !== 'architecture') return false;
70
- const components = Array.isArray(diagram?.components) ? diagram.components : [];
71
- return Boolean(diagram?.meta?.repository) || components.some((component) => Array.isArray(component?.sources) && component.sources.length);
150
+ const authored = evidenceNodes(diagramType, diagram);
151
+ if (!authored) return false;
152
+ return Boolean(diagram?.meta?.repository) || authored.nodes.some((node) => Array.isArray(node?.sources) && node.sources.length);
72
153
  }
73
154
 
74
155
  export function verifyRepositoryEvidence(diagramType, diagram, repoRootInput) {
75
156
  if (!hasRepositoryEvidence(diagramType, diagram)) return null;
76
- if (diagramType !== 'architecture') evidenceFailure('repository-evidence/type-unsupported', 'Repository evidence is currently supported for architecture diagrams only.', {
77
- subject: { diagramType },
78
- supportedFixes: ['use architecture mode or remove repository evidence'],
79
- });
157
+ const { collection, nodes: authoredNodes } = evidenceNodes(diagramType, diagram);
80
158
 
81
159
  const repository = diagram.meta?.repository;
82
160
  if (!repository) evidenceFailure('repository-evidence/repository-required', 'Repository evidence requires /meta/repository.', {
83
- subject: { path: '/meta/repository' },
84
- supportedFixes: ['add the pinned repository metadata or remove component sources'],
161
+ subject: { path: '/meta/repository', diagramType, collection },
162
+ supportedFixes: [`add the pinned repository metadata or remove /${collection} sources`],
85
163
  });
86
164
  if (!FULL_SHA_RE.test(repository.revision || '')) {
87
165
  evidenceFailure('repository-evidence/revision-invalid', '/meta/repository/revision must be a full 40-character commit SHA.', {
@@ -90,29 +168,35 @@ export function verifyRepositoryEvidence(diagramType, diagram, repoRootInput) {
90
168
  supportedFixes: ['pin one full 40-character commit SHA'],
91
169
  });
92
170
  }
93
- const location = parseRepositoryRemote(repository.url, { authored: true });
171
+ const location = parseRepositoryRemote(repository.url, { authored: true, provider: repository.provider });
94
172
  if (!location) {
173
+ // A filesystem path is the common authoring mistake: the field carries the
174
+ // remote origin identity, which `git remote get-url origin` reports.
175
+ const filesystemPath = /^(?:[\\/]|~|\.{1,2}(?:[\\/]|$)|[A-Za-z]:[\\/])/.test(String(repository.url ?? ''));
95
176
  evidenceFailure('repository-evidence/url-invalid', '/meta/repository/url must be a credential-free HTTP(S) or Git SSH repository address without query, fragment, or dot segments.', {
96
177
  subject: { path: '/meta/repository/url' },
97
- supportedFixes: ['declare the matching repository address without credentials; use link_mode: local-only for internal repositories'],
178
+ evidence: filesystemPath ? { authoredValueLooksLike: 'local filesystem path; the expected value is the remote origin address' } : {},
179
+ supportedFixes: ['run `git remote get-url origin` inside --repo-root and declare that credential-free address', 'use link_mode: local-only for internal repositories'],
98
180
  });
99
181
  }
100
182
  const linkMode = repository.link_mode ?? 'web';
101
183
  if (!['web', 'local-only'].includes(linkMode)) evidenceFailure('repository-evidence/link-mode-invalid', 'Repository link_mode must be web or local-only.');
102
- if (repository.provider !== undefined && (!['github', 'gitee'].includes(repository.provider) || repository.provider !== location.provider)) {
103
- evidenceFailure('repository-evidence/provider-invalid', 'Repository provider must match its supported public host (github.com or gitee.com).', {
184
+ if (repository.provider !== undefined && (!['github', 'gitee', 'gitlab'].includes(repository.provider) || repository.provider !== location.provider)) {
185
+ evidenceFailure('repository-evidence/provider-invalid', 'Repository provider must match its supported public host (github.com, gitee.com or gitlab.com); gitlab also names a self-managed GitLab host.', {
104
186
  subject: { path: '/meta/repository/provider' },
105
187
  supportedFixes: ['use the matching provider or omit provider and select link_mode: local-only'],
106
188
  });
107
189
  }
108
- if (linkMode === 'web' && (!location.provider || location.protocol !== 'https:' || location.endpoint !== 'standard' || !/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(location.path))) {
109
- evidenceFailure('repository-evidence/links-unsupported', 'Web source links require a canonical GitHub or Gitee HTTPS owner/repository URL.', {
190
+ // GitLab projects may sit in nested groups; GitHub and Gitee are owner/repository.
191
+ const webPath = location.provider === 'gitlab' ? /^[A-Za-z0-9_.-]+(?:\/[A-Za-z0-9_.-]+)+$/ : /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/;
192
+ if (linkMode === 'web' && (!location.provider || location.protocol !== 'https:' || location.endpoint !== 'standard' || !webPath.test(location.path))) {
193
+ evidenceFailure('repository-evidence/links-unsupported', 'Web source links require a canonical GitHub, Gitee, or GitLab HTTPS repository URL.', {
110
194
  subject: { path: '/meta/repository/url' },
111
- supportedFixes: ['use a canonical GitHub or Gitee URL, or select link_mode: local-only to retain local verification without web links'],
195
+ supportedFixes: ['use a canonical GitHub, Gitee, or GitLab HTTPS URL; declare provider: gitlab for a self-managed GitLab host', 'select link_mode: local-only to retain local verification without web links'],
112
196
  });
113
197
  }
114
198
  if (!repoRootInput) {
115
- evidenceFailure('repository-evidence/root-required', 'This diagram declares source evidence. Pass --repo-root <repository> so the renderer can verify it before rendering.', {
199
+ evidenceFailure('repository-evidence/root-required', 'This diagram declares source evidence. Pass --repo-root <repository> so Archify can verify it before rendering.', {
116
200
  subject: { path: '/meta/repository' },
117
201
  supportedFixes: ['pass --repo-root with the matching local Git checkout'],
118
202
  });
@@ -130,15 +214,23 @@ export function verifyRepositoryEvidence(diagramType, diagram, repoRootInput) {
130
214
  });
131
215
  }
132
216
  const gitRoot = gitValue(realRoot, ['rev-parse', '--show-toplevel'], `Evidence root "${realRoot}" is not a Git repository.`);
133
- if (fs.realpathSync(gitRoot) !== realRoot) {
217
+ const rootIdentity = sameEntry(realRoot, gitRoot);
218
+ if (rootIdentity.status === 'unknown') {
219
+ evidenceFailure('repository-evidence/root-identity-indeterminate', 'Could not determine whether the evidence root is the Git top-level directory.', {
220
+ subject: { repoRoot: realRoot },
221
+ evidence: { gitTopLevel: gitRoot, relation: rootIdentity.reason },
222
+ supportedFixes: ['pass the readable Git top-level directory using its canonical filesystem path'],
223
+ });
224
+ }
225
+ if (rootIdentity.status === 'different') {
134
226
  evidenceFailure('repository-evidence/root-not-top-level', `Evidence root must be the Git top-level directory: ${gitRoot}`, {
135
227
  subject: { repoRoot: realRoot },
136
228
  evidence: { gitTopLevel: gitRoot },
137
229
  supportedFixes: [`pass --repo-root ${gitRoot}`],
138
230
  });
139
231
  }
140
- const origin = gitValue(realRoot, ['remote', 'get-url', 'origin'], 'Evidence repository must have an origin remote.');
141
- if (parseRepositoryRemote(origin)?.identity !== location.identity) {
232
+ const origin = gitValue(realRoot, ['remote', 'get-url', 'origin'], 'Evidence repository must have an origin remote.').trim();
233
+ if (parseRepositoryRemote(origin, { provider: repository.provider })?.identity !== location.identity) {
142
234
  const safeOrigin = redactRepositoryRemote(origin);
143
235
  evidenceFailure('repository-evidence/origin-mismatch', `Evidence repository origin ${JSON.stringify(safeOrigin)} does not match ${JSON.stringify(repository.url)}.`, {
144
236
  subject: { repoRoot: realRoot },
@@ -157,68 +249,132 @@ export function verifyRepositoryEvidence(diagramType, diagram, repoRootInput) {
157
249
  });
158
250
  }
159
251
 
252
+ // The batch is an optimization only: every path, line-range, file and line
253
+ // check still runs in source order in the verification loop below, so a
254
+ // citation the batch cannot answer for never reorders the diagnostics.
255
+ const citedObjects = new Map();
256
+ for (const [nodeIndex, node] of authoredNodes.entries()) {
257
+ if (!Array.isArray(node.sources) || node.sources.length === 0) continue;
258
+ for (const [sourceIndex, authored] of node.sources.entries()) {
259
+ const at = `/${collection}/${nodeIndex}/sources/${sourceIndex}`;
260
+ let sourcePath;
261
+ try {
262
+ sourcePath = withDiagnosticRecordingSuppressed(() => verifiedSourcePath(authored.path, `${at}/path`));
263
+ } catch {
264
+ continue;
265
+ }
266
+ const object = `${revision}:${sourcePath}`;
267
+ citedObjects.set(object, citedObjects.get(object) || Boolean(authored.line));
268
+ }
269
+ }
270
+ const prefetchedBlobs = prefetchBlobs(realRoot, citedObjects);
271
+
160
272
  const nodes = Object.create(null);
273
+ const sourceDiagnostics = [];
161
274
  let referenceCount = 0;
162
- const components = Array.isArray(diagram.components) ? diagram.components : [];
163
- for (const [componentIndex, component] of components.entries()) {
164
- if (!Array.isArray(component.sources) || component.sources.length === 0) continue;
275
+ for (const [nodeIndex, node] of authoredNodes.entries()) {
276
+ if (!Array.isArray(node.sources) || node.sources.length === 0) continue;
277
+ // `componentId` shipped with the architecture-only path; keep it beside the
278
+ // type-neutral `nodeId` so existing agent handling stays valid.
279
+ const nodeSubject = collection === 'components'
280
+ ? { diagramType, collection, nodeId: node.id, componentId: node.id }
281
+ : { diagramType, collection, nodeId: node.id };
165
282
  const verified = [];
166
- for (const [sourceIndex, authored] of component.sources.entries()) {
167
- const where = `/components/${componentIndex}/sources/${sourceIndex}/path`;
168
- const source = {
169
- path: verifiedSourcePath(authored.path, where),
170
- ...(authored.line ? { line: authored.line } : {}),
171
- ...(authored.end_line ? { endLine: authored.end_line } : {}),
172
- ...(authored.label ? { label: authored.label } : {}),
173
- };
174
- if (source.endLine && !source.line) {
175
- evidenceFailure('repository-evidence/line-required', `/components/${componentIndex}/sources/${sourceIndex}/end_line requires line.`, {
176
- subject: { path: `/components/${componentIndex}/sources/${sourceIndex}/end_line`, componentId: component.id },
177
- supportedFixes: ['add line or remove end_line'],
178
- });
179
- }
180
- if (source.endLine && source.endLine < source.line) {
181
- evidenceFailure('repository-evidence/line-range-invalid', `/components/${componentIndex}/sources/${sourceIndex}/end_line must be greater than or equal to line.`, {
182
- subject: { path: `/components/${componentIndex}/sources/${sourceIndex}`, componentId: component.id },
183
- evidence: { line: source.line, endLine: source.endLine },
184
- supportedFixes: ['use an end_line greater than or equal to line'],
185
- });
186
- }
187
- const object = `${revision}:${source.path}`;
188
- const type = runGit(realRoot, ['cat-file', '-t', object]);
189
- if (type.status !== 0 || type.stdout.trim() !== 'blob') {
190
- evidenceFailure('repository-evidence/file-missing', `${where} does not identify a file at revision ${revision}.`, {
191
- subject: { path: where, componentId: component.id },
192
- evidence: { sourcePath: source.path, revision },
193
- supportedFixes: ['use a file path that exists at the pinned revision'],
194
- });
195
- }
196
- if (source.line) {
197
- const content = runGit(realRoot, ['show', object]);
198
- if (content.status !== 0) evidenceFailure('repository-evidence/file-unreadable', `${where} could not be read at revision ${revision}.`, {
199
- subject: { path: where, componentId: component.id },
200
- evidence: { sourcePath: source.path, revision },
201
- supportedFixes: ['verify the pinned blob is readable in the local checkout'],
202
- });
203
- const lineCount = sourceLineCount(content.stdout);
204
- const requestedLine = source.endLine || source.line;
205
- if (requestedLine > lineCount) {
206
- evidenceFailure('repository-evidence/line-out-of-range', `/components/${componentIndex}/sources/${sourceIndex} requests line ${requestedLine}, but ${source.path} has ${lineCount} lines at revision ${revision}.`, {
207
- subject: { path: `/components/${componentIndex}/sources/${sourceIndex}`, componentId: component.id },
208
- evidence: { sourcePath: source.path, requestedLine, lineCount, revision },
209
- supportedFixes: ['use a line range that exists at the pinned revision'],
283
+ for (const [sourceIndex, authored] of node.sources.entries()) {
284
+ const at = `/${collection}/${nodeIndex}/sources/${sourceIndex}`;
285
+ const where = `${at}/path`;
286
+ try {
287
+ const source = {
288
+ path: verifiedSourcePath(authored.path, where),
289
+ ...(authored.line ? { line: authored.line } : {}),
290
+ ...(authored.end_line ? { endLine: authored.end_line } : {}),
291
+ ...(authored.label ? { label: authored.label } : {}),
292
+ };
293
+ if (source.endLine && !source.line) {
294
+ evidenceFailure('repository-evidence/line-required', `${at}/end_line requires line.`, {
295
+ subject: { path: `${at}/end_line`, ...nodeSubject },
296
+ supportedFixes: ['add line or remove end_line'],
297
+ });
298
+ }
299
+ if (source.endLine && source.endLine < source.line) {
300
+ evidenceFailure('repository-evidence/line-range-invalid', `${at}/end_line must be greater than or equal to line.`, {
301
+ subject: { path: at, ...nodeSubject },
302
+ evidence: { line: source.line, endLine: source.endLine },
303
+ supportedFixes: ['use an end_line greater than or equal to line'],
210
304
  });
211
305
  }
306
+ const object = `${revision}:${source.path}`;
307
+ const prefetched = prefetchedBlobs ? prefetchedBlobs.get(object) : undefined;
308
+ const objectIsBlob = prefetched
309
+ ? !prefetched.missing && prefetched.type === 'blob'
310
+ : (() => {
311
+ const type = runGit(realRoot, ['cat-file', '-t', object]);
312
+ return type.status === 0 && type.stdout.trim() === 'blob';
313
+ })();
314
+ if (!objectIsBlob) {
315
+ // A promisor blob can exist in the pinned tree without being stored
316
+ // locally. Do not tell the caller to change a correct source path.
317
+ const tree = runGit(realRoot, ['ls-tree', '-z', revision, '--', source.path]);
318
+ const knownBlob = tree.status === 0 && tree.stdout.split('\0').some((entry) => {
319
+ const separator = entry.indexOf('\t');
320
+ return entry.slice(separator + 1) === source.path
321
+ && /^\d+ blob [a-f0-9]+$/.test(entry.slice(0, separator));
322
+ });
323
+ if (knownBlob) {
324
+ evidenceFailure('repository-evidence/object-unavailable', `${where} exists at revision ${revision}, but its content is unavailable in the local repository.`, {
325
+ subject: { path: where, ...nodeSubject },
326
+ evidence: { sourcePath: source.path, revision },
327
+ supportedFixes: ['explicitly fetch the pinned source objects into the local checkout, then retry verification'],
328
+ });
329
+ }
330
+ evidenceFailure('repository-evidence/file-missing', `${where} does not identify a file at revision ${revision}.`, {
331
+ subject: { path: where, ...nodeSubject },
332
+ evidence: { sourcePath: source.path, revision },
333
+ supportedFixes: ['use a file path that exists at the pinned revision'],
334
+ });
335
+ }
336
+ if (source.line) {
337
+ const content = prefetched && Object.hasOwn(prefetched, 'content')
338
+ ? { status: 0, stdout: prefetched.content }
339
+ : runGit(realRoot, ['show', object]);
340
+ if (content.status !== 0) evidenceFailure('repository-evidence/file-unreadable', `${where} could not be read at revision ${revision}.`, {
341
+ subject: { path: where, ...nodeSubject },
342
+ evidence: { sourcePath: source.path, revision },
343
+ supportedFixes: ['verify the pinned blob is readable in the local checkout'],
344
+ });
345
+ const lineCount = sourceLineCount(content.stdout);
346
+ const requestedLine = source.endLine || source.line;
347
+ if (requestedLine > lineCount) {
348
+ evidenceFailure('repository-evidence/line-out-of-range', `${at} requests line ${requestedLine}, but ${source.path} has ${lineCount} lines at revision ${revision}.`, {
349
+ subject: { path: at, ...nodeSubject },
350
+ evidence: { sourcePath: source.path, requestedLine, lineCount, revision },
351
+ supportedFixes: ['use a line range that exists at the pinned revision'],
352
+ });
353
+ }
354
+ }
355
+ verified.push({ ...source, ...(linkMode === 'web' ? { href: repositorySourceHref(location.provider, location.url, revision, source) } : {}) });
356
+ referenceCount += 1;
357
+ } catch (error) {
358
+ // Independent citations can be repaired together. Keep the first
359
+ // diagnostic and source order; an invalid reference never verifies.
360
+ if (!error?.archifyDiagnostics?.length) throw error;
361
+ sourceDiagnostics.push(...error.archifyDiagnostics);
362
+ // An unavailable Git process or an oversized read is not a bad
363
+ // citation. Stop rather than repeat that failure for every source.
364
+ if (error.archifyDiagnostics.some(({ code }) => code === 'repository-evidence/git-unavailable')) {
365
+ throwDiagnosticError(sourceDiagnostics[0].message, sourceDiagnostics);
366
+ }
212
367
  }
213
- verified.push({ ...source, ...(linkMode === 'web' ? { href: repositorySourceHref(location.provider, location.url, revision, source) } : {}) });
214
- referenceCount += 1;
215
368
  }
216
- nodes[component.id] = verified;
369
+ nodes[node.id] = verified;
370
+ }
371
+ if (sourceDiagnostics.length) {
372
+ throwDiagnosticError(sourceDiagnostics[0].message, sourceDiagnostics);
217
373
  }
218
374
  if (referenceCount === 0) {
219
- evidenceFailure('repository-evidence/source-required', '/meta/repository requires at least one component source reference.', {
220
- subject: { path: '/meta/repository' },
221
- supportedFixes: ['add at least one verified component source or remove repository metadata'],
375
+ evidenceFailure('repository-evidence/source-required', `/meta/repository requires at least one /${collection} source reference.`, {
376
+ subject: { path: '/meta/repository', diagramType, collection },
377
+ supportedFixes: [`add at least one verified /${collection} source or remove repository metadata`],
222
378
  });
223
379
  }
224
380
 
@@ -230,7 +386,7 @@ export function verifyRepositoryEvidence(diagramType, diagram, repoRootInput) {
230
386
  revision,
231
387
  shortRevision: revision.slice(0, 7),
232
388
  label: location.provider === 'github' ? location.path : location.url.replace(/^(?:https?:\/\/|ssh:\/\/git@|git@)/, ''),
233
- ...(linkMode === 'web' ? { href: `${location.url}/tree/${revision}` } : { linkMode }),
389
+ ...(linkMode === 'web' ? { href: repositoryTreeHref(location.provider, location.url, revision) } : { linkMode }),
234
390
  },
235
391
  referenceCount,
236
392
  nodes,
@@ -1,6 +1,10 @@
1
1
  // Repository identity and forge links are independent of local Git object checks.
2
2
  // This module never contacts a remote server or reads the user's SSH config.
3
- export function parseRepositoryRemote(value, { authored = false } = {}) {
3
+ // Public forge hosts select their provider automatically. GitLab is also
4
+ // self-hosted, so an explicit `provider: "gitlab"` hint selects it on any host.
5
+ const PROVIDER_HOSTS = { 'github.com': 'github', 'gitee.com': 'gitee', 'gitlab.com': 'gitlab' };
6
+
7
+ export function parseRepositoryRemote(value, { authored = false, provider: providerHint } = {}) {
4
8
  if (typeof value !== 'string') return null;
5
9
  const raw = authored ? value : value.trim();
6
10
  if (!raw || /[\s\\\u0000-\u001f\u007f?#]/.test(raw)) return null;
@@ -25,7 +29,7 @@ export function parseRepositoryRemote(value, { authored = false } = {}) {
25
29
  if (authored && protocol !== 'ssh:' && (url.username || url.password)) return null;
26
30
  const hostname = url.hostname.toLowerCase();
27
31
  if (!hostname) return null;
28
- const provider = hostname === 'github.com' ? 'github' : hostname === 'gitee.com' ? 'gitee' : null;
32
+ const provider = Object.hasOwn(PROVIDER_HOSTS, hostname) ? PROVIDER_HOSTS[hostname] : providerHint === 'gitlab' ? 'gitlab' : null;
29
33
  const last = segments.length - 1;
30
34
  if (provider) segments[last] = segments[last].replace(provider === 'github' ? /\.git$/i : /\.git$/, '');
31
35
  if (!segments[last] || segments[last] === '.' || segments[last] === '..') return null;
@@ -36,7 +40,8 @@ export function parseRepositoryRemote(value, { authored = false } = {}) {
36
40
  const endpoint = provider && ((protocol === 'https:' && port === '443') || (protocol === 'ssh:' && port === '22'))
37
41
  ? 'standard' : `${protocol}${port}`;
38
42
  const pathKind = provider ? 'repository' : scp && !scpAbsolute ? 'relative' : 'absolute';
39
- const identityPath = provider === 'github' ? repositoryPath.toLowerCase() : repositoryPath;
43
+ // path-contract-allow: url-path -- GitHub and GitLab repository paths are case-insensitive URL identities.
44
+ const identityPath = provider === 'github' || provider === 'gitlab' ? repositoryPath.toLowerCase() : repositoryPath;
40
45
  const encodedPath = segments.map(encodeURIComponent).join('/');
41
46
  const canonicalUrl = scp ? `git@${hostname}:${scpAbsolute ? '/' : ''}${repositoryPath}`
42
47
  : `${protocol}//${protocol === 'ssh:' ? 'git@' : ''}${url.host}/${encodedPath}`;
@@ -54,5 +59,13 @@ export function repositorySourceHref(provider, url, revision, source) {
54
59
  const end = source.endLine && source.endLine !== source.line
55
60
  ? `-${provider === 'github' ? 'L' : ''}${source.endLine}` : '';
56
61
  const fragment = source.line ? `#L${source.line}${end}` : '';
57
- return `${url}/blob/${revision}/${encodedPath}${fragment}`;
62
+ if (provider !== 'gitlab') return `${url}/blob/${revision}/${encodedPath}${fragment}`;
63
+ // GitLab renders Markdown blobs, where line anchors select nothing; the
64
+ // plain view keeps the cited lines highlighted.
65
+ const plain = source.line && /\.(?:md|markdown)$/i.test(source.path) ? '?plain=1' : '';
66
+ return `${url}/-/blob/${revision}/${encodedPath}${plain}${fragment}`;
67
+ }
68
+
69
+ export function repositoryTreeHref(provider, url, revision) {
70
+ return `${url}/${provider === 'gitlab' ? '-/' : ''}tree/${revision}`;
58
71
  }