@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,203 @@
|
|
|
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 {
|
|
8
|
+
DEFAULT_OUTPUT_DIRECTORY,
|
|
9
|
+
DEFAULT_KNOWLEDGE_MANIFEST_PATH,
|
|
10
|
+
DEFAULT_MANIFEST_PATH,
|
|
11
|
+
KNOWLEDGE_MANIFEST_SCHEMA_VERSION
|
|
12
|
+
} from './constants.js';
|
|
13
|
+
import { validateKnowledgeEvidenceBundle } from './knowledge-evidence-bundle.js';
|
|
14
|
+
import { validateKnowledgeManifestInvariants } from './knowledge-manifest.js';
|
|
15
|
+
import { isPortableRelativePath } from './portable.js';
|
|
16
|
+
import { loadProjectManifestInput } from './project-manifest-input.js';
|
|
17
|
+
|
|
18
|
+
export const DEFAULT_KNOWLEDGE_EVIDENCE_BUNDLE_PATH =
|
|
19
|
+
`${DEFAULT_OUTPUT_DIRECTORY}/knowledge-evidence-bundle.json`;
|
|
20
|
+
|
|
21
|
+
const knowledgeManifestSchema = JSON.parse(await readFile(
|
|
22
|
+
new URL('../schemas/knowledge-manifest.schema.json', import.meta.url),
|
|
23
|
+
'utf8'
|
|
24
|
+
));
|
|
25
|
+
const ajv = new Ajv2020({ allErrors: true, strict: true, strictRequired: false });
|
|
26
|
+
addFormats(ajv);
|
|
27
|
+
const validateKnowledgeManifestSchema = ajv.compile(knowledgeManifestSchema);
|
|
28
|
+
|
|
29
|
+
export class VersionAnalysisInputError extends Error {
|
|
30
|
+
constructor(message, code = 'VERSION_ANALYSIS_INPUT_INVALID') {
|
|
31
|
+
super(`Version Analysis input error: ${message}`);
|
|
32
|
+
this.name = 'VersionAnalysisInputError';
|
|
33
|
+
this.code = code;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function inputError(message, code) {
|
|
38
|
+
return new VersionAnalysisInputError(message, code);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function digest(bytes) {
|
|
42
|
+
return `sha256:${createHash('sha256').update(bytes).digest('hex')}`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function parseJson(bytes, artifactType) {
|
|
46
|
+
try {
|
|
47
|
+
return JSON.parse(Buffer.from(bytes).toString('utf8'));
|
|
48
|
+
} catch {
|
|
49
|
+
throw inputError(`${artifactType} bytes are not valid JSON.`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function artifactFor(source, options, defaultArtifact, artifactType) {
|
|
54
|
+
const artifact = typeof source === 'object' && source !== null && 'bytes' in source
|
|
55
|
+
? source.artifact
|
|
56
|
+
: options.artifact ?? defaultArtifact;
|
|
57
|
+
if (!isPortableRelativePath(artifact)) {
|
|
58
|
+
throw inputError(`${artifactType} artifact must be a portable repository-relative path.`);
|
|
59
|
+
}
|
|
60
|
+
return artifact;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function readInputBytes(source, artifactType) {
|
|
64
|
+
if (typeof source === 'string' || source instanceof URL) return readFile(source);
|
|
65
|
+
if (source && typeof source === 'object' && 'bytes' in source) {
|
|
66
|
+
if (!(source.bytes instanceof Uint8Array)) {
|
|
67
|
+
throw inputError(`${artifactType} bytes must be a Uint8Array or Buffer.`);
|
|
68
|
+
}
|
|
69
|
+
return Buffer.from(source.bytes);
|
|
70
|
+
}
|
|
71
|
+
throw inputError(`${artifactType} source must be a file path or { bytes, artifact }.`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export async function loadKnowledgeManifestInput(source, options = {}) {
|
|
75
|
+
const artifact = artifactFor(source, options, DEFAULT_KNOWLEDGE_MANIFEST_PATH, 'Knowledge Manifest');
|
|
76
|
+
const bytes = await readInputBytes(source, 'Knowledge Manifest');
|
|
77
|
+
const manifest = parseJson(bytes, 'Knowledge Manifest');
|
|
78
|
+
|
|
79
|
+
if (manifest?.schemaVersion !== KNOWLEDGE_MANIFEST_SCHEMA_VERSION) {
|
|
80
|
+
throw inputError(`unsupported Knowledge Manifest schema version; expected ${KNOWLEDGE_MANIFEST_SCHEMA_VERSION}.`);
|
|
81
|
+
}
|
|
82
|
+
if (!validateKnowledgeManifestSchema(manifest)) {
|
|
83
|
+
throw inputError(
|
|
84
|
+
`Knowledge Manifest schema validation failed: ${ajv.errorsText(validateKnowledgeManifestSchema.errors, { separator: '; ' })}`
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
const invariantErrors = validateKnowledgeManifestInvariants(manifest);
|
|
88
|
+
if (invariantErrors.length > 0) {
|
|
89
|
+
throw inputError(`Knowledge Manifest runtime invariants failed: ${invariantErrors.join(' ')}`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
manifest,
|
|
94
|
+
input: {
|
|
95
|
+
knowledgeManifest: {
|
|
96
|
+
schemaVersion: manifest.schemaVersion,
|
|
97
|
+
artifact,
|
|
98
|
+
artifactDigest: digest(bytes),
|
|
99
|
+
researchId: manifest.research.researchId
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export async function loadKnowledgeEvidenceBundleInput(source, options = {}) {
|
|
106
|
+
const artifact = artifactFor(source, options, DEFAULT_KNOWLEDGE_EVIDENCE_BUNDLE_PATH, 'Knowledge Evidence Bundle');
|
|
107
|
+
const bytes = await readInputBytes(source, 'Knowledge Evidence Bundle');
|
|
108
|
+
const bundle = parseJson(bytes, 'Knowledge Evidence Bundle');
|
|
109
|
+
|
|
110
|
+
if (bundle?.schemaVersion !== '1.0.0') {
|
|
111
|
+
throw inputError('unsupported Knowledge Evidence Bundle schema version; expected 1.0.0.');
|
|
112
|
+
}
|
|
113
|
+
try {
|
|
114
|
+
validateKnowledgeEvidenceBundle(bundle);
|
|
115
|
+
} catch (error) {
|
|
116
|
+
throw inputError(error.message);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
bundle,
|
|
121
|
+
input: {
|
|
122
|
+
evidenceArtifact: {
|
|
123
|
+
schemaVersion: bundle.schemaVersion,
|
|
124
|
+
artifact,
|
|
125
|
+
artifactDigest: digest(bytes)
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function assertProjectKnowledgeLineage(projectInput, knowledgeInput, knowledgeManifest) {
|
|
132
|
+
const expected = knowledgeManifest.input.projectManifest;
|
|
133
|
+
const actual = projectInput.projectManifest;
|
|
134
|
+
const mismatches = [];
|
|
135
|
+
if (expected.schemaVersion !== actual.schemaVersion) mismatches.push('Project Manifest schemaVersion');
|
|
136
|
+
if (expected.artifactDigest !== actual.artifactDigest) mismatches.push('Project Manifest artifactDigest');
|
|
137
|
+
if (expected.repository.name !== actual.repository.name) mismatches.push('Project Manifest repository.name');
|
|
138
|
+
if (expected.repository.root !== actual.repository.root) mismatches.push('Project Manifest repository.root');
|
|
139
|
+
if (mismatches.length > 0) {
|
|
140
|
+
throw inputError(`Knowledge Manifest lineage mismatch: ${mismatches.join(', ')}.`, 'LINEAGE_MISMATCH');
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function assertEvidenceKnowledgeLineage(knowledgeInput, evidenceBundle) {
|
|
145
|
+
const expected = evidenceBundle.input.knowledgeManifest;
|
|
146
|
+
const actual = knowledgeInput.knowledgeManifest;
|
|
147
|
+
const mismatches = [];
|
|
148
|
+
if (expected.schemaVersion !== actual.schemaVersion) mismatches.push('Knowledge Manifest schemaVersion');
|
|
149
|
+
if (expected.artifactDigest !== actual.artifactDigest) mismatches.push('Knowledge Manifest artifactDigest');
|
|
150
|
+
if (expected.researchId !== actual.researchId) mismatches.push('Knowledge Manifest researchId');
|
|
151
|
+
if (mismatches.length > 0) {
|
|
152
|
+
throw inputError(`Knowledge Evidence Bundle lineage mismatch: ${mismatches.join(', ')}.`, 'LINEAGE_MISMATCH');
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function assertEvidenceReferences(knowledgeManifest, evidenceBundle) {
|
|
157
|
+
const packageIds = new Set(knowledgeManifest.packages.map((item) => item.id));
|
|
158
|
+
const sourceIds = new Set(knowledgeManifest.sources.map((item) => item.id));
|
|
159
|
+
const errors = [];
|
|
160
|
+
for (const item of evidenceBundle.evidence) {
|
|
161
|
+
if (!packageIds.has(item.packageId)) errors.push(`Evidence ${item.id} references unknown package ${item.packageId}.`);
|
|
162
|
+
if (!sourceIds.has(item.sourceId)) errors.push(`Evidence ${item.id} references unknown source ${item.sourceId}.`);
|
|
163
|
+
}
|
|
164
|
+
for (const warning of evidenceBundle.warnings) {
|
|
165
|
+
if (warning.packageId && !packageIds.has(warning.packageId)) {
|
|
166
|
+
errors.push(`Evidence warning ${warning.code} references unknown package ${warning.packageId}.`);
|
|
167
|
+
}
|
|
168
|
+
if (warning.sourceId && !sourceIds.has(warning.sourceId)) {
|
|
169
|
+
errors.push(`Evidence warning ${warning.code} references unknown source ${warning.sourceId}.`);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
if (errors.length > 0) throw inputError(errors.join(' '));
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export async function loadVersionAnalysisArtifacts(sources, options = {}) {
|
|
176
|
+
const project = await loadProjectManifestInput(
|
|
177
|
+
sources.projectManifest,
|
|
178
|
+
{ artifact: options.projectManifestArtifact ?? DEFAULT_MANIFEST_PATH }
|
|
179
|
+
);
|
|
180
|
+
const knowledge = await loadKnowledgeManifestInput(
|
|
181
|
+
sources.knowledgeManifest,
|
|
182
|
+
{ artifact: options.knowledgeManifestArtifact ?? DEFAULT_KNOWLEDGE_MANIFEST_PATH }
|
|
183
|
+
);
|
|
184
|
+
const evidence = await loadKnowledgeEvidenceBundleInput(
|
|
185
|
+
sources.evidenceBundle,
|
|
186
|
+
{ artifact: options.evidenceBundleArtifact ?? DEFAULT_KNOWLEDGE_EVIDENCE_BUNDLE_PATH }
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
assertProjectKnowledgeLineage(project.input, knowledge.input, knowledge.manifest);
|
|
190
|
+
assertEvidenceKnowledgeLineage(knowledge.input, evidence.bundle);
|
|
191
|
+
assertEvidenceReferences(knowledge.manifest, evidence.bundle);
|
|
192
|
+
|
|
193
|
+
return {
|
|
194
|
+
projectManifest: project.manifest,
|
|
195
|
+
knowledgeManifest: knowledge.manifest,
|
|
196
|
+
evidenceBundle: evidence.bundle,
|
|
197
|
+
input: {
|
|
198
|
+
projectManifest: project.input.projectManifest,
|
|
199
|
+
knowledgeManifest: knowledge.input.knowledgeManifest,
|
|
200
|
+
evidenceArtifact: evidence.input.evidenceArtifact
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
}
|
|
@@ -0,0 +1,314 @@
|
|
|
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 { canonicalJsonBytes } from './canonical-json.js';
|
|
8
|
+
import { ARTIFACT_GENERATOR_NAME, VERSION } from './constants.js';
|
|
9
|
+
import { DEPENDENCY_AI_CONTEXT_VERSION } from './dependency-ai-context.js';
|
|
10
|
+
import { VERSION_ANALYSIS_PROMPT_VERSION } from './ai-version-analysis.js';
|
|
11
|
+
import { compareText } from './portable.js';
|
|
12
|
+
|
|
13
|
+
export const VERSION_ANALYSIS_SCHEMA_VERSION = '1.0.0';
|
|
14
|
+
|
|
15
|
+
const schema = JSON.parse(await readFile(
|
|
16
|
+
new URL('../schemas/version-analysis.schema.json', import.meta.url),
|
|
17
|
+
'utf8'
|
|
18
|
+
));
|
|
19
|
+
const ajv = new Ajv2020({ allErrors: true, strict: true, strictRequired: false });
|
|
20
|
+
addFormats(ajv);
|
|
21
|
+
const validateSchema = ajv.compile(schema);
|
|
22
|
+
|
|
23
|
+
function digest(value) {
|
|
24
|
+
const bytes = value instanceof Uint8Array ? value : canonicalJsonBytes(value);
|
|
25
|
+
return `sha256:${createHash('sha256').update(bytes).digest('hex')}`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function compareBy(...selectors) {
|
|
29
|
+
return (left, right) => {
|
|
30
|
+
for (const selector of selectors) {
|
|
31
|
+
const result = compareText(selector(left), selector(right));
|
|
32
|
+
if (result !== 0) return result;
|
|
33
|
+
}
|
|
34
|
+
return 0;
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const compareResults = compareBy(
|
|
39
|
+
(item) => item.dependency.projectId,
|
|
40
|
+
(item) => item.dependency.manifest,
|
|
41
|
+
(item) => item.dependency.dependencyType,
|
|
42
|
+
(item) => item.dependency.packageId,
|
|
43
|
+
(item) => item.versions.declaredVersion ?? '',
|
|
44
|
+
(item) => item.versions.targetVersion ?? '',
|
|
45
|
+
(item) => item.contextId
|
|
46
|
+
);
|
|
47
|
+
const compareEvidence = compareBy((item) => item.id);
|
|
48
|
+
const compareFindings = compareBy((item) => item.id);
|
|
49
|
+
|
|
50
|
+
function sorted(values) {
|
|
51
|
+
return [...values].sort(compareText);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function isSorted(items, comparator) {
|
|
55
|
+
return items.every((item, index) => index === 0 || comparator(items[index - 1], item) <= 0);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function duplicateValues(values) {
|
|
59
|
+
const seen = new Set();
|
|
60
|
+
const duplicates = new Set();
|
|
61
|
+
for (const value of values) {
|
|
62
|
+
if (seen.has(value)) duplicates.add(value);
|
|
63
|
+
seen.add(value);
|
|
64
|
+
}
|
|
65
|
+
return [...duplicates].sort(compareText);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function addMismatch(errors, field, actual, expected) {
|
|
69
|
+
if (actual !== expected) errors.push(`${field} is ${actual}; expected ${expected}.`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function resultIdentity(result) {
|
|
73
|
+
return [
|
|
74
|
+
result.dependency.projectId,
|
|
75
|
+
result.dependency.manifest,
|
|
76
|
+
result.dependency.dependencyType,
|
|
77
|
+
result.dependency.packageId,
|
|
78
|
+
result.versions.declaredVersion ?? '',
|
|
79
|
+
result.versions.targetVersion ?? ''
|
|
80
|
+
].join('\0');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function resultIdMaterial(result) {
|
|
84
|
+
return {
|
|
85
|
+
contextId: result.contextId,
|
|
86
|
+
dependency: result.dependency,
|
|
87
|
+
versions: result.versions
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function refsFor(result) {
|
|
92
|
+
return [
|
|
93
|
+
...result.summaryEvidenceRefs,
|
|
94
|
+
...result.riskEvidenceRefs,
|
|
95
|
+
...result.findings.flatMap((finding) => finding.evidenceRefs)
|
|
96
|
+
];
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function manifestSummary(results) {
|
|
100
|
+
return {
|
|
101
|
+
resultCount: results.length,
|
|
102
|
+
analyzedCount: results.filter((item) => item.status === 'analyzed').length,
|
|
103
|
+
skippedCount: results.filter((item) => item.status === 'skipped').length,
|
|
104
|
+
failedCount: results.filter((item) => item.status === 'failed').length,
|
|
105
|
+
requiresHumanReviewCount: results.filter((item) => item.requiresHumanReview).length,
|
|
106
|
+
riskCounts: {
|
|
107
|
+
low: results.filter((item) => item.riskLevel === 'low').length,
|
|
108
|
+
medium: results.filter((item) => item.riskLevel === 'medium').length,
|
|
109
|
+
high: results.filter((item) => item.riskLevel === 'high').length,
|
|
110
|
+
unknown: results.filter((item) => item.riskLevel === 'unknown').length
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function evidenceFromContext(context) {
|
|
116
|
+
return context.knowledge.evidence
|
|
117
|
+
.map((item) => ({
|
|
118
|
+
id: item.id,
|
|
119
|
+
kind: item.kind,
|
|
120
|
+
sourceId: item.sourceId,
|
|
121
|
+
sourceUrl: item.sourceUrl,
|
|
122
|
+
authority: item.authority,
|
|
123
|
+
trust: item.trust,
|
|
124
|
+
retrievedAt: item.retrievedAt,
|
|
125
|
+
contentDigest: item.contentDigest,
|
|
126
|
+
locator: item.locator,
|
|
127
|
+
releaseVersions: sorted(item.releaseVersions)
|
|
128
|
+
}))
|
|
129
|
+
.sort(compareEvidence);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function buildResult(context, analysisResult) {
|
|
133
|
+
const result = {
|
|
134
|
+
id: digest(resultIdMaterial(analysisResult)),
|
|
135
|
+
status: analysisResult.status,
|
|
136
|
+
contextId: analysisResult.contextId,
|
|
137
|
+
dependency: structuredClone(analysisResult.dependency),
|
|
138
|
+
versions: structuredClone(analysisResult.versions),
|
|
139
|
+
summary: analysisResult.summary,
|
|
140
|
+
summaryEvidenceRefs: sorted(analysisResult.summaryEvidenceRefs),
|
|
141
|
+
riskLevel: analysisResult.riskLevel,
|
|
142
|
+
riskEvidenceRefs: sorted(analysisResult.riskEvidenceRefs),
|
|
143
|
+
findings: structuredClone(analysisResult.findings)
|
|
144
|
+
.map((finding) => ({
|
|
145
|
+
...finding,
|
|
146
|
+
appliesToVersions: sorted(finding.appliesToVersions),
|
|
147
|
+
evidenceRefs: sorted(finding.evidenceRefs)
|
|
148
|
+
}))
|
|
149
|
+
.sort(compareFindings),
|
|
150
|
+
evidence: evidenceFromContext(context),
|
|
151
|
+
evidenceCoverage: analysisResult.evidenceCoverage,
|
|
152
|
+
confidence: analysisResult.confidence ?? {
|
|
153
|
+
evidenceCoverage: analysisResult.evidenceCoverage,
|
|
154
|
+
validationStatus: analysisResult.validation.status,
|
|
155
|
+
requiresHumanReview: analysisResult.requiresHumanReview
|
|
156
|
+
},
|
|
157
|
+
validation: {
|
|
158
|
+
status: analysisResult.validation.status,
|
|
159
|
+
warningCodes: sorted(analysisResult.validation.warningCodes)
|
|
160
|
+
},
|
|
161
|
+
requiresHumanReview: analysisResult.requiresHumanReview,
|
|
162
|
+
humanReviewReasons: sorted(analysisResult.humanReviewReasons),
|
|
163
|
+
nextAction: analysisResult.nextAction,
|
|
164
|
+
limitations: structuredClone(analysisResult.limitations)
|
|
165
|
+
.sort(compareBy((item) => item.code, (item) => item.message))
|
|
166
|
+
};
|
|
167
|
+
return result;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function validatePair(context, analysisResult) {
|
|
171
|
+
if (context.contextId !== analysisResult.contextId) {
|
|
172
|
+
throw new Error(`Version Analysis Manifest input error: context/result mismatch for ${analysisResult.contextId}.`);
|
|
173
|
+
}
|
|
174
|
+
const deterministicFields = ['dependency', 'versions'];
|
|
175
|
+
for (const field of deterministicFields) {
|
|
176
|
+
if (JSON.stringify(context[field]) !== JSON.stringify(analysisResult[field])) {
|
|
177
|
+
throw new Error(`Version Analysis Manifest input error: result ${analysisResult.contextId} changed deterministic ${field}.`);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function validateVersionAnalysisManifestInvariants(manifest) {
|
|
183
|
+
const errors = [];
|
|
184
|
+
const results = manifest.results ?? [];
|
|
185
|
+
if (!isSorted(results, compareResults)) errors.push('results must be sorted by dependency and target.');
|
|
186
|
+
|
|
187
|
+
for (const duplicate of duplicateValues(results.map((item) => item.id))) {
|
|
188
|
+
errors.push(`Duplicate result id ${duplicate}.`);
|
|
189
|
+
}
|
|
190
|
+
for (const duplicate of duplicateValues(results.map(resultIdentity))) {
|
|
191
|
+
errors.push(`Duplicate dependency analysis result ${duplicate.replaceAll('\0', ' / ')}.`);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
for (const result of results) {
|
|
195
|
+
const expectedId = digest(resultIdMaterial(result));
|
|
196
|
+
if (result.id !== expectedId) errors.push(`Result ${result.id} id is not stable for its context/dependency/version facts.`);
|
|
197
|
+
if (!isSorted(result.evidence, compareEvidence)) errors.push(`Result ${result.id} evidence must be sorted by id.`);
|
|
198
|
+
if (!isSorted(result.findings, compareFindings)) errors.push(`Result ${result.id} findings must be sorted by id.`);
|
|
199
|
+
const evidenceIds = new Set(result.evidence.map((item) => item.id));
|
|
200
|
+
for (const duplicate of duplicateValues(result.evidence.map((item) => item.id))) {
|
|
201
|
+
errors.push(`Result ${result.id} has duplicate evidence ${duplicate}.`);
|
|
202
|
+
}
|
|
203
|
+
for (const ref of refsFor(result)) {
|
|
204
|
+
if (!evidenceIds.has(ref)) errors.push(`Result ${result.id} references unknown evidence ${ref}.`);
|
|
205
|
+
}
|
|
206
|
+
if (result.confidence.evidenceCoverage !== result.evidenceCoverage) {
|
|
207
|
+
errors.push(`Result ${result.id} confidence.evidenceCoverage does not match evidenceCoverage.`);
|
|
208
|
+
}
|
|
209
|
+
if (result.confidence.validationStatus !== result.validation.status) {
|
|
210
|
+
errors.push(`Result ${result.id} confidence.validationStatus does not match validation.status.`);
|
|
211
|
+
}
|
|
212
|
+
if (result.confidence.requiresHumanReview !== result.requiresHumanReview) {
|
|
213
|
+
errors.push(`Result ${result.id} confidence.requiresHumanReview does not match requiresHumanReview.`);
|
|
214
|
+
}
|
|
215
|
+
const versions = result.versions;
|
|
216
|
+
const installedFields = [
|
|
217
|
+
'installedVersion',
|
|
218
|
+
'installedVersionStatus',
|
|
219
|
+
'installedVersionSource',
|
|
220
|
+
'installedVersionReason'
|
|
221
|
+
];
|
|
222
|
+
const installedFieldCount = installedFields.filter((field) => field in versions).length;
|
|
223
|
+
if (installedFieldCount !== 0 && installedFieldCount !== installedFields.length) {
|
|
224
|
+
errors.push(`Result ${result.id} has an incomplete installed-version baseline.`);
|
|
225
|
+
} else if (versions.installedVersionStatus === 'resolved') {
|
|
226
|
+
if (
|
|
227
|
+
versions.currentVersion !== versions.installedVersion
|
|
228
|
+
|| versions.currentVersionSource !== 'resolvedArtifact'
|
|
229
|
+
|| versions.installedVersionSource === null
|
|
230
|
+
|| versions.installedVersionReason !== null
|
|
231
|
+
) {
|
|
232
|
+
errors.push(`Result ${result.id} does not preserve its resolved installed-version baseline.`);
|
|
233
|
+
}
|
|
234
|
+
} else if (
|
|
235
|
+
versions.installedVersionStatus === 'unresolved'
|
|
236
|
+
&& (
|
|
237
|
+
versions.installedVersion !== null
|
|
238
|
+
|| versions.installedVersionSource !== null
|
|
239
|
+
|| versions.installedVersionReason === null
|
|
240
|
+
)
|
|
241
|
+
) {
|
|
242
|
+
errors.push(`Result ${result.id} has inconsistent unresolved installed-version fields.`);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
addMismatch(errors, 'analysis.resultCount', manifest.analysis?.resultCount, results.length);
|
|
247
|
+
const summary = manifestSummary(results);
|
|
248
|
+
addMismatch(errors, 'summary.resultCount', manifest.summary?.resultCount, summary.resultCount);
|
|
249
|
+
addMismatch(errors, 'summary.analyzedCount', manifest.summary?.analyzedCount, summary.analyzedCount);
|
|
250
|
+
addMismatch(errors, 'summary.skippedCount', manifest.summary?.skippedCount, summary.skippedCount);
|
|
251
|
+
addMismatch(errors, 'summary.failedCount', manifest.summary?.failedCount, summary.failedCount);
|
|
252
|
+
addMismatch(
|
|
253
|
+
errors,
|
|
254
|
+
'summary.requiresHumanReviewCount',
|
|
255
|
+
manifest.summary?.requiresHumanReviewCount,
|
|
256
|
+
summary.requiresHumanReviewCount
|
|
257
|
+
);
|
|
258
|
+
for (const level of ['low', 'medium', 'high', 'unknown']) {
|
|
259
|
+
addMismatch(errors, `summary.riskCounts.${level}`, manifest.summary?.riskCounts?.[level], summary.riskCounts[level]);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return errors.sort(compareText);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export function validateVersionAnalysisManifest(manifest) {
|
|
266
|
+
if (manifest?.schemaVersion !== VERSION_ANALYSIS_SCHEMA_VERSION) {
|
|
267
|
+
throw new Error(`Version Analysis Manifest validation error: unsupported schema version; expected ${VERSION_ANALYSIS_SCHEMA_VERSION}.`);
|
|
268
|
+
}
|
|
269
|
+
if (!validateSchema(manifest)) {
|
|
270
|
+
throw new Error(`Version Analysis Manifest validation error: schema validation failed: ${ajv.errorsText(validateSchema.errors, { separator: '; ' })}`);
|
|
271
|
+
}
|
|
272
|
+
const invariantErrors = validateVersionAnalysisManifestInvariants(manifest);
|
|
273
|
+
if (invariantErrors.length > 0) {
|
|
274
|
+
throw new Error(`Version Analysis Manifest validation error: runtime invariants failed: ${invariantErrors.join(' ')}`);
|
|
275
|
+
}
|
|
276
|
+
return manifest;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export function buildVersionAnalysisManifest({
|
|
280
|
+
input,
|
|
281
|
+
contexts,
|
|
282
|
+
results,
|
|
283
|
+
generatedAt = new Date(),
|
|
284
|
+
promptVersion = VERSION_ANALYSIS_PROMPT_VERSION,
|
|
285
|
+
contextVersion = DEPENDENCY_AI_CONTEXT_VERSION
|
|
286
|
+
}) {
|
|
287
|
+
if (!Array.isArray(contexts) || !Array.isArray(results) || contexts.length !== results.length) {
|
|
288
|
+
throw new Error('Version Analysis Manifest input error: contexts and results must be arrays of the same length.');
|
|
289
|
+
}
|
|
290
|
+
const records = contexts.map((context, index) => {
|
|
291
|
+
const result = results[index];
|
|
292
|
+
validatePair(context, result);
|
|
293
|
+
return buildResult(context, result);
|
|
294
|
+
}).sort(compareResults);
|
|
295
|
+
|
|
296
|
+
const manifest = {
|
|
297
|
+
schemaVersion: VERSION_ANALYSIS_SCHEMA_VERSION,
|
|
298
|
+
generatedAt: generatedAt instanceof Date ? generatedAt.toISOString() : generatedAt,
|
|
299
|
+
generator: { name: ARTIFACT_GENERATOR_NAME, version: VERSION },
|
|
300
|
+
input: structuredClone(input),
|
|
301
|
+
analysis: {
|
|
302
|
+
promptVersion,
|
|
303
|
+
contextVersion,
|
|
304
|
+
resultCount: records.length
|
|
305
|
+
},
|
|
306
|
+
summary: manifestSummary(records),
|
|
307
|
+
results: records
|
|
308
|
+
};
|
|
309
|
+
return validateVersionAnalysisManifest(manifest);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
export function versionAnalysisManifestDigest(manifest) {
|
|
313
|
+
return digest(manifest);
|
|
314
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { mkdir, open, rename, rm } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
import { validateVersionAnalysisManifest } from './version-analysis-manifest.js';
|
|
5
|
+
|
|
6
|
+
export function serializeVersionAnalysisManifest(manifest) {
|
|
7
|
+
validateVersionAnalysisManifest(manifest);
|
|
8
|
+
return `${JSON.stringify(manifest, null, 2)}\n`;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export async function writeVersionAnalysisManifest(outputPath, manifest) {
|
|
12
|
+
const target = path.resolve(outputPath);
|
|
13
|
+
const temporary = `${target}.${process.pid}.tmp`;
|
|
14
|
+
const contents = serializeVersionAnalysisManifest(manifest);
|
|
15
|
+
await mkdir(path.dirname(target), { recursive: true });
|
|
16
|
+
let handle;
|
|
17
|
+
try {
|
|
18
|
+
handle = await open(temporary, 'w', 0o600);
|
|
19
|
+
await handle.writeFile(contents, 'utf8');
|
|
20
|
+
await handle.sync();
|
|
21
|
+
await handle.close();
|
|
22
|
+
handle = null;
|
|
23
|
+
await rename(temporary, target);
|
|
24
|
+
} catch (error) {
|
|
25
|
+
await handle?.close().catch(() => {});
|
|
26
|
+
await rm(temporary, { force: true }).catch(() => {});
|
|
27
|
+
throw error;
|
|
28
|
+
}
|
|
29
|
+
return target;
|
|
30
|
+
}
|