@thomasminh1995/depverdict 0.6.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +518 -0
- package/bin/depverdict.js +7 -0
- package/bin/upgradelens.js +7 -0
- package/docs/GR-01-Semantic-Grounding-Failure-Analysis.md +309 -0
- package/docs/GR-02-Versioned-Action-Evaluation-Criteria.md +187 -0
- package/docs/GR-03-Extractive-Contract-Safety-Experiment.md +242 -0
- package/docs/GR-04-Versioned-Production-Extractive-Contract.md +154 -0
- package/docs/IA-01-Repository-Usage-Discovery.md +122 -0
- package/docs/IA-02-Repository-Impact-Analysis.md +118 -0
- package/docs/IA-03-Repository-Impact-Evidence.md +160 -0
- package/docs/IA-04-CLI-Orchestration.md +235 -0
- package/docs/IA-05-Real-Provider-Validation.md +235 -0
- package/docs/IA-05-VinGrade-Validation.md +339 -0
- package/docs/MVP-01.md +48 -0
- package/docs/MVP-02-Architecture.md +536 -0
- package/docs/MVP-02-CLI-HTTP-Runtime.md +50 -0
- package/docs/MVP-02-HTTP-Lifecycle.md +37 -0
- package/docs/MVP-02-Knowledge-Manifest-Generation.md +75 -0
- package/docs/MVP-02-Knowledge-Manifest.md +279 -0
- package/docs/MVP-02-Knowledge-Research-Orchestration.md +62 -0
- package/docs/MVP-02-Knowledge-Store.md +87 -0
- package/docs/MVP-02-PyPI-Registry-Adapter.md +97 -0
- package/docs/MVP-02-Research-Planning.md +196 -0
- package/docs/MVP-02-Source-Provenance.md +81 -0
- package/docs/MVP-02-npm-Registry-Adapter.md +99 -0
- package/docs/RR-01-End-to-End-and-Real-Provider-Validation.md +393 -0
- package/docs/RR-01-RERUN-Extractive-Contract-Validation.md +566 -0
- package/docs/RR-02-Full-Product-Workflow-and-Developer-CLI-UX-Review.md +35 -0
- package/docs/RR02-FIX-01-Persistent-Qualification-Resolution.md +32 -0
- package/docs/RR02-FIX-02-Stage-aware-CLI-Progress-and-Heartbeat.md +28 -0
- package/docs/RR02-FIX-03-npm-Capture-Evidence-Exclusion.md +33 -0
- package/docs/RR02-FIX-03A-Complete-Package-Exclusion-and-Evidence-Commit.md +223 -0
- package/docs/RR02-FIX-04-Event-loop-safe-Heartbeat.md +393 -0
- package/docs/RR02-FIX-05-Materialize-Persisted-Qualification.md +207 -0
- package/docs/RR02-RERUN-CLI-Qualification-Progress-UX-and-Package-Validation.md +342 -0
- package/docs/VinGrade-MVP-02-Validation.md +480 -0
- package/docs/VinGrade-RC02-Live-Validation.md +273 -0
- package/docs/ai-capability-discovery.md +573 -0
- package/docs/ai-engineering-production-readiness.md +390 -0
- package/docs/ai-engineering-review.md +251 -0
- package/docs/ai-runtime-governance-discovery.md +754 -0
- package/docs/architecture-overview.md +78 -0
- package/docs/cli-progress.md +111 -0
- package/docs/decisions/diff-01-brand-distribution-identity.md +450 -0
- package/docs/decisions/diff-02-identity-compatibility-contract.md +274 -0
- package/docs/decisions/diff-03-repository-docs-community-migration.md +165 -0
- package/docs/decisions/diff-04-release-evidence-gap-acceptance.md +110 -0
- package/docs/discovery/mvp-05-ai-migration-planning-discovery.md +350 -0
- package/docs/gateway-runtime-discovery.md +614 -0
- package/docs/live-ai-validation.md +299 -0
- package/docs/migration-planning-qualification-resolution.md +99 -0
- package/docs/migrations/upgradelens-to-depverdict.md +97 -0
- package/docs/mp-r03-deterministic-upgrade-decision-architecture.md +135 -0
- package/docs/mp-r04-evidence-bounded-migration-handoff-architecture.md +116 -0
- package/docs/mp-r05-product-completion-and-decision-first-cli-architecture.md +162 -0
- package/docs/mvp-05-deterministic-context-runtime.md +127 -0
- package/docs/mvp-05-migration-checklist-contract.md +166 -0
- package/docs/mvp-05-migration-checklist-orchestration.md +105 -0
- package/docs/mvp-05-migration-evaluation-and-qualification.md +135 -0
- package/docs/mvp-05-provider-neutral-generator.md +79 -0
- package/docs/ollama-local-smoke-validation.md +156 -0
- package/docs/openai-compatible-runtime-discovery.md +789 -0
- package/docs/openrouter-one-dependency-validation.md +205 -0
- package/docs/oss-02-package-guard-hardening-architecture.md +131 -0
- package/docs/oss-04-public-ci-package-metadata-architecture.md +142 -0
- package/docs/package-content-policy.md +98 -0
- package/docs/releases/v0.5.0-technical-preview.md +114 -0
- package/docs/releases/v0.6.0-alpha.1-depverdict-preview.md +101 -0
- package/docs/reviews/diff-02-identity-contract-compatibility.md +413 -0
- package/docs/reviews/diff-03-repository-docs-community-migration.md +364 -0
- package/docs/reviews/diff-04-depverdict-distribution-identity-readiness-rereview.md +453 -0
- package/docs/reviews/diff-04-fix-post-rename-identity-release-remediation.md +310 -0
- package/docs/reviews/diff-05-final-preview-distribution-qualification.md +561 -0
- package/docs/reviews/mvp-05-final-product-value-workflow-rereview.md +471 -0
- package/docs/reviews/mvp-05-product-workflow-review.md +605 -0
- package/docs/reviews/oss-01-duplicate-artifact-investigation-cleanup.md +411 -0
- package/docs/reviews/oss-02-package-guard-hardening.md +303 -0
- package/docs/reviews/oss-03-community-scaffolding.md +378 -0
- package/docs/reviews/oss-04-public-ci-package-metadata.md +439 -0
- package/docs/reviews/oss-05-technical-preview-qualification.md +482 -0
- package/docs/reviews/upgradelens-vs-upgradedepdetective-source-comparison.md +355 -0
- package/docs/reviews/v0.5.0-pre-release-smoke.md +263 -0
- package/docs/reviews/v0.5.0-version-bump-release-verification.md +306 -0
- package/docs/runtime-contract-discovery.md +521 -0
- package/docs/structured-output-compatibility-report.md +100 -0
- package/docs/ts-fix-01-exact-duplicate-occurrence-target-selection-architecture.md +111 -0
- package/docs/version-analysis-architecture.md +827 -0
- package/eval/README.md +86 -0
- package/eval/datasets/generic/declared-constraint.json +59 -0
- package/eval/datasets/generic/evidence-conflict.json +73 -0
- package/eval/datasets/generic/major-breaking-release.json +66 -0
- package/eval/datasets/generic/missing-evidence.json +46 -0
- package/eval/datasets/generic/patch-release-low.json +59 -0
- package/eval/datasets/node/axios-patch-low.json +59 -0
- package/eval/datasets/node/react-major-breaking.json +66 -0
- package/eval/datasets/node/react-minor-compatibility.json +66 -0
- package/eval/datasets/python/fastapi-deprecation.json +66 -0
- package/eval/datasets/python/pydantic-major-breaking.json +66 -0
- package/eval/migration-planning/golden-dataset-v2.json +306 -0
- package/eval/migration-planning/golden-dataset.json +214 -0
- package/eval/schemas/expected-result.schema.json +88 -0
- package/eval/schemas/golden-case.schema.json +181 -0
- package/package.json +57 -0
- package/schemas/ai-scorecard.schema.json +132 -0
- package/schemas/benchmark-report.schema.json +189 -0
- package/schemas/benchmark.schema.json +55 -0
- package/schemas/capability-profile.schema.json +44 -0
- package/schemas/conformance-report.schema.json +150 -0
- package/schemas/deployment-profile.schema.json +64 -0
- package/schemas/evaluation-report.schema.json +158 -0
- package/schemas/knowledge-evidence-bundle.schema.json +150 -0
- package/schemas/knowledge-manifest.schema.json +548 -0
- package/schemas/metrics.schema.json +178 -0
- package/schemas/migration-checklist-extractive-candidate.schema.json +42 -0
- package/schemas/migration-checklist.schema.json +706 -0
- package/schemas/migration-evaluation-dataset-v2.schema.json +208 -0
- package/schemas/migration-evaluation-dataset.schema.json +204 -0
- package/schemas/migration-planning-qualification-record.schema.json +234 -0
- package/schemas/project-manifest.schema.json +308 -0
- package/schemas/qualification-record.schema.json +56 -0
- package/schemas/repository-impact-evidence.schema.json +232 -0
- package/schemas/repository-impact.schema.json +202 -0
- package/schemas/upgrade-decision.schema.json +273 -0
- package/schemas/usage-index.schema.json +179 -0
- package/schemas/version-analysis.schema.json +449 -0
- package/src/ai-runtime-debug.js +325 -0
- package/src/ai-runtime-error.js +42 -0
- package/src/ai-runtime.js +174 -0
- package/src/ai-scorecard.js +204 -0
- package/src/ai-version-analysis.js +484 -0
- package/src/artifact-root-compatibility.js +91 -0
- package/src/benchmark-report.js +111 -0
- package/src/benchmark-runner.js +191 -0
- package/src/canonical-json.js +69 -0
- package/src/cli.js +1299 -0
- package/src/conformance-report.js +158 -0
- package/src/conformance-runner.js +253 -0
- package/src/constants.js +73 -0
- package/src/cooperative-scheduler.js +79 -0
- package/src/dependencies.js +44 -0
- package/src/dependency-ai-context.js +625 -0
- package/src/detectors.js +253 -0
- package/src/discovery.js +234 -0
- package/src/ecosystem-version-adapter.js +294 -0
- package/src/environment-compatibility.js +77 -0
- package/src/evaluation-comparator.js +158 -0
- package/src/evaluation-report.js +76 -0
- package/src/evaluation-runner.js +248 -0
- package/src/evidence-source-adapter.js +472 -0
- package/src/files.js +89 -0
- package/src/governance-diagnostics.js +64 -0
- package/src/governance-loader.js +63 -0
- package/src/governance-metadata.js +346 -0
- package/src/governance-validator.js +360 -0
- package/src/http/bounded-fetch.js +278 -0
- package/src/http/cli-http-runtime.js +44 -0
- package/src/impact/input-loader.js +157 -0
- package/src/impact/matcher.js +40 -0
- package/src/impact/repository-impact.js +199 -0
- package/src/impact/runtime.js +24 -0
- package/src/impact/status.js +62 -0
- package/src/impact/writer.js +30 -0
- package/src/impact-evidence/input-loader.js +202 -0
- package/src/impact-evidence/repository-impact-evidence.js +234 -0
- package/src/impact-evidence/runtime.js +16 -0
- package/src/impact-evidence/writer.js +30 -0
- package/src/index.js +563 -0
- package/src/installed-version-baseline.js +196 -0
- package/src/knowledge-cache.js +324 -0
- package/src/knowledge-evidence-bundle.js +101 -0
- package/src/knowledge-evidence-producer.js +233 -0
- package/src/knowledge-manifest-builder.js +188 -0
- package/src/knowledge-manifest-writer.js +32 -0
- package/src/knowledge-manifest.js +255 -0
- package/src/knowledge-research.js +615 -0
- package/src/metrics-engine.js +205 -0
- package/src/migration-checklist/ai-candidate.js +320 -0
- package/src/migration-checklist/assembler.js +37 -0
- package/src/migration-checklist/context-runtime.js +828 -0
- package/src/migration-checklist/evaluation/action-criteria.js +244 -0
- package/src/migration-checklist/evaluation/comparator-v2.js +332 -0
- package/src/migration-checklist/evaluation/comparator.js +279 -0
- package/src/migration-checklist/evaluation/dataset-v2.js +227 -0
- package/src/migration-checklist/evaluation/dataset.js +336 -0
- package/src/migration-checklist/evaluation/extractive-fixtures-v2.js +148 -0
- package/src/migration-checklist/evaluation/metrics-v2.js +226 -0
- package/src/migration-checklist/evaluation/metrics.js +158 -0
- package/src/migration-checklist/evaluation/qualification-v2.js +321 -0
- package/src/migration-checklist/evaluation/qualification.js +239 -0
- package/src/migration-checklist/evaluation/runner-v2.js +294 -0
- package/src/migration-checklist/evaluation/runner.js +194 -0
- package/src/migration-checklist/evaluation/scorecard-v2.js +106 -0
- package/src/migration-checklist/evaluation/scorecard.js +86 -0
- package/src/migration-checklist/extractive-candidate.js +166 -0
- package/src/migration-checklist/extractive-prompt.js +62 -0
- package/src/migration-checklist/generator.js +702 -0
- package/src/migration-checklist/grounding-policy.js +117 -0
- package/src/migration-checklist/input-loader.js +613 -0
- package/src/migration-checklist/migration-checklist.js +635 -0
- package/src/migration-checklist/presentation.js +292 -0
- package/src/migration-checklist/progress.js +96 -0
- package/src/migration-checklist/prompt.js +83 -0
- package/src/migration-checklist/qualification-guard.js +462 -0
- package/src/migration-checklist/qualification-resolution.js +122 -0
- package/src/migration-checklist/qualification-store.js +225 -0
- package/src/migration-checklist/runtime.js +205 -0
- package/src/migration-checklist/verification.js +134 -0
- package/src/migration-checklist/writer.js +35 -0
- package/src/openai-compatible-provider.js +451 -0
- package/src/orchestration/failure-log.js +32 -0
- package/src/orchestration/pipeline.js +200 -0
- package/src/orchestration/progress-events.js +337 -0
- package/src/orchestration/progress-reporter.js +131 -0
- package/src/orchestration/text-writer.js +22 -0
- package/src/portable.js +13 -0
- package/src/product-completion.js +249 -0
- package/src/project-manifest-input.js +90 -0
- package/src/project-manifest.js +141 -0
- package/src/python-requirements.js +137 -0
- package/src/registry/npm-packument.js +256 -0
- package/src/registry/npm-registry-adapter.js +262 -0
- package/src/registry/pypi-project.js +300 -0
- package/src/registry/pypi-registry-adapter.js +235 -0
- package/src/registry/sanitize-registry-body.js +51 -0
- package/src/renderers/console.js +160 -0
- package/src/renderers/impact-presentation.js +278 -0
- package/src/renderers/markdown.js +172 -0
- package/src/research-plan.js +455 -0
- package/src/runtime-conformance.js +275 -0
- package/src/source-provenance.js +393 -0
- package/src/source-url.js +62 -0
- package/src/structured-output-schema.js +66 -0
- package/src/target-selector.js +306 -0
- package/src/upgrade-decision/input-loader.js +107 -0
- package/src/upgrade-decision/presentation.js +43 -0
- package/src/upgrade-decision/runtime.js +21 -0
- package/src/upgrade-decision/upgrade-decision.js +626 -0
- package/src/upgrade-decision/writer.js +30 -0
- package/src/usage/analyzer-registry.js +63 -0
- package/src/usage/coverage.js +116 -0
- package/src/usage/input-loader.js +139 -0
- package/src/usage/js/analyzer.js +240 -0
- package/src/usage/js/parser.js +21 -0
- package/src/usage/runtime.js +187 -0
- package/src/usage/scope.js +44 -0
- package/src/usage/source-files.js +50 -0
- package/src/usage/usage-index.js +217 -0
- package/src/usage/writer.js +31 -0
- package/src/version-analysis-loader.js +203 -0
- package/src/version-analysis-manifest.js +314 -0
- package/src/version-analysis-writer.js +30 -0
|
@@ -0,0 +1,472 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
import { canonicalJson } from './canonical-json.js';
|
|
4
|
+
import { fetchEvidenceDocument } from './http/bounded-fetch.js';
|
|
5
|
+
import { createCacheIdentity } from './knowledge-cache.js';
|
|
6
|
+
import { compareText } from './portable.js';
|
|
7
|
+
import { canonicalizeSourceUrl } from './source-url.js';
|
|
8
|
+
|
|
9
|
+
export const DEFAULT_EVIDENCE_SOURCE_LIMIT = 5;
|
|
10
|
+
export const DEFAULT_EVIDENCE_DOCUMENT_BYTES = 512 * 1024;
|
|
11
|
+
export const DEFAULT_EVIDENCE_CONTENT_CHARACTERS = 24_000;
|
|
12
|
+
export const DEFAULT_EVIDENCE_TTL_MS = 24 * 60 * 60 * 1_000;
|
|
13
|
+
|
|
14
|
+
const ROLE_PRIORITY = new Map([
|
|
15
|
+
['releaseNotes', 0],
|
|
16
|
+
['releases', 1],
|
|
17
|
+
['changelog', 2],
|
|
18
|
+
['migrationGuide', 3]
|
|
19
|
+
]);
|
|
20
|
+
const PRIMARY_KINDS = new Set([
|
|
21
|
+
'releaseNotes', 'changelog', 'migrationGuide', 'breakingChanges', 'deprecations', 'compatibility'
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
function digestText(value) {
|
|
25
|
+
return `sha256:${createHash('sha256').update(value, 'utf8').digest('hex')}`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function digestJson(value) {
|
|
29
|
+
return digestText(canonicalJson(value));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function sourceIdFor(packageId, group, url) {
|
|
33
|
+
return `${packageId}:${group}:${createHash('sha256').update(url).digest('hex')}`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function decodeHtmlEntities(value) {
|
|
37
|
+
const named = { amp: '&', apos: "'", gt: '>', lt: '<', nbsp: ' ', quot: '"' };
|
|
38
|
+
return value.replace(/&(#x[0-9a-f]+|#\d+|[a-z]+);/gi, (match, entity) => {
|
|
39
|
+
if (entity[0] === '#') {
|
|
40
|
+
const number = entity[1].toLowerCase() === 'x'
|
|
41
|
+
? Number.parseInt(entity.slice(2), 16)
|
|
42
|
+
: Number.parseInt(entity.slice(1), 10);
|
|
43
|
+
return Number.isSafeInteger(number) && number > 0 && number <= 0x10ffff
|
|
44
|
+
? String.fromCodePoint(number)
|
|
45
|
+
: match;
|
|
46
|
+
}
|
|
47
|
+
return named[entity.toLowerCase()] ?? match;
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function htmlToText(value) {
|
|
52
|
+
return decodeHtmlEntities(value
|
|
53
|
+
.replace(/<(script|style|nav|footer|header)\b[^>]*>[\s\S]*?<\/\1>/gi, '')
|
|
54
|
+
.replace(/<h([1-6])\b[^>]*>/gi, (_match, level) => `\n${'#'.repeat(Number(level))} `)
|
|
55
|
+
.replace(/<\/(?:h[1-6]|p|div|section|article|li|tr)>/gi, '\n')
|
|
56
|
+
.replace(/<(?:p|div|section|article)\b[^>]*>/gi, '')
|
|
57
|
+
.replace(/<br\s*\/?>/gi, '\n')
|
|
58
|
+
.replace(/<li\b[^>]*>/gi, '- ')
|
|
59
|
+
.replace(/<[^>]+>/g, ' '));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function normalizeEvidenceContent(value, {
|
|
63
|
+
mediaType = 'text/plain',
|
|
64
|
+
maxCharacters = DEFAULT_EVIDENCE_CONTENT_CHARACTERS
|
|
65
|
+
} = {}) {
|
|
66
|
+
if (typeof value !== 'string') return '';
|
|
67
|
+
const source = /(?:text\/html|application\/xhtml\+xml)/i.test(mediaType) ? htmlToText(value) : value;
|
|
68
|
+
const normalized = source
|
|
69
|
+
.normalize('NFC')
|
|
70
|
+
.replace(/[a-z][a-z0-9+.-]*:\/\/[^\s/?#]*@[^\s]+/gi, '[credentialed URL removed]')
|
|
71
|
+
.replace(/\r\n?/g, '\n')
|
|
72
|
+
.replace(/\u0000/g, '')
|
|
73
|
+
.split('\n')
|
|
74
|
+
.map((line) => line.replace(/[\t ]+$/g, ''))
|
|
75
|
+
.join('\n')
|
|
76
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
77
|
+
.trim();
|
|
78
|
+
if (normalized.length <= maxCharacters) return normalized;
|
|
79
|
+
const bounded = normalized.slice(0, maxCharacters);
|
|
80
|
+
const boundary = Math.max(bounded.lastIndexOf('\n'), bounded.lastIndexOf(' '));
|
|
81
|
+
return `${bounded.slice(0, boundary > maxCharacters * 0.8 ? boundary : maxCharacters).trimEnd()}\n[content truncated]`;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function portableDocument(request, response) {
|
|
85
|
+
if (request.format !== 'githubReleases') {
|
|
86
|
+
return {
|
|
87
|
+
mediaType: 'text/plain',
|
|
88
|
+
text: normalizeEvidenceContent(response.text, { mediaType: response.mediaType })
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
let releases;
|
|
92
|
+
try {
|
|
93
|
+
releases = JSON.parse(response.text);
|
|
94
|
+
} catch {
|
|
95
|
+
return { mediaType: 'application/json', text: '[]' };
|
|
96
|
+
}
|
|
97
|
+
const portable = Array.isArray(releases) ? releases.filter((release) => release && typeof release === 'object')
|
|
98
|
+
.map((release) => ({
|
|
99
|
+
tag_name: typeof release.tag_name === 'string' ? release.tag_name : null,
|
|
100
|
+
name: typeof release.name === 'string' ? normalizeEvidenceContent(release.name) : null,
|
|
101
|
+
body: typeof release.body === 'string' ? normalizeEvidenceContent(release.body, { maxCharacters: 8_000 }) : null,
|
|
102
|
+
draft: release.draft === true,
|
|
103
|
+
published_at: typeof release.published_at === 'string' ? release.published_at : null
|
|
104
|
+
})) : [];
|
|
105
|
+
return { mediaType: 'application/json', text: JSON.stringify(portable) };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function primaryKindForRole(role) {
|
|
109
|
+
if (role === 'releaseNotes' || role === 'releases') return 'releaseNotes';
|
|
110
|
+
if (role === 'changelog') return 'changelog';
|
|
111
|
+
if (role === 'migrationGuide') return 'migrationGuide';
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function classifyEvidenceContent({ role, url = '', heading = '', content = '' } = {}) {
|
|
116
|
+
const text = `${heading}\n${content.slice(0, 400)}`;
|
|
117
|
+
if (/\b(?:breaking changes?|breaking)\b/i.test(heading)) return 'breakingChanges';
|
|
118
|
+
if (/\b(?:deprecated|deprecations?)\b/i.test(heading)) return 'deprecations';
|
|
119
|
+
if (/\b(?:compatibility|requirements?|supported (?:runtime|platform|python|node))\b/i.test(heading)) {
|
|
120
|
+
return 'compatibility';
|
|
121
|
+
}
|
|
122
|
+
if (/\b(?:migration|migrating|upgrade guide)\b/i.test(heading)
|
|
123
|
+
|| /(?:^|\/)(?:migration|migrating|upgrade)(?:[-_.\/]|$)/i.test(new URL(url, 'https://invalid.example').pathname)) {
|
|
124
|
+
return 'migrationGuide';
|
|
125
|
+
}
|
|
126
|
+
if (/^\s*#+\s*(?:breaking|deprecated|compatibility)/im.test(text)) return primaryKindForRole(role);
|
|
127
|
+
return primaryKindForRole(role);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function githubRepositoryCoordinates(value) {
|
|
131
|
+
try {
|
|
132
|
+
const url = new URL(value);
|
|
133
|
+
const parts = url.pathname.split('/').filter(Boolean);
|
|
134
|
+
if (url.hostname !== 'github.com' || parts.length !== 2) return null;
|
|
135
|
+
return { owner: parts[0], repository: parts[1] };
|
|
136
|
+
} catch {
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function derivedRequest(packageRecord, source, { role, group, url, format }) {
|
|
142
|
+
const canonical = canonicalizeSourceUrl(url);
|
|
143
|
+
if (!canonical) return null;
|
|
144
|
+
return {
|
|
145
|
+
packageId: packageRecord.id,
|
|
146
|
+
ecosystem: packageRecord.ecosystem,
|
|
147
|
+
sourceId: sourceIdFor(packageRecord.id, group, canonical),
|
|
148
|
+
sourceKind: role === 'releases' ? 'releaseFeed' : 'officialDocumentation',
|
|
149
|
+
role,
|
|
150
|
+
url: canonical,
|
|
151
|
+
format,
|
|
152
|
+
discoveredFrom: source.id,
|
|
153
|
+
trustEvidenceSourceIds: source.discoveredFrom ? [source.discoveredFrom] : [],
|
|
154
|
+
authority: 'publisherProvided',
|
|
155
|
+
trust: 'publisher',
|
|
156
|
+
conflictsWith: []
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function directRequest(packageRecord, source, role) {
|
|
161
|
+
const canonical = canonicalizeSourceUrl(source.url);
|
|
162
|
+
if (!canonical) return null;
|
|
163
|
+
return {
|
|
164
|
+
packageId: packageRecord.id,
|
|
165
|
+
ecosystem: packageRecord.ecosystem,
|
|
166
|
+
sourceId: source.id,
|
|
167
|
+
sourceKind: source.kind,
|
|
168
|
+
role,
|
|
169
|
+
url: canonical,
|
|
170
|
+
format: 'document',
|
|
171
|
+
discoveredFrom: source.discoveredFrom,
|
|
172
|
+
trustEvidenceSourceIds: [...source.trustEvidenceSourceIds],
|
|
173
|
+
authority: source.authority,
|
|
174
|
+
trust: source.trust,
|
|
175
|
+
conflictsWith: [...(source.conflictsWith ?? [])]
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** Discover only registry-qualified direct documents and a bounded GitHub repository mapping. */
|
|
180
|
+
export function discoverEvidenceSourceRequests(packageRecord, sources, {
|
|
181
|
+
maxCandidates = DEFAULT_EVIDENCE_SOURCE_LIMIT
|
|
182
|
+
} = {}) {
|
|
183
|
+
const packageSources = sources.filter((source) => packageRecord.sourceIds.includes(source.id));
|
|
184
|
+
const requests = [];
|
|
185
|
+
for (const source of packageSources) {
|
|
186
|
+
if (source.kind === 'registry') continue;
|
|
187
|
+
for (const role of source.supports ?? []) {
|
|
188
|
+
if (ROLE_PRIORITY.has(role)) requests.push(directRequest(packageRecord, source, role));
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
for (const source of packageSources.filter((item) => item.supports?.includes('repository'))) {
|
|
192
|
+
const repository = githubRepositoryCoordinates(source.url);
|
|
193
|
+
if (!repository) continue;
|
|
194
|
+
const base = `https://api.github.com/repos/${repository.owner}/${repository.repository}`;
|
|
195
|
+
const raw = `https://raw.githubusercontent.com/${repository.owner}/${repository.repository}/HEAD`;
|
|
196
|
+
requests.push(
|
|
197
|
+
derivedRequest(packageRecord, source, {
|
|
198
|
+
role: 'releases', group: 'releaseFeed', url: `${base}/releases`, format: 'githubReleases'
|
|
199
|
+
}),
|
|
200
|
+
derivedRequest(packageRecord, source, {
|
|
201
|
+
role: 'changelog', group: 'changelog', url: `${raw}/CHANGELOG.md`, format: 'document'
|
|
202
|
+
}),
|
|
203
|
+
derivedRequest(packageRecord, source, {
|
|
204
|
+
role: 'migrationGuide', group: 'migrationGuide', url: `${raw}/MIGRATION.md`, format: 'document'
|
|
205
|
+
})
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
const unique = new Map();
|
|
209
|
+
for (const request of requests.filter(Boolean).sort((left, right) =>
|
|
210
|
+
(ROLE_PRIORITY.get(left.role) ?? 99) - (ROLE_PRIORITY.get(right.role) ?? 99)
|
|
211
|
+
|| compareText(left.url, right.url))) {
|
|
212
|
+
if (!unique.has(request.url)) unique.set(request.url, request);
|
|
213
|
+
}
|
|
214
|
+
return [...unique.values()].slice(0, maxCandidates);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function versionForTag(tag, packageRecord) {
|
|
218
|
+
if (typeof tag !== 'string') return null;
|
|
219
|
+
const versions = packageRecord.releaseIndex.map((release) => release.version).filter(Boolean)
|
|
220
|
+
.sort((left, right) => right.length - left.length || compareText(left, right));
|
|
221
|
+
return versions.find((version) => tag === version || tag === `v${version}`
|
|
222
|
+
|| tag.endsWith(`@${version}`) || tag.endsWith(`-${version}`)) ?? null;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function versionsForHeading(heading, packageRecord) {
|
|
226
|
+
return packageRecord.releaseIndex.map((release) => release.version).filter((version) =>
|
|
227
|
+
new RegExp(`(^|[^0-9A-Za-z])${version.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}([^0-9A-Za-z]|$)`).test(heading)
|
|
228
|
+
).sort(compareText);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function splitMarkdownSections(content) {
|
|
232
|
+
const lines = content.split('\n');
|
|
233
|
+
const starts = [];
|
|
234
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
235
|
+
const match = /^(#{1,6})\s+(.+?)\s*$/.exec(lines[index]);
|
|
236
|
+
if (match) starts.push({ index, heading: match[2] });
|
|
237
|
+
}
|
|
238
|
+
if (starts.length === 0) return [{ heading: '', content }];
|
|
239
|
+
return starts.map((start, index) => ({
|
|
240
|
+
heading: start.heading,
|
|
241
|
+
content: lines.slice(start.index, starts[index + 1]?.index ?? lines.length).join('\n').trim()
|
|
242
|
+
})).filter((section) => section.content.length > 0);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function evidenceItem({ request, retrievedAt, kind, locator, releaseVersions, content }) {
|
|
246
|
+
const contentDigest = digestText(content);
|
|
247
|
+
const material = {
|
|
248
|
+
packageId: request.packageId,
|
|
249
|
+
sourceId: request.sourceId,
|
|
250
|
+
kind,
|
|
251
|
+
locator,
|
|
252
|
+
releaseVersions,
|
|
253
|
+
contentDigest
|
|
254
|
+
};
|
|
255
|
+
return {
|
|
256
|
+
id: digestJson(material),
|
|
257
|
+
packageId: request.packageId,
|
|
258
|
+
sourceId: request.sourceId,
|
|
259
|
+
kind,
|
|
260
|
+
contentDigest,
|
|
261
|
+
retrievedAt,
|
|
262
|
+
mediaType: 'text/plain',
|
|
263
|
+
locator,
|
|
264
|
+
releaseVersions,
|
|
265
|
+
content
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function evidenceFromDocument(request, packageRecord, document, retrievedAt) {
|
|
270
|
+
const normalized = normalizeEvidenceContent(document.text, { mediaType: document.mediaType });
|
|
271
|
+
if (!normalized) return [];
|
|
272
|
+
const sections = splitMarkdownSections(normalized);
|
|
273
|
+
const items = [];
|
|
274
|
+
for (const [index, section] of sections.entries()) {
|
|
275
|
+
const kind = classifyEvidenceContent({
|
|
276
|
+
role: request.role,
|
|
277
|
+
url: request.url,
|
|
278
|
+
heading: section.heading,
|
|
279
|
+
content: section.content
|
|
280
|
+
});
|
|
281
|
+
if (!PRIMARY_KINDS.has(kind)) continue;
|
|
282
|
+
const releaseVersions = versionsForHeading(section.heading, packageRecord);
|
|
283
|
+
items.push(evidenceItem({
|
|
284
|
+
request,
|
|
285
|
+
retrievedAt,
|
|
286
|
+
kind,
|
|
287
|
+
locator: section.heading ? `heading:${section.heading}` : `document:${index + 1}`,
|
|
288
|
+
releaseVersions,
|
|
289
|
+
content: section.content
|
|
290
|
+
}));
|
|
291
|
+
}
|
|
292
|
+
return items.slice(0, 12);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function evidenceFromGithubReleases(request, packageRecord, document, retrievedAt) {
|
|
296
|
+
let releases;
|
|
297
|
+
try {
|
|
298
|
+
releases = JSON.parse(document.text);
|
|
299
|
+
} catch {
|
|
300
|
+
return [];
|
|
301
|
+
}
|
|
302
|
+
if (!Array.isArray(releases)) return [];
|
|
303
|
+
return releases.filter((release) => release && typeof release === 'object'
|
|
304
|
+
&& release.draft !== true && typeof release.tag_name === 'string')
|
|
305
|
+
.flatMap((release) => {
|
|
306
|
+
const version = versionForTag(release.tag_name, packageRecord);
|
|
307
|
+
const content = normalizeEvidenceContent([
|
|
308
|
+
release.name ? `# ${release.name}` : `# ${release.tag_name}`,
|
|
309
|
+
`Tag: ${release.tag_name}`,
|
|
310
|
+
release.published_at ? `Published: ${release.published_at}` : '',
|
|
311
|
+
typeof release.body === 'string' ? release.body : ''
|
|
312
|
+
].filter(Boolean).join('\n\n'), { mediaType: 'text/markdown', maxCharacters: 8_000 });
|
|
313
|
+
if (!content || !version) return [];
|
|
314
|
+
const heading = release.name ?? release.tag_name;
|
|
315
|
+
return [evidenceItem({
|
|
316
|
+
request,
|
|
317
|
+
retrievedAt,
|
|
318
|
+
kind: classifyEvidenceContent({ role: 'releaseNotes', heading, content }) ?? 'releaseNotes',
|
|
319
|
+
locator: `release:${release.tag_name}`,
|
|
320
|
+
releaseVersions: [version],
|
|
321
|
+
content
|
|
322
|
+
})];
|
|
323
|
+
}).slice(0, 12);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function requestIdentity(request) {
|
|
327
|
+
return createCacheIdentity({
|
|
328
|
+
adapter: 'official-evidence',
|
|
329
|
+
resourceKind: 'evidence-document',
|
|
330
|
+
packageId: request.packageId,
|
|
331
|
+
resourceVariant: createHash('sha256').update(request.url).digest('hex'),
|
|
332
|
+
adapterVersion: '1'
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function warning(request, code, message, retryable) {
|
|
337
|
+
return { code, packageId: request.packageId, sourceId: request.sourceId, message, retryable };
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function snapshot(content, mediaType, retrievedAt, freshness) {
|
|
341
|
+
return { contentDigest: digestText(content), mediaType, retrievedAt, freshness };
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function sourceRecord(request, status, sourceSnapshot) {
|
|
345
|
+
return {
|
|
346
|
+
id: request.sourceId,
|
|
347
|
+
kind: request.sourceKind,
|
|
348
|
+
authority: request.authority,
|
|
349
|
+
trust: request.trust,
|
|
350
|
+
url: request.url,
|
|
351
|
+
status,
|
|
352
|
+
supports: [request.role],
|
|
353
|
+
discoveredFrom: request.discoveredFrom,
|
|
354
|
+
trustEvidenceSourceIds: [...request.trustEvidenceSourceIds].sort(compareText),
|
|
355
|
+
snapshot: sourceSnapshot,
|
|
356
|
+
conflictsWith: [...request.conflictsWith].sort(compareText)
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
async function mapBounded(items, concurrency, worker) {
|
|
361
|
+
const results = new Array(items.length);
|
|
362
|
+
let next = 0;
|
|
363
|
+
async function run() {
|
|
364
|
+
while (next < items.length) {
|
|
365
|
+
const index = next;
|
|
366
|
+
next += 1;
|
|
367
|
+
results[index] = await worker(items[index]);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
await Promise.all(Array.from({ length: Math.min(concurrency, items.length) }, run));
|
|
371
|
+
return results;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/** Provider-neutral source adapter for bounded, official evidence documents. */
|
|
375
|
+
export function createEvidenceSourceAdapter({
|
|
376
|
+
fetch: fetchImplementation = globalThis.fetch,
|
|
377
|
+
cache,
|
|
378
|
+
clock = () => new Date(),
|
|
379
|
+
offline = false,
|
|
380
|
+
timeoutMs = 10_000,
|
|
381
|
+
maxResponseBytes = DEFAULT_EVIDENCE_DOCUMENT_BYTES,
|
|
382
|
+
ttlMs = DEFAULT_EVIDENCE_TTL_MS,
|
|
383
|
+
maxCandidatesPerPackage = DEFAULT_EVIDENCE_SOURCE_LIMIT,
|
|
384
|
+
concurrency = 2
|
|
385
|
+
} = {}) {
|
|
386
|
+
if (!cache || typeof cache.read !== 'function' || typeof cache.write !== 'function') {
|
|
387
|
+
throw new Error('Evidence Source Adapter requires a Knowledge Store cache.');
|
|
388
|
+
}
|
|
389
|
+
return {
|
|
390
|
+
async enrich({ packages, sources }) {
|
|
391
|
+
const requests = packages.flatMap((packageRecord) =>
|
|
392
|
+
discoverEvidenceSourceRequests(packageRecord, sources, { maxCandidates: maxCandidatesPerPackage })
|
|
393
|
+
.map((request) => ({ request, packageRecord })));
|
|
394
|
+
const outcomes = await mapBounded(requests, concurrency, async ({ request, packageRecord }) => {
|
|
395
|
+
const identity = requestIdentity(request);
|
|
396
|
+
const cached = await cache.read(identity);
|
|
397
|
+
let document;
|
|
398
|
+
let retrievedAt;
|
|
399
|
+
let freshness = 'fresh';
|
|
400
|
+
if (cached.status === 'fresh' || (offline && cached.status === 'expired')) {
|
|
401
|
+
document = cached.body;
|
|
402
|
+
retrievedAt = cached.storedAt;
|
|
403
|
+
freshness = cached.status === 'fresh' ? 'fresh' : 'stale';
|
|
404
|
+
} else if (offline) {
|
|
405
|
+
return {
|
|
406
|
+
packageId: request.packageId,
|
|
407
|
+
source: sourceRecord(request, 'unavailable', null), evidence: [],
|
|
408
|
+
warnings: [warning(request, 'EVIDENCE_SOURCE_UNAVAILABLE', 'Offline mode has no cached official evidence document.', false)]
|
|
409
|
+
};
|
|
410
|
+
} else {
|
|
411
|
+
let response;
|
|
412
|
+
try {
|
|
413
|
+
response = await fetchEvidenceDocument(request.url, {
|
|
414
|
+
fetchImplementation, timeoutMs, maxResponseBytes
|
|
415
|
+
});
|
|
416
|
+
} catch {
|
|
417
|
+
return {
|
|
418
|
+
packageId: request.packageId,
|
|
419
|
+
source: sourceRecord(request, 'unavailable', null), evidence: [],
|
|
420
|
+
warnings: [warning(request, 'EVIDENCE_SOURCE_UNAVAILABLE', 'Official evidence source could not be retrieved.', true)]
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
if (response.status !== 200) {
|
|
424
|
+
return {
|
|
425
|
+
packageId: request.packageId,
|
|
426
|
+
source: sourceRecord(request, response.status === 404 ? 'notFound' : 'unavailable', null), evidence: [],
|
|
427
|
+
warnings: [warning(request, 'RELEASE_EVIDENCE_NOT_FOUND', 'Official release or migration evidence was not found at this source.', false)]
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
document = portableDocument(request, response);
|
|
431
|
+
const stored = await cache.write(identity, document, { ttlMs });
|
|
432
|
+
retrievedAt = stored.storedAt;
|
|
433
|
+
}
|
|
434
|
+
const normalizedSnapshot = normalizeEvidenceContent(document.text, {
|
|
435
|
+
mediaType: document.mediaType,
|
|
436
|
+
maxCharacters: DEFAULT_EVIDENCE_CONTENT_CHARACTERS
|
|
437
|
+
});
|
|
438
|
+
const evidence = request.format === 'githubReleases'
|
|
439
|
+
? evidenceFromGithubReleases(request, packageRecord, document, retrievedAt)
|
|
440
|
+
: evidenceFromDocument(request, packageRecord, document, retrievedAt);
|
|
441
|
+
const warnings = evidence.length === 0
|
|
442
|
+
? [warning(request, 'RELEASE_EVIDENCE_NOT_FOUND', 'Official source contained no versioned release or migration evidence.', false)]
|
|
443
|
+
: [];
|
|
444
|
+
return {
|
|
445
|
+
packageId: request.packageId,
|
|
446
|
+
source: sourceRecord(
|
|
447
|
+
request,
|
|
448
|
+
freshness === 'stale' ? 'stale' : 'available',
|
|
449
|
+
snapshot(normalizedSnapshot, document.mediaType, retrievedAt, freshness)
|
|
450
|
+
),
|
|
451
|
+
evidence,
|
|
452
|
+
warnings
|
|
453
|
+
};
|
|
454
|
+
});
|
|
455
|
+
return {
|
|
456
|
+
packageSources: [...new Set(outcomes.map((outcome) => outcome.packageId))].sort(compareText).map((packageId) => ({
|
|
457
|
+
packageId,
|
|
458
|
+
sourceIds: outcomes.filter((outcome) => outcome.packageId === packageId)
|
|
459
|
+
.map((outcome) => outcome.source.id).sort(compareText)
|
|
460
|
+
})),
|
|
461
|
+
sources: outcomes.map((outcome) => outcome.source).sort((left, right) => compareText(left.id, right.id)),
|
|
462
|
+
evidence: outcomes.flatMap((outcome) => outcome.evidence)
|
|
463
|
+
.filter((item, index, items) => items.findIndex((other) =>
|
|
464
|
+
other.packageId === item.packageId && other.contentDigest === item.contentDigest) === index)
|
|
465
|
+
.sort((left, right) => compareText(left.id, right.id)),
|
|
466
|
+
warnings: outcomes.flatMap((outcome) => outcome.warnings).sort((left, right) =>
|
|
467
|
+
compareText(left.packageId, right.packageId) || compareText(left.sourceId, right.sourceId)
|
|
468
|
+
|| compareText(left.code, right.code) || compareText(left.message, right.message))
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
};
|
|
472
|
+
}
|
package/src/files.js
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { readdir, readFile } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
import { DEFAULT_IGNORED_DIRECTORIES } from './constants.js';
|
|
5
|
+
|
|
6
|
+
const EXACT_CANDIDATES = new Set([
|
|
7
|
+
'Gemfile',
|
|
8
|
+
'app.json',
|
|
9
|
+
'build.gradle',
|
|
10
|
+
'build.gradle.kts',
|
|
11
|
+
'composer.json',
|
|
12
|
+
'Cargo.toml',
|
|
13
|
+
'go.mod',
|
|
14
|
+
'package.json',
|
|
15
|
+
'pnpm-workspace.yaml',
|
|
16
|
+
'pom.xml',
|
|
17
|
+
'pyproject.toml',
|
|
18
|
+
'requirements.txt',
|
|
19
|
+
'settings.gradle',
|
|
20
|
+
'settings.gradle.kts'
|
|
21
|
+
]);
|
|
22
|
+
|
|
23
|
+
const CANDIDATE_EXTENSIONS = ['.csproj', '.fsproj', '.vbproj', '.sln'];
|
|
24
|
+
|
|
25
|
+
export function toPosixPath(value) {
|
|
26
|
+
return value.split(path.sep).join('/');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function relativePath(root, target) {
|
|
30
|
+
const relative = toPosixPath(path.relative(root, target));
|
|
31
|
+
return relative || '.';
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function isCandidate(name) {
|
|
35
|
+
return EXACT_CANDIDATES.has(name) || CANDIDATE_EXTENSIONS.some((extension) => name.endsWith(extension));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function readText(filePath) {
|
|
39
|
+
return readFile(filePath, 'utf8');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function readJson(filePath) {
|
|
43
|
+
return JSON.parse(await readText(filePath));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function collectCandidateFiles(root, options = {}) {
|
|
47
|
+
const maxDepth = options.maxDepth ?? Number.POSITIVE_INFINITY;
|
|
48
|
+
const cooperativeScheduler = options.cooperativeScheduler;
|
|
49
|
+
const files = [];
|
|
50
|
+
const warnings = [];
|
|
51
|
+
|
|
52
|
+
async function visit(directory, depth) {
|
|
53
|
+
cooperativeScheduler?.checkpoint();
|
|
54
|
+
let entries;
|
|
55
|
+
try {
|
|
56
|
+
entries = await readdir(directory, { withFileTypes: true });
|
|
57
|
+
} catch (error) {
|
|
58
|
+
warnings.push({
|
|
59
|
+
code: 'DIRECTORY_UNREADABLE',
|
|
60
|
+
path: relativePath(root, directory),
|
|
61
|
+
message: `Unable to read directory (${error.code ?? 'unknown error'})`
|
|
62
|
+
});
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
entries.sort((left, right) => left.name.localeCompare(right.name));
|
|
67
|
+
|
|
68
|
+
for (const entry of entries) {
|
|
69
|
+
const entryPath = path.join(directory, entry.name);
|
|
70
|
+
try {
|
|
71
|
+
if (entry.isSymbolicLink()) continue;
|
|
72
|
+
|
|
73
|
+
if (entry.isDirectory()) {
|
|
74
|
+
if (depth < maxDepth && !DEFAULT_IGNORED_DIRECTORIES.has(entry.name)) {
|
|
75
|
+
await visit(entryPath, depth + 1);
|
|
76
|
+
}
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (entry.isFile() && isCandidate(entry.name)) files.push(entryPath);
|
|
81
|
+
} finally {
|
|
82
|
+
await cooperativeScheduler?.boundary();
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
await visit(root, 0);
|
|
88
|
+
return { files, warnings };
|
|
89
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { compareText } from './portable.js';
|
|
2
|
+
|
|
3
|
+
export const GOVERNANCE_VALIDATION_STAGES = Object.freeze({
|
|
4
|
+
LOAD_JSON: 1,
|
|
5
|
+
SCHEMA_VALIDATION: 2,
|
|
6
|
+
DIGEST_VERIFICATION: 3,
|
|
7
|
+
CROSS_ARTIFACT_VALIDATION: 4,
|
|
8
|
+
GOVERNANCE_POLICY_VALIDATION: 5,
|
|
9
|
+
TASK_SCOPE_VALIDATION: 6
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
export const GOVERNANCE_DIAGNOSTIC_CODES = Object.freeze([
|
|
13
|
+
'LOAD_ERROR',
|
|
14
|
+
'INVALID_JSON',
|
|
15
|
+
'INVALID_SCHEMA',
|
|
16
|
+
'DIGEST_MISMATCH',
|
|
17
|
+
'UNKNOWN_CAPABILITY',
|
|
18
|
+
'UNKNOWN_DEPLOYMENT',
|
|
19
|
+
'UNKNOWN_CONFORMANCE',
|
|
20
|
+
'UNKNOWN_QUALIFICATION',
|
|
21
|
+
'GOVERNANCE_POLICY_VIOLATION',
|
|
22
|
+
'TASK_SCOPE_MISMATCH',
|
|
23
|
+
'INVALID_CERTIFICATION_STATE',
|
|
24
|
+
'UNSUPPORTED_STATUS_TRANSITION'
|
|
25
|
+
]);
|
|
26
|
+
|
|
27
|
+
const CODE_SET = new Set(GOVERNANCE_DIAGNOSTIC_CODES);
|
|
28
|
+
const ARTIFACT_ORDER = new Map([
|
|
29
|
+
['capabilityProfile', 0],
|
|
30
|
+
['deploymentProfile', 1],
|
|
31
|
+
['conformanceReport', 2],
|
|
32
|
+
['qualificationRecord', 3],
|
|
33
|
+
['bundle', 4]
|
|
34
|
+
]);
|
|
35
|
+
|
|
36
|
+
export function createGovernanceDiagnostic({ stage, code, artifact, path = '$', message }) {
|
|
37
|
+
if (!Number.isInteger(stage) || stage < 1 || stage > 6) {
|
|
38
|
+
throw new TypeError('Governance diagnostic stage must be an integer from 1 through 6.');
|
|
39
|
+
}
|
|
40
|
+
if (!CODE_SET.has(code)) throw new TypeError(`Unknown governance diagnostic code: ${code}.`);
|
|
41
|
+
if (!ARTIFACT_ORDER.has(artifact)) throw new TypeError(`Unknown governance diagnostic artifact: ${artifact}.`);
|
|
42
|
+
if (typeof path !== 'string' || path.length === 0 || typeof message !== 'string' || message.length === 0) {
|
|
43
|
+
throw new TypeError('Governance diagnostic path and message are required.');
|
|
44
|
+
}
|
|
45
|
+
return { stage, code, artifact, path, message };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function compareDiagnostics(left, right) {
|
|
49
|
+
return left.stage - right.stage
|
|
50
|
+
|| ARTIFACT_ORDER.get(left.artifact) - ARTIFACT_ORDER.get(right.artifact)
|
|
51
|
+
|| compareText(left.code, right.code)
|
|
52
|
+
|| compareText(left.path, right.path)
|
|
53
|
+
|| compareText(left.message, right.message);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function sortGovernanceDiagnostics(diagnostics) {
|
|
57
|
+
const unique = new Map();
|
|
58
|
+
for (const diagnostic of diagnostics) {
|
|
59
|
+
const validated = createGovernanceDiagnostic(diagnostic);
|
|
60
|
+
const key = JSON.stringify(validated);
|
|
61
|
+
if (!unique.has(key)) unique.set(key, validated);
|
|
62
|
+
}
|
|
63
|
+
return [...unique.values()].sort(compareDiagnostics);
|
|
64
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
GOVERNANCE_VALIDATION_STAGES,
|
|
5
|
+
createGovernanceDiagnostic
|
|
6
|
+
} from './governance-diagnostics.js';
|
|
7
|
+
import { validateGovernanceArtifacts } from './governance-validator.js';
|
|
8
|
+
|
|
9
|
+
const ARTIFACTS = Object.freeze([
|
|
10
|
+
['capabilityProfile', 'UNKNOWN_CAPABILITY'],
|
|
11
|
+
['deploymentProfile', 'UNKNOWN_DEPLOYMENT'],
|
|
12
|
+
['conformanceReport', 'UNKNOWN_CONFORMANCE'],
|
|
13
|
+
['qualificationRecord', 'UNKNOWN_QUALIFICATION']
|
|
14
|
+
]);
|
|
15
|
+
|
|
16
|
+
async function loadJsonArtifact(filePath, artifact, unknownCode) {
|
|
17
|
+
if (typeof filePath !== 'string' || filePath.length === 0) {
|
|
18
|
+
return {
|
|
19
|
+
diagnostic: createGovernanceDiagnostic({
|
|
20
|
+
stage: GOVERNANCE_VALIDATION_STAGES.LOAD_JSON,
|
|
21
|
+
code: unknownCode,
|
|
22
|
+
artifact,
|
|
23
|
+
message: 'Required governance artifact path is missing.'
|
|
24
|
+
})
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
let text;
|
|
28
|
+
try {
|
|
29
|
+
text = await readFile(filePath, 'utf8');
|
|
30
|
+
} catch {
|
|
31
|
+
return {
|
|
32
|
+
diagnostic: createGovernanceDiagnostic({
|
|
33
|
+
stage: GOVERNANCE_VALIDATION_STAGES.LOAD_JSON,
|
|
34
|
+
code: unknownCode,
|
|
35
|
+
artifact,
|
|
36
|
+
message: 'Required governance artifact could not be loaded.'
|
|
37
|
+
})
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
try {
|
|
41
|
+
return { value: JSON.parse(text) };
|
|
42
|
+
} catch {
|
|
43
|
+
return {
|
|
44
|
+
diagnostic: createGovernanceDiagnostic({
|
|
45
|
+
stage: GOVERNANCE_VALIDATION_STAGES.LOAD_JSON,
|
|
46
|
+
code: 'INVALID_JSON',
|
|
47
|
+
artifact,
|
|
48
|
+
message: 'Governance artifact is not valid JSON.'
|
|
49
|
+
})
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function loadGovernanceBundle(paths, options = {}) {
|
|
55
|
+
const artifacts = {};
|
|
56
|
+
const initialDiagnostics = [];
|
|
57
|
+
for (const [artifact, unknownCode] of ARTIFACTS) {
|
|
58
|
+
const loaded = await loadJsonArtifact(paths?.[artifact], artifact, unknownCode);
|
|
59
|
+
if (loaded.diagnostic) initialDiagnostics.push(loaded.diagnostic);
|
|
60
|
+
else artifacts[artifact] = loaded.value;
|
|
61
|
+
}
|
|
62
|
+
return validateGovernanceArtifacts(artifacts, { ...options, initialDiagnostics });
|
|
63
|
+
}
|