@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.
- package/ai/skills/stacks-ai/SKILL.md +22 -0
- package/ai/skills/stacks-native/CATALOG.md +5 -5
- package/ai/skills/stacks-payments/SKILL.md +10 -0
- package/ai/skills/stacks-technical-diagrams/LICENSE +202 -22
- package/ai/skills/stacks-technical-diagrams/LICENSE-MIT +22 -0
- package/ai/skills/stacks-technical-diagrams/NOTICE +26 -0
- package/ai/skills/stacks-technical-diagrams/SKILL.md +106 -266
- package/ai/skills/stacks-technical-diagrams/THIRD_PARTY_NOTICES.md +11 -2
- package/ai/skills/stacks-technical-diagrams/UPSTREAM.json +12 -0
- package/ai/skills/stacks-technical-diagrams/agents/openai.yaml +2 -2
- package/ai/skills/stacks-technical-diagrams/assets/template.html +3655 -3798
- package/ai/skills/stacks-technical-diagrams/bin/archify.mjs +7290 -0
- package/ai/skills/stacks-technical-diagrams/bin/delivery-update.mjs +7 -0
- package/ai/skills/stacks-technical-diagrams/bin/diagrams +1 -0
- package/ai/skills/stacks-technical-diagrams/bin/finalize.mjs +1056 -0
- package/ai/skills/stacks-technical-diagrams/bin/open-artifact.mjs +72 -12
- package/ai/skills/stacks-technical-diagrams/bin/preview.mjs +348 -31
- package/ai/skills/stacks-technical-diagrams/bin/recover-output.mjs +41 -0
- package/ai/skills/stacks-technical-diagrams/bin/technical-diagrams.mjs +3 -2097
- package/ai/skills/stacks-technical-diagrams/bin/visual-check.mjs +2142 -157
- package/ai/skills/stacks-technical-diagrams/brand-marks/README.md +3 -3
- package/ai/skills/stacks-technical-diagrams/delta/architecture-delta.mjs +130 -33
- package/ai/skills/stacks-technical-diagrams/examples/agent-run.lifecycle.json +25 -30
- package/ai/skills/stacks-technical-diagrams/examples/agent-tool-call.workflow.json +0 -20
- package/ai/skills/stacks-technical-diagrams/examples/archify-dev-activity.timeline.json +176 -0
- package/ai/skills/stacks-technical-diagrams/examples/archify-repository.tree.json +197 -0
- package/ai/skills/stacks-technical-diagrams/examples/async-job-roundtrip.sequence.json +1 -6
- package/ai/skills/stacks-technical-diagrams/examples/brand-aware-delivery.architecture.json +3 -16
- package/ai/skills/stacks-technical-diagrams/examples/cache-miss-request.sequence.json +2 -7
- package/ai/skills/stacks-technical-diagrams/examples/checkout-platform.base.architecture.json +2 -1
- package/ai/skills/stacks-technical-diagrams/examples/checkout-platform.head.architecture.json +2 -1
- package/ai/skills/stacks-technical-diagrams/examples/checkout-request.waterfall.json +20 -0
- package/ai/skills/stacks-technical-diagrams/examples/class-payment-processors-rendered.html +14987 -0
- package/ai/skills/stacks-technical-diagrams/examples/class-payments-rendered.html +14787 -0
- package/ai/skills/stacks-technical-diagrams/examples/dataflow-product-analytics.html +14880 -0
- package/ai/skills/stacks-technical-diagrams/examples/delivery.explainer.json +155 -0
- package/ai/skills/stacks-technical-diagrams/examples/deployment-release.lifecycle.json +57 -34
- package/ai/skills/stacks-technical-diagrams/examples/erd-orders-rendered.html +15112 -0
- package/ai/skills/stacks-technical-diagrams/examples/event-stream.dataflow.json +1 -6
- package/ai/skills/stacks-technical-diagrams/examples/example-rebuild.waterfall.json +212 -0
- package/ai/skills/stacks-technical-diagrams/examples/holiday-planning.workflow.json +143 -0
- package/ai/skills/stacks-technical-diagrams/examples/incident-response.workflow.json +1 -6
- package/ai/skills/stacks-technical-diagrams/examples/lifecycle-agent-run.html +14892 -0
- package/ai/skills/stacks-technical-diagrams/examples/locales/de.json +463 -0
- package/ai/skills/stacks-technical-diagrams/examples/locales/fr.json +463 -0
- package/ai/skills/stacks-technical-diagrams/examples/locales/fr.partial.json +22 -0
- package/ai/skills/stacks-technical-diagrams/examples/locales/it.json +463 -0
- package/ai/skills/stacks-technical-diagrams/examples/locales/ja.json +463 -0
- package/ai/skills/stacks-technical-diagrams/examples/locales/pt.json +463 -0
- package/ai/skills/stacks-technical-diagrams/examples/locales/ru.json +463 -0
- package/ai/skills/stacks-technical-diagrams/examples/orders.erd.json +337 -0
- package/ai/skills/stacks-technical-diagrams/examples/payment-incident.timeline.json +24 -0
- package/ai/skills/stacks-technical-diagrams/examples/payment-platform.tree.json +22 -0
- package/ai/skills/stacks-technical-diagrams/examples/payment-processors.class.json +177 -0
- package/ai/skills/stacks-technical-diagrams/examples/payments.class.json +85 -0
- package/ai/skills/stacks-technical-diagrams/examples/product-analytics.dataflow.json +1 -6
- package/ai/skills/stacks-technical-diagrams/examples/production-deployment.architecture.json +3 -23
- package/ai/skills/stacks-technical-diagrams/examples/release-delivery.workflow.json +1 -6
- package/ai/skills/stacks-technical-diagrams/examples/sequence-cache-miss-request.html +14895 -0
- package/ai/skills/stacks-technical-diagrams/examples/subscription-billing-rendered.html +15369 -0
- package/ai/skills/stacks-technical-diagrams/examples/subscription-billing.erd.json +548 -0
- package/ai/skills/stacks-technical-diagrams/examples/timeline-archify-dev-activity-rendered.html +15002 -0
- package/ai/skills/stacks-technical-diagrams/examples/timeline-payment-incident-rendered.html +14772 -0
- package/ai/skills/stacks-technical-diagrams/examples/tree-archify-repository-rendered.html +15071 -0
- package/ai/skills/stacks-technical-diagrams/examples/tree-payment-platform-rendered.html +14810 -0
- package/ai/skills/stacks-technical-diagrams/examples/waterfall-checkout-request-rendered.html +14754 -0
- package/ai/skills/stacks-technical-diagrams/examples/waterfall-example-rebuild-rendered.html +14940 -0
- package/ai/skills/stacks-technical-diagrams/examples/web-app-rendered.html +14889 -0
- package/ai/skills/stacks-technical-diagrams/examples/web-app.architecture.json +1 -6
- package/ai/skills/stacks-technical-diagrams/examples/workflow-agent-tool-call-rendered.html +14907 -0
- package/ai/skills/stacks-technical-diagrams/explainer/browser-check.mjs +208 -0
- package/ai/skills/stacks-technical-diagrams/explainer/charts.mjs +70 -0
- package/ai/skills/stacks-technical-diagrams/explainer/check.mjs +168 -0
- package/ai/skills/stacks-technical-diagrams/explainer/diagrams.mjs +518 -0
- package/ai/skills/stacks-technical-diagrams/explainer/diff.mjs +124 -0
- package/ai/skills/stacks-technical-diagrams/explainer/inline.mjs +155 -0
- package/ai/skills/stacks-technical-diagrams/explainer/layout.mjs +35 -0
- package/ai/skills/stacks-technical-diagrams/explainer/page-bundle.mjs +16 -0
- package/ai/skills/stacks-technical-diagrams/explainer/parse.mjs +300 -0
- package/ai/skills/stacks-technical-diagrams/explainer/patch.mjs +117 -0
- package/ai/skills/stacks-technical-diagrams/explainer/prepare.mjs +131 -0
- package/ai/skills/stacks-technical-diagrams/explainer/reader.css +472 -0
- package/ai/skills/stacks-technical-diagrams/explainer/reader.js +667 -0
- package/ai/skills/stacks-technical-diagrams/explainer/render.mjs +224 -0
- package/ai/skills/stacks-technical-diagrams/explainer/revision.mjs +20 -0
- package/ai/skills/stacks-technical-diagrams/explainer/sources.mjs +318 -0
- package/ai/skills/stacks-technical-diagrams/explainer/validate.mjs +138 -0
- package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/LICENSE +40 -0
- package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/README.md +5 -0
- package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/3DKXF73K.js +4 -0
- package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/42UTT654.js +3 -0
- package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/DKTLEL5E.js +1 -0
- package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/EEIX5ZYH.js +2 -0
- package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/EJ5KOXYD.js +25 -0
- package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/GUJTEFZK.js +1 -0
- package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/IZOKQ2GT.js +4 -0
- package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/L4LY553T.js +3 -0
- package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/MSKIJHB2.js +2 -0
- package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/RVARI57O.js +1 -0
- package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/UKSIWDDN.js +2 -0
- package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/UPTXNAXN.js +2 -0
- package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/XTDUIGUI.js +9 -0
- package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/YAYTP6L5.js +9 -0
- package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/core.js +1 -0
- package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/diagrams/pie.js +1 -0
- package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/diagrams/quadrantChart.js +1 -0
- package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/diagrams/radar.js +1 -0
- package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/diagrams/sankey.js +1 -0
- package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/diagrams/venn.js +1 -0
- package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/diagrams/xychart.js +1 -0
- package/ai/skills/stacks-technical-diagrams/explainer/writing.mjs +139 -0
- package/ai/skills/stacks-technical-diagrams/locales/README.md +40 -0
- package/ai/skills/stacks-technical-diagrams/locales/ar.json +488 -0
- package/ai/skills/stacks-technical-diagrams/locales/en.json +488 -0
- package/ai/skills/stacks-technical-diagrams/locales/es.json +488 -0
- package/ai/skills/stacks-technical-diagrams/locales/ko.json +488 -0
- package/ai/skills/stacks-technical-diagrams/locales/manifest.json +10 -0
- package/ai/skills/stacks-technical-diagrams/locales/zh-CN.json +488 -0
- package/ai/skills/stacks-technical-diagrams/locales/zh-TW.json +488 -0
- package/ai/skills/stacks-technical-diagrams/migrations/workflow-v2.mjs +1 -0
- package/ai/skills/stacks-technical-diagrams/recipes/scenarios.mjs +115 -51
- package/ai/skills/stacks-technical-diagrams/references/architecture-layout-repair.md +18 -0
- package/ai/skills/stacks-technical-diagrams/references/authoring-contract.md +453 -51
- package/ai/skills/stacks-technical-diagrams/references/authoring-defaults.md +57 -0
- package/ai/skills/stacks-technical-diagrams/references/brand-marks.md +57 -3
- package/ai/skills/stacks-technical-diagrams/references/delivery-contract.md +603 -58
- package/ai/skills/stacks-technical-diagrams/references/explainer-authoring.md +274 -0
- package/ai/skills/stacks-technical-diagrams/references/repository-authoring.md +126 -0
- package/ai/skills/stacks-technical-diagrams/references/update-awareness.md +13 -0
- package/ai/skills/stacks-technical-diagrams/references/viewer-runtime.md +18 -11
- package/ai/skills/stacks-technical-diagrams/renderers/architecture/grid.mjs +1 -1
- package/ai/skills/stacks-technical-diagrams/renderers/architecture/labels.mjs +2 -0
- package/ai/skills/stacks-technical-diagrams/renderers/architecture/render-architecture.mjs +550 -424
- package/ai/skills/stacks-technical-diagrams/renderers/architecture/routing.mjs +1469 -0
- package/ai/skills/stacks-technical-diagrams/renderers/class/README.md +129 -0
- package/ai/skills/stacks-technical-diagrams/renderers/class/render-class.mjs +1095 -0
- package/ai/skills/stacks-technical-diagrams/renderers/dataflow/README.md +89 -17
- package/ai/skills/stacks-technical-diagrams/renderers/dataflow/dataflow-compiler.mjs +1605 -0
- package/ai/skills/stacks-technical-diagrams/renderers/dataflow/joint-label-routing.mjs +165 -0
- package/ai/skills/stacks-technical-diagrams/renderers/dataflow/label-capacity.mjs +81 -0
- package/ai/skills/stacks-technical-diagrams/renderers/dataflow/render-dataflow.mjs +14 -474
- package/ai/skills/stacks-technical-diagrams/renderers/erd/README.md +246 -0
- package/ai/skills/stacks-technical-diagrams/renderers/erd/grid.mjs +150 -0
- package/ai/skills/stacks-technical-diagrams/renderers/erd/render-erd.mjs +1588 -0
- package/ai/skills/stacks-technical-diagrams/renderers/explainer/render-explainer.mjs +42 -0
- package/ai/skills/stacks-technical-diagrams/renderers/lifecycle/README.md +83 -94
- package/ai/skills/stacks-technical-diagrams/renderers/lifecycle/render-lifecycle.mjs +990 -417
- package/ai/skills/stacks-technical-diagrams/renderers/sequence/README.md +49 -17
- package/ai/skills/stacks-technical-diagrams/renderers/sequence/render-sequence.mjs +735 -94
- package/ai/skills/stacks-technical-diagrams/renderers/shared/atomic-output.mjs +1982 -0
- package/ai/skills/stacks-technical-diagrams/renderers/shared/automatic-labels.mjs +290 -0
- package/ai/skills/stacks-technical-diagrams/renderers/shared/brand-marks.mjs +169 -26
- package/ai/skills/stacks-technical-diagrams/renderers/shared/cli.mjs +266 -54
- package/ai/skills/stacks-technical-diagrams/renderers/shared/desktop-readability.mjs +106 -0
- package/ai/skills/stacks-technical-diagrams/renderers/shared/diagnostics.mjs +0 -0
- package/ai/skills/stacks-technical-diagrams/renderers/shared/generated-validators.mjs +268 -2
- package/ai/skills/stacks-technical-diagrams/renderers/shared/geometry.mjs +774 -124
- package/ai/skills/stacks-technical-diagrams/renderers/shared/i18n.mjs +289 -556
- package/ai/skills/stacks-technical-diagrams/renderers/shared/layout-report.mjs +18 -1
- package/ai/skills/stacks-technical-diagrams/renderers/shared/legend.mjs +16 -6
- package/ai/skills/stacks-technical-diagrams/renderers/shared/node-card-style.mjs +32 -0
- package/ai/skills/stacks-technical-diagrams/renderers/shared/output-path.mjs +329 -229
- package/ai/skills/stacks-technical-diagrams/renderers/shared/owned-temp-directory.mjs +82 -0
- package/ai/skills/stacks-technical-diagrams/renderers/shared/path-semantics.mjs +1017 -0
- package/ai/skills/stacks-technical-diagrams/renderers/shared/portable-path.mjs +408 -0
- package/ai/skills/stacks-technical-diagrams/renderers/shared/repository-evidence.mjs +235 -79
- package/ai/skills/stacks-technical-diagrams/renderers/shared/repository-location.mjs +17 -4
- package/ai/skills/stacks-technical-diagrams/renderers/shared/route-quality.mjs +670 -0
- package/ai/skills/stacks-technical-diagrams/renderers/shared/sidecar-path.mjs +38 -0
- package/ai/skills/stacks-technical-diagrams/renderers/shared/software-notices.mjs +6 -0
- package/ai/skills/stacks-technical-diagrams/renderers/shared/spatial-grid.mjs +66 -0
- package/ai/skills/stacks-technical-diagrams/renderers/shared/svg-path-analysis.mjs +135 -0
- package/ai/skills/stacks-technical-diagrams/renderers/shared/text-fit.mjs +59 -3
- package/ai/skills/stacks-technical-diagrams/renderers/shared/utils.mjs +63 -35
- package/ai/skills/stacks-technical-diagrams/renderers/shared/validator.mjs +2 -5
- package/ai/skills/stacks-technical-diagrams/renderers/timeline/README.md +76 -0
- package/ai/skills/stacks-technical-diagrams/renderers/timeline/render-timeline.mjs +576 -0
- package/ai/skills/stacks-technical-diagrams/renderers/tree/README.md +96 -0
- package/ai/skills/stacks-technical-diagrams/renderers/tree/render-tree.mjs +573 -0
- package/ai/skills/stacks-technical-diagrams/renderers/waterfall/README.md +73 -0
- package/ai/skills/stacks-technical-diagrams/renderers/waterfall/render-waterfall.mjs +413 -0
- package/ai/skills/stacks-technical-diagrams/renderers/workflow/README.md +137 -23
- package/ai/skills/stacks-technical-diagrams/renderers/workflow/render-workflow.mjs +18 -6
- package/ai/skills/stacks-technical-diagrams/renderers/workflow/workflow-compiler.mjs +2230 -710
- package/ai/skills/stacks-technical-diagrams/renderers/workflow/workflow-migration-geometry.mjs +17 -0
- package/ai/skills/stacks-technical-diagrams/renderers/workflow/workflow-placement-repair.mjs +94 -0
- package/ai/skills/stacks-technical-diagrams/schemas/README.md +87 -40
- package/ai/skills/stacks-technical-diagrams/schemas/architecture.schema.json +7 -33
- package/ai/skills/stacks-technical-diagrams/schemas/class.schema.json +345 -0
- package/ai/skills/stacks-technical-diagrams/schemas/common.schema.json +68 -2
- package/ai/skills/stacks-technical-diagrams/schemas/dataflow.schema.json +14 -3
- package/ai/skills/stacks-technical-diagrams/schemas/erd.schema.json +381 -0
- package/ai/skills/stacks-technical-diagrams/schemas/explainer.schema.json +927 -0
- package/ai/skills/stacks-technical-diagrams/schemas/lifecycle.schema.json +23 -110
- package/ai/skills/stacks-technical-diagrams/schemas/sequence.schema.json +15 -4
- package/ai/skills/stacks-technical-diagrams/schemas/timeline.schema.json +160 -0
- package/ai/skills/stacks-technical-diagrams/schemas/tree.schema.json +141 -0
- package/ai/skills/stacks-technical-diagrams/schemas/waterfall.schema.json +147 -0
- package/ai/skills/stacks-technical-diagrams/schemas/workflow.schema.json +14 -3
- package/ai/skills/stacks-technical-diagrams/scripts/check-render-output.mjs +876 -243
- package/ai/skills/stacks-technical-diagrams/scripts/check-update.mjs +1717 -0
- package/ai/skills/stacks-technical-diagrams/scripts/delivery-update-child.mjs +26 -0
- package/ai/skills/stacks-technical-diagrams/scripts/generate-brand-marks.mjs +141 -0
- package/ai/skills/stacks-technical-diagrams/scripts/generate-validators.mjs +105 -0
- package/ai/skills/stacks-technical-diagrams/scripts/render-examples.mjs +11 -1
- package/ai/skills/stacks-technical-diagrams/scripts/update-contract.mjs +187 -0
- package/ai/skills/stacks-technical-diagrams/skill-release.json +10 -0
- package/ai/skills/stacks-testing/SKILL.md +8 -1
- package/ide/vscode/package.json +1 -1
- 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 {
|
|
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
|
-
|
|
24
|
+
// 固定 SHA 的来源必须读取原始对象,不能使用本地 replacement refs 的替换内容。
|
|
25
|
+
const result = spawnSync('git', ['--no-replace-objects', '-C', repoRoot, ...args], {
|
|
23
26
|
encoding: 'utf8',
|
|
24
|
-
maxBuffer:
|
|
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
|
-
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
return Boolean(diagram?.meta?.repository) ||
|
|
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
|
-
|
|
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: [
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
109
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
163
|
-
|
|
164
|
-
|
|
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
|
|
167
|
-
const
|
|
168
|
-
const
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
evidence
|
|
184
|
-
|
|
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[
|
|
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',
|
|
220
|
-
subject: { path: '/meta/repository' },
|
|
221
|
-
supportedFixes: [
|
|
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:
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
}
|