@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,233 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { mkdir, open, rename, rm } from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
import { canonicalJson } from './canonical-json.js';
|
|
6
|
+
import { ARTIFACT_GENERATOR_NAME, VERSION } from './constants.js';
|
|
7
|
+
import {
|
|
8
|
+
KNOWLEDGE_EVIDENCE_BUNDLE_SCHEMA_VERSION,
|
|
9
|
+
validateKnowledgeEvidenceBundle
|
|
10
|
+
} from './knowledge-evidence-bundle.js';
|
|
11
|
+
import { validateKnowledgeManifest } from './knowledge-manifest-builder.js';
|
|
12
|
+
import { compareText } from './portable.js';
|
|
13
|
+
|
|
14
|
+
function digestBytes(bytes) {
|
|
15
|
+
return `sha256:${createHash('sha256').update(bytes).digest('hex')}`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function digestText(value) {
|
|
19
|
+
return digestBytes(Buffer.from(value, 'utf8'));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function digestJson(value) {
|
|
23
|
+
return digestText(canonicalJson(value));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function textValue(value) {
|
|
27
|
+
if (value === null || value === undefined) return 'unknown';
|
|
28
|
+
if (typeof value === 'boolean') return value ? 'true' : 'false';
|
|
29
|
+
return String(value);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function sourceFor(manifest, sourceId) {
|
|
33
|
+
return manifest.sources.find((source) => source.id === sourceId) ?? null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function retrievedAtFor(manifest, source) {
|
|
37
|
+
return source?.snapshot?.retrievedAt ?? manifest.generatedAt;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function latestEvidenceContent(packageRecord) {
|
|
41
|
+
return [
|
|
42
|
+
`Package: ${packageRecord.id}`,
|
|
43
|
+
`Ecosystem: ${packageRecord.ecosystem}`,
|
|
44
|
+
`Registry: ${packageRecord.identity.registry}`,
|
|
45
|
+
`Normalized name: ${packageRecord.identity.normalizedName}`,
|
|
46
|
+
`Latest version: ${packageRecord.latest.version}`,
|
|
47
|
+
`Latest selection: ${packageRecord.latest.selection}`,
|
|
48
|
+
`Published at: ${textValue(packageRecord.latest.publishedAt)}`,
|
|
49
|
+
`Release URL: ${textValue(packageRecord.latest.releaseUrl)}`,
|
|
50
|
+
`Prerelease: ${textValue(packageRecord.latest.prerelease)}`,
|
|
51
|
+
`Yanked: ${textValue(packageRecord.latest.yanked)}`,
|
|
52
|
+
`Deprecated: ${textValue(packageRecord.latest.deprecated)}`,
|
|
53
|
+
`Package description: ${textValue(packageRecord.metadata.description)}`,
|
|
54
|
+
`License: ${textValue(packageRecord.metadata.license)}`,
|
|
55
|
+
`Deprecation message: ${textValue(packageRecord.metadata.deprecationMessage)}`,
|
|
56
|
+
`Project status: ${textValue(packageRecord.metadata.projectStatus)}`
|
|
57
|
+
].join('\n');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function evidenceId(material) {
|
|
61
|
+
return digestJson(material);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function buildLatestEvidence(manifest, packageRecord) {
|
|
65
|
+
if (!packageRecord.latest?.version || !packageRecord.latest?.sourceId) return null;
|
|
66
|
+
const source = sourceFor(manifest, packageRecord.latest.sourceId);
|
|
67
|
+
if (!source || !['available', 'stale'].includes(source.status)) return null;
|
|
68
|
+
const content = latestEvidenceContent(packageRecord);
|
|
69
|
+
const contentDigest = digestText(content);
|
|
70
|
+
const releaseVersions = [packageRecord.latest.version].sort(compareText);
|
|
71
|
+
const locator = `registry-latest:${packageRecord.latest.version}`;
|
|
72
|
+
const material = {
|
|
73
|
+
packageId: packageRecord.id,
|
|
74
|
+
sourceId: packageRecord.latest.sourceId,
|
|
75
|
+
kind: 'registryFact',
|
|
76
|
+
locator,
|
|
77
|
+
releaseVersions,
|
|
78
|
+
contentDigest
|
|
79
|
+
};
|
|
80
|
+
return {
|
|
81
|
+
id: evidenceId(material),
|
|
82
|
+
packageId: packageRecord.id,
|
|
83
|
+
sourceId: packageRecord.latest.sourceId,
|
|
84
|
+
kind: 'registryFact',
|
|
85
|
+
contentDigest,
|
|
86
|
+
retrievedAt: retrievedAtFor(manifest, source),
|
|
87
|
+
mediaType: 'text/plain',
|
|
88
|
+
locator,
|
|
89
|
+
releaseVersions,
|
|
90
|
+
content
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function compareEvidence(left, right) {
|
|
95
|
+
return compareText(left.id, right.id);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function compareWarnings(left, right) {
|
|
99
|
+
return compareText(left.packageId ?? '', right.packageId ?? '')
|
|
100
|
+
|| compareText(left.sourceId ?? '', right.sourceId ?? '')
|
|
101
|
+
|| compareText(left.code, right.code)
|
|
102
|
+
|| compareText(left.message, right.message);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function bundleWarningFromManifest(warning) {
|
|
106
|
+
return {
|
|
107
|
+
code: warning.code,
|
|
108
|
+
...(warning.packageId ? { packageId: warning.packageId } : {}),
|
|
109
|
+
...(warning.sourceId ? { sourceId: warning.sourceId } : {}),
|
|
110
|
+
message: warning.message
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function evidenceMissingWarning(packageRecord) {
|
|
115
|
+
return {
|
|
116
|
+
code: 'EVIDENCE_MISSING',
|
|
117
|
+
packageId: packageRecord.id,
|
|
118
|
+
message: `No portable evidence could be produced for ${packageRecord.id}.`
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function sortedUniqueWarnings(warnings) {
|
|
123
|
+
const seen = new Set();
|
|
124
|
+
const unique = [];
|
|
125
|
+
for (const warning of warnings.sort(compareWarnings)) {
|
|
126
|
+
const key = [
|
|
127
|
+
warning.packageId ?? '',
|
|
128
|
+
warning.sourceId ?? '',
|
|
129
|
+
warning.code,
|
|
130
|
+
warning.message
|
|
131
|
+
].join('\0');
|
|
132
|
+
if (!seen.has(key)) {
|
|
133
|
+
seen.add(key);
|
|
134
|
+
unique.push(warning);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return unique;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function summaryFor(evidence, warnings) {
|
|
141
|
+
return {
|
|
142
|
+
evidenceCount: evidence.length,
|
|
143
|
+
packageCount: new Set(evidence.map((item) => item.packageId)).size,
|
|
144
|
+
sourceCount: new Set(evidence.map((item) => item.sourceId)).size,
|
|
145
|
+
warningCount: warnings.length
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function buildKnowledgeEvidenceBundle(manifest, {
|
|
150
|
+
knowledgeManifestArtifact,
|
|
151
|
+
knowledgeManifestBytes,
|
|
152
|
+
generatedAt,
|
|
153
|
+
enrichedEvidence = []
|
|
154
|
+
}) {
|
|
155
|
+
validateKnowledgeManifest(manifest);
|
|
156
|
+
if (!(knowledgeManifestBytes instanceof Uint8Array)) {
|
|
157
|
+
throw new Error('Knowledge Evidence Bundle producer requires Knowledge Manifest bytes.');
|
|
158
|
+
}
|
|
159
|
+
if (!knowledgeManifestArtifact) {
|
|
160
|
+
throw new Error('Knowledge Evidence Bundle producer requires Knowledge Manifest artifact path.');
|
|
161
|
+
}
|
|
162
|
+
if (!Array.isArray(enrichedEvidence)) {
|
|
163
|
+
throw new Error('Knowledge Evidence Bundle producer requires enrichedEvidence to be an array.');
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const evidence = [];
|
|
167
|
+
const warnings = (manifest.warnings ?? []).map(bundleWarningFromManifest);
|
|
168
|
+
for (const packageRecord of manifest.packages) {
|
|
169
|
+
const item = buildLatestEvidence(manifest, packageRecord);
|
|
170
|
+
if (item) evidence.push(item);
|
|
171
|
+
else if (!['invalid', 'notFound'].includes(packageRecord.status)) warnings.push(evidenceMissingWarning(packageRecord));
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const packageIds = new Set(manifest.packages.map((item) => item.id));
|
|
175
|
+
const sourceIds = new Set(manifest.sources.map((item) => item.id));
|
|
176
|
+
const seenContent = new Set(evidence.map((item) => `${item.packageId}\0${item.contentDigest}`));
|
|
177
|
+
for (const item of enrichedEvidence) {
|
|
178
|
+
if (!packageIds.has(item?.packageId) || !sourceIds.has(item?.sourceId)) {
|
|
179
|
+
throw new Error('Knowledge Evidence Bundle producer received enriched evidence with unknown provenance.');
|
|
180
|
+
}
|
|
181
|
+
const contentKey = `${item.packageId}\0${item.contentDigest}`;
|
|
182
|
+
if (seenContent.has(contentKey)) continue;
|
|
183
|
+
seenContent.add(contentKey);
|
|
184
|
+
evidence.push(structuredClone(item));
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
evidence.sort(compareEvidence);
|
|
188
|
+
const sortedWarnings = sortedUniqueWarnings(warnings);
|
|
189
|
+
const bundle = {
|
|
190
|
+
schemaVersion: KNOWLEDGE_EVIDENCE_BUNDLE_SCHEMA_VERSION,
|
|
191
|
+
generatedAt: generatedAt ?? manifest.generatedAt,
|
|
192
|
+
generator: { name: ARTIFACT_GENERATOR_NAME, version: VERSION },
|
|
193
|
+
input: {
|
|
194
|
+
knowledgeManifest: {
|
|
195
|
+
schemaVersion: manifest.schemaVersion,
|
|
196
|
+
artifact: knowledgeManifestArtifact,
|
|
197
|
+
artifactDigest: digestBytes(Buffer.from(knowledgeManifestBytes)),
|
|
198
|
+
researchId: manifest.research.researchId
|
|
199
|
+
}
|
|
200
|
+
},
|
|
201
|
+
summary: summaryFor(evidence, sortedWarnings),
|
|
202
|
+
evidence,
|
|
203
|
+
warnings: sortedWarnings
|
|
204
|
+
};
|
|
205
|
+
return validateKnowledgeEvidenceBundle(bundle);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export function serializeKnowledgeEvidenceBundle(bundle) {
|
|
209
|
+
validateKnowledgeEvidenceBundle(bundle);
|
|
210
|
+
return `${JSON.stringify(bundle, null, 2)}\n`;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export async function writeKnowledgeEvidenceBundle(outputPath, bundle) {
|
|
214
|
+
validateKnowledgeEvidenceBundle(bundle);
|
|
215
|
+
const target = path.resolve(outputPath);
|
|
216
|
+
const temporary = `${target}.${process.pid}.tmp`;
|
|
217
|
+
const contents = serializeKnowledgeEvidenceBundle(bundle);
|
|
218
|
+
await mkdir(path.dirname(target), { recursive: true });
|
|
219
|
+
let handle;
|
|
220
|
+
try {
|
|
221
|
+
handle = await open(temporary, 'w', 0o600);
|
|
222
|
+
await handle.writeFile(contents, 'utf8');
|
|
223
|
+
await handle.sync();
|
|
224
|
+
await handle.close();
|
|
225
|
+
handle = null;
|
|
226
|
+
await rename(temporary, target);
|
|
227
|
+
} catch (error) {
|
|
228
|
+
await handle?.close().catch(() => {});
|
|
229
|
+
await rm(temporary, { force: true }).catch(() => {});
|
|
230
|
+
throw error;
|
|
231
|
+
}
|
|
232
|
+
return target;
|
|
233
|
+
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
|
|
4
|
+
import Ajv2020 from 'ajv/dist/2020.js';
|
|
5
|
+
import addFormats from 'ajv-formats';
|
|
6
|
+
|
|
7
|
+
import { canonicalJson } from './canonical-json.js';
|
|
8
|
+
import {
|
|
9
|
+
ARTIFACT_GENERATOR_NAME,
|
|
10
|
+
KNOWLEDGE_MANIFEST_SCHEMA_VERSION,
|
|
11
|
+
VERSION
|
|
12
|
+
} from './constants.js';
|
|
13
|
+
import { validateKnowledgeManifestInvariants } from './knowledge-manifest.js';
|
|
14
|
+
|
|
15
|
+
const schema = JSON.parse(await readFile(
|
|
16
|
+
new URL('../schemas/knowledge-manifest.schema.json', import.meta.url), 'utf8'
|
|
17
|
+
));
|
|
18
|
+
const ajv = new Ajv2020({ allErrors: true, strict: true, strictRequired: false });
|
|
19
|
+
addFormats(ajv);
|
|
20
|
+
const validateSchema = ajv.compile(schema);
|
|
21
|
+
|
|
22
|
+
function digest(value) {
|
|
23
|
+
return `sha256:${createHash('sha256').update(canonicalJson(value), 'utf8').digest('hex')}`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function clone(value) {
|
|
27
|
+
return structuredClone(value);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function date(value, field) {
|
|
31
|
+
const parsed = new Date(value);
|
|
32
|
+
if (Number.isNaN(parsed.getTime())) throw new Error(`Knowledge Manifest builder requires a valid ${field}.`);
|
|
33
|
+
return parsed.toISOString();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function publicOccurrence(occurrence) {
|
|
37
|
+
return {
|
|
38
|
+
projectId: occurrence.projectId,
|
|
39
|
+
projectPath: occurrence.projectPath,
|
|
40
|
+
manifest: occurrence.manifest,
|
|
41
|
+
dependencyType: occurrence.dependencyType,
|
|
42
|
+
declaredName: occurrence.declaredName,
|
|
43
|
+
declaredVersion: occurrence.declaredVersion
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function publicPackage(packageRecord) {
|
|
48
|
+
return {
|
|
49
|
+
id: packageRecord.id,
|
|
50
|
+
ecosystem: packageRecord.ecosystem,
|
|
51
|
+
status: packageRecord.status,
|
|
52
|
+
identity: clone(packageRecord.identity),
|
|
53
|
+
occurrences: packageRecord.occurrences.map(publicOccurrence),
|
|
54
|
+
metadata: clone(packageRecord.metadata),
|
|
55
|
+
latest: clone(packageRecord.latest),
|
|
56
|
+
releaseIndex: clone(packageRecord.releaseIndex),
|
|
57
|
+
sourceIds: clone(packageRecord.sourceIds),
|
|
58
|
+
warningCodes: clone(packageRecord.warningCodes)
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function defaultPolicy(mode) {
|
|
63
|
+
return {
|
|
64
|
+
mode,
|
|
65
|
+
policyVersion: '1',
|
|
66
|
+
registryBases: { npm: 'https://registry.npmjs.org', pypi: 'https://pypi.org' },
|
|
67
|
+
ttlPolicyVersion: '1',
|
|
68
|
+
sourceAllowlistVersion: '1',
|
|
69
|
+
includePrereleases: false
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function compareText(left = '', right = '') {
|
|
74
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function compareWarnings(left, right) {
|
|
78
|
+
return compareText(left.packageId ?? '', right.packageId ?? '')
|
|
79
|
+
|| compareText(left.code, right.code)
|
|
80
|
+
|| compareText(left.sourceId ?? '', right.sourceId ?? '')
|
|
81
|
+
|| compareText(left.message, right.message);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function policyFor(value, mode) {
|
|
85
|
+
const policy = { ...defaultPolicy(mode), ...(value ?? {}) };
|
|
86
|
+
policy.mode = mode;
|
|
87
|
+
policy.registryBases = { ...defaultPolicy(mode).registryBases, ...(value?.registryBases ?? {}) };
|
|
88
|
+
return policy;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function sourceDigest(source) {
|
|
92
|
+
return source.snapshot?.contentDigest ?? digest({
|
|
93
|
+
id: source.id,
|
|
94
|
+
kind: source.kind,
|
|
95
|
+
authority: source.authority,
|
|
96
|
+
trust: source.trust,
|
|
97
|
+
url: source.url,
|
|
98
|
+
status: source.status,
|
|
99
|
+
supports: source.supports,
|
|
100
|
+
discoveredFrom: source.discoveredFrom,
|
|
101
|
+
trustEvidenceSourceIds: source.trustEvidenceSourceIds,
|
|
102
|
+
conflictsWith: source.conflictsWith ?? []
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Deterministic identity for one input lineage, policy, and source set. */
|
|
107
|
+
export function createResearchId(input, policy, sources) {
|
|
108
|
+
return digest({
|
|
109
|
+
projectManifestDigest: input.projectManifest.artifactDigest,
|
|
110
|
+
policyFingerprint: digest(policy),
|
|
111
|
+
sourceDigests: [...sources]
|
|
112
|
+
.sort((left, right) => left.id < right.id ? -1 : left.id > right.id ? 1 : 0)
|
|
113
|
+
.map(sourceDigest)
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Schema and relational validation for the public Knowledge Manifest. */
|
|
118
|
+
export function validateKnowledgeManifest(manifest) {
|
|
119
|
+
if (!validateSchema(manifest)) {
|
|
120
|
+
throw new Error(`Knowledge Manifest schema validation failed: ${ajv.errorsText(validateSchema.errors, { separator: '; ' })}`);
|
|
121
|
+
}
|
|
122
|
+
const invariantErrors = validateKnowledgeManifestInvariants(manifest);
|
|
123
|
+
if (invariantErrors.length > 0) {
|
|
124
|
+
throw new Error(`Knowledge Manifest runtime invariants failed: ${invariantErrors.join(' ')}`);
|
|
125
|
+
}
|
|
126
|
+
return manifest;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Convert one validated internal KnowledgeResearchResult into public schema 1.0.0. */
|
|
130
|
+
export function buildKnowledgeManifest(researchResult, { policy, generatedAt } = {}) {
|
|
131
|
+
if (!researchResult || researchResult.resultVersion !== '1') {
|
|
132
|
+
throw new Error('Knowledge Manifest builder requires KnowledgeResearchResult version 1.');
|
|
133
|
+
}
|
|
134
|
+
const mode = policy?.mode ?? 'online';
|
|
135
|
+
const publicPolicy = policyFor(policy, mode);
|
|
136
|
+
const packages = researchResult.packages.map(publicPackage).sort((left, right) => compareText(left.id, right.id));
|
|
137
|
+
const sources = clone(researchResult.sources).sort((left, right) => compareText(left.id, right.id));
|
|
138
|
+
const warnings = clone(researchResult.warnings).sort(compareWarnings);
|
|
139
|
+
const completedAt = date(researchResult.execution.completedAt, 'execution completedAt');
|
|
140
|
+
const manifest = {
|
|
141
|
+
schemaVersion: KNOWLEDGE_MANIFEST_SCHEMA_VERSION,
|
|
142
|
+
generatedAt: generatedAt ? date(generatedAt, 'generatedAt') : completedAt,
|
|
143
|
+
generator: { name: ARTIFACT_GENERATOR_NAME, version: VERSION },
|
|
144
|
+
input: { projectManifest: clone(researchResult.input.projectManifest) },
|
|
145
|
+
policy: publicPolicy,
|
|
146
|
+
research: {
|
|
147
|
+
researchId: createResearchId({ projectManifest: researchResult.input.projectManifest }, publicPolicy, sources),
|
|
148
|
+
startedAt: date(researchResult.execution.startedAt, 'execution startedAt'),
|
|
149
|
+
completedAt,
|
|
150
|
+
durationMs: researchResult.execution.durationMs,
|
|
151
|
+
inputOccurrenceCount: researchResult.summary.inputOccurrenceCount,
|
|
152
|
+
inputPackageCount: packages.length,
|
|
153
|
+
researchedPackageCount: packages.filter((item) => item.status !== 'invalid').length,
|
|
154
|
+
sourceCount: sources.length,
|
|
155
|
+
cacheHitCount: researchResult.summary.cacheHitCount,
|
|
156
|
+
cacheMissCount: researchResult.summary.cacheMissCount,
|
|
157
|
+
cacheRevalidationCount: researchResult.summary.cacheRevalidationCount,
|
|
158
|
+
retryCount: researchResult.summary.retryCount,
|
|
159
|
+
partialFailureCount: researchResult.summary.partialFailureCount
|
|
160
|
+
},
|
|
161
|
+
summary: {
|
|
162
|
+
inputOccurrenceCount: researchResult.summary.inputOccurrenceCount,
|
|
163
|
+
packageCount: packages.length,
|
|
164
|
+
resolvedPackageCount: researchResult.summary.resolvedPackageCount,
|
|
165
|
+
partialPackageCount: researchResult.summary.partialPackageCount,
|
|
166
|
+
notFoundPackageCount: researchResult.summary.notFoundPackageCount,
|
|
167
|
+
invalidPackageCount: researchResult.summary.invalidPackageCount,
|
|
168
|
+
unavailablePackageCount: researchResult.summary.unavailablePackageCount,
|
|
169
|
+
sourceCount: sources.length,
|
|
170
|
+
warningCount: warnings.length,
|
|
171
|
+
cacheHitCount: researchResult.summary.cacheHitCount,
|
|
172
|
+
cacheMissCount: researchResult.summary.cacheMissCount,
|
|
173
|
+
staleSourceCount: sources.filter((source) => source.status === 'stale').length
|
|
174
|
+
},
|
|
175
|
+
packages,
|
|
176
|
+
sources,
|
|
177
|
+
cache: {
|
|
178
|
+
mode: publicPolicy.mode,
|
|
179
|
+
policyVersion: publicPolicy.ttlPolicyVersion,
|
|
180
|
+
hitCount: researchResult.summary.cacheHitCount,
|
|
181
|
+
missCount: researchResult.summary.cacheMissCount,
|
|
182
|
+
revalidationCount: researchResult.summary.cacheRevalidationCount,
|
|
183
|
+
staleEntryCount: sources.filter((source) => source.status === 'stale').length
|
|
184
|
+
},
|
|
185
|
+
warnings
|
|
186
|
+
};
|
|
187
|
+
return validateKnowledgeManifest(manifest);
|
|
188
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { mkdir, open, rename, rm } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
import { validateKnowledgeManifest } from './knowledge-manifest-builder.js';
|
|
5
|
+
|
|
6
|
+
/** Serialize a validated public manifest as stable UTF-8 pretty JSON. */
|
|
7
|
+
export function serializeKnowledgeManifest(manifest) {
|
|
8
|
+
return `${JSON.stringify(manifest, null, 2)}\n`;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Atomically replace a manifest using write, flush, close, and rename. */
|
|
12
|
+
export async function writeKnowledgeManifest(outputPath, manifest) {
|
|
13
|
+
validateKnowledgeManifest(manifest);
|
|
14
|
+
const target = path.resolve(outputPath);
|
|
15
|
+
const temporary = `${target}.${process.pid}.tmp`;
|
|
16
|
+
const contents = serializeKnowledgeManifest(manifest);
|
|
17
|
+
await mkdir(path.dirname(target), { recursive: true });
|
|
18
|
+
let handle;
|
|
19
|
+
try {
|
|
20
|
+
handle = await open(temporary, 'w', 0o600);
|
|
21
|
+
await handle.writeFile(contents, 'utf8');
|
|
22
|
+
await handle.sync();
|
|
23
|
+
await handle.close();
|
|
24
|
+
handle = null;
|
|
25
|
+
await rename(temporary, target);
|
|
26
|
+
} catch (error) {
|
|
27
|
+
await handle?.close().catch(() => {});
|
|
28
|
+
await rm(temporary, { force: true }).catch(() => {});
|
|
29
|
+
throw error;
|
|
30
|
+
}
|
|
31
|
+
return target;
|
|
32
|
+
}
|