@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,275 @@
|
|
|
1
|
+
import Ajv2020 from 'ajv/dist/2020.js';
|
|
2
|
+
|
|
3
|
+
import { isAiRuntimeError } from './ai-runtime-error.js';
|
|
4
|
+
|
|
5
|
+
export const RUNTIME_CONFORMANCE_OUTPUT_SCHEMA = Object.freeze({
|
|
6
|
+
$schema: 'https://json-schema.org/draft/2020-12/schema',
|
|
7
|
+
type: 'object',
|
|
8
|
+
additionalProperties: false,
|
|
9
|
+
required: ['status', 'protocol'],
|
|
10
|
+
properties: {
|
|
11
|
+
status: { const: 'ok' },
|
|
12
|
+
protocol: { const: 'chat-completions' }
|
|
13
|
+
}
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
export const RUNTIME_CONFORMANCE_CAPABILITIES = Object.freeze([
|
|
17
|
+
'connectivity',
|
|
18
|
+
'authentication',
|
|
19
|
+
'timeout',
|
|
20
|
+
'cancellation',
|
|
21
|
+
'structuredOutput',
|
|
22
|
+
'jsonSchema',
|
|
23
|
+
'jsonMode',
|
|
24
|
+
'responseValidation',
|
|
25
|
+
'usage',
|
|
26
|
+
'identity',
|
|
27
|
+
'contentType',
|
|
28
|
+
'httpErrors',
|
|
29
|
+
'oversizedResponse',
|
|
30
|
+
'redirectProtection',
|
|
31
|
+
'providerErrors'
|
|
32
|
+
]);
|
|
33
|
+
|
|
34
|
+
const ajv = new Ajv2020({ allErrors: true, strict: true });
|
|
35
|
+
const validateOutputSchema = ajv.compile(RUNTIME_CONFORMANCE_OUTPUT_SCHEMA);
|
|
36
|
+
|
|
37
|
+
export class RuntimeConformanceValidationError extends Error {
|
|
38
|
+
constructor(code, message) {
|
|
39
|
+
super(message);
|
|
40
|
+
this.name = 'RuntimeConformanceValidationError';
|
|
41
|
+
this.code = code;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function validateRuntimeConformanceOutput(output) {
|
|
46
|
+
let candidate;
|
|
47
|
+
try {
|
|
48
|
+
candidate = JSON.parse(output);
|
|
49
|
+
} catch {
|
|
50
|
+
throw new RuntimeConformanceValidationError(
|
|
51
|
+
'INVALID_OUTPUT_JSON',
|
|
52
|
+
'Runtime output is not valid JSON.'
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
if (!validateOutputSchema(candidate)) {
|
|
56
|
+
throw new RuntimeConformanceValidationError(
|
|
57
|
+
'INVALID_OUTPUT_SCHEMA',
|
|
58
|
+
'Runtime output does not satisfy the conformance schema.'
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
return candidate;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function defineCase({
|
|
65
|
+
id,
|
|
66
|
+
capability,
|
|
67
|
+
capabilities = [capability],
|
|
68
|
+
expected = 'SUCCESS',
|
|
69
|
+
accepted = [expected],
|
|
70
|
+
transport,
|
|
71
|
+
required = true,
|
|
72
|
+
capabilityStatusByOutcome = {},
|
|
73
|
+
authorization,
|
|
74
|
+
mode = 'jsonSchema',
|
|
75
|
+
requireExactModelIdentity = false,
|
|
76
|
+
assertions = []
|
|
77
|
+
}) {
|
|
78
|
+
return Object.freeze({
|
|
79
|
+
id,
|
|
80
|
+
capability,
|
|
81
|
+
capabilities: Object.freeze([...capabilities]),
|
|
82
|
+
expected,
|
|
83
|
+
accepted: Object.freeze([...accepted]),
|
|
84
|
+
transport: Object.freeze({ ...transport }),
|
|
85
|
+
required,
|
|
86
|
+
capabilityStatusByOutcome: Object.freeze({ ...capabilityStatusByOutcome }),
|
|
87
|
+
authorization,
|
|
88
|
+
mode,
|
|
89
|
+
requireExactModelIdentity,
|
|
90
|
+
assertions: Object.freeze([...assertions])
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const HTTP_ERROR_CASES = [
|
|
95
|
+
[401, 'AUTH_ERROR'],
|
|
96
|
+
[403, 'ACCESS_DENIED'],
|
|
97
|
+
[404, 'MODEL_NOT_FOUND', 'model_not_found: configured model does not exist'],
|
|
98
|
+
[408, 'TIMEOUT'],
|
|
99
|
+
[409, 'PROVIDER_ERROR'],
|
|
100
|
+
[422, 'SCHEMA_REJECTED', 'invalid json_schema supplied'],
|
|
101
|
+
[429, 'RATE_LIMITED'],
|
|
102
|
+
[500, 'PROVIDER_ERROR'],
|
|
103
|
+
[502, 'PROVIDER_UNAVAILABLE'],
|
|
104
|
+
[503, 'PROVIDER_UNAVAILABLE'],
|
|
105
|
+
[504, 'TIMEOUT']
|
|
106
|
+
].map(([status, expected, descriptor]) => defineCase({
|
|
107
|
+
id: `http-${status}`,
|
|
108
|
+
capability: 'httpErrors',
|
|
109
|
+
expected,
|
|
110
|
+
transport: { type: 'httpError', status, descriptor }
|
|
111
|
+
}));
|
|
112
|
+
|
|
113
|
+
export const RUNTIME_CONFORMANCE_CASES = Object.freeze([
|
|
114
|
+
defineCase({
|
|
115
|
+
id: 'connectivity-reachable',
|
|
116
|
+
capability: 'connectivity',
|
|
117
|
+
transport: { type: 'success' },
|
|
118
|
+
assertions: ['outputSchema']
|
|
119
|
+
}),
|
|
120
|
+
defineCase({
|
|
121
|
+
id: 'connectivity-unreachable',
|
|
122
|
+
capability: 'connectivity',
|
|
123
|
+
expected: 'NETWORK_ERROR',
|
|
124
|
+
transport: { type: 'networkError' }
|
|
125
|
+
}),
|
|
126
|
+
defineCase({
|
|
127
|
+
id: 'authentication-none',
|
|
128
|
+
capability: 'authentication',
|
|
129
|
+
transport: { type: 'success' },
|
|
130
|
+
assertions: ['noAuthorization']
|
|
131
|
+
}),
|
|
132
|
+
defineCase({
|
|
133
|
+
id: 'authentication-bearer',
|
|
134
|
+
capability: 'authentication',
|
|
135
|
+
authorization: 'Bearer conformance-fixture-token',
|
|
136
|
+
transport: { type: 'success' },
|
|
137
|
+
assertions: ['bearerAuthorization']
|
|
138
|
+
}),
|
|
139
|
+
defineCase({
|
|
140
|
+
id: 'authentication-invalid',
|
|
141
|
+
capability: 'authentication',
|
|
142
|
+
authorization: 'Bearer invalid-conformance-fixture-token',
|
|
143
|
+
expected: 'AUTH_ERROR',
|
|
144
|
+
transport: { type: 'httpError', status: 401 }
|
|
145
|
+
}),
|
|
146
|
+
defineCase({
|
|
147
|
+
id: 'timeout-classification',
|
|
148
|
+
capability: 'timeout',
|
|
149
|
+
expected: 'TIMEOUT',
|
|
150
|
+
transport: { type: 'waitForAbort' }
|
|
151
|
+
}),
|
|
152
|
+
defineCase({
|
|
153
|
+
id: 'cancellation-propagation',
|
|
154
|
+
capability: 'cancellation',
|
|
155
|
+
expected: 'CANCELLED',
|
|
156
|
+
transport: { type: 'waitForAbort' },
|
|
157
|
+
assertions: ['callerAbort']
|
|
158
|
+
}),
|
|
159
|
+
defineCase({
|
|
160
|
+
id: 'structured-output-json-schema',
|
|
161
|
+
capability: 'structuredOutput',
|
|
162
|
+
capabilities: ['structuredOutput', 'jsonSchema'],
|
|
163
|
+
transport: { type: 'success' },
|
|
164
|
+
assertions: ['structuredRequest', 'outputSchema']
|
|
165
|
+
}),
|
|
166
|
+
defineCase({
|
|
167
|
+
id: 'json-mode-capability',
|
|
168
|
+
capability: 'jsonMode',
|
|
169
|
+
expected: 'SUCCESS_OR_STRUCTURED_OUTPUT_UNSUPPORTED',
|
|
170
|
+
accepted: ['SUCCESS', 'STRUCTURED_OUTPUT_UNSUPPORTED'],
|
|
171
|
+
transport: { type: 'success' },
|
|
172
|
+
required: false,
|
|
173
|
+
capabilityStatusByOutcome: { STRUCTURED_OUTPUT_UNSUPPORTED: 'NOT_SUPPORTED' },
|
|
174
|
+
mode: 'json'
|
|
175
|
+
}),
|
|
176
|
+
defineCase({
|
|
177
|
+
id: 'response-empty-content',
|
|
178
|
+
capability: 'responseValidation',
|
|
179
|
+
expected: 'INVALID_RESPONSE',
|
|
180
|
+
transport: { type: 'success', content: ' ' }
|
|
181
|
+
}),
|
|
182
|
+
defineCase({
|
|
183
|
+
id: 'response-multiple-choices',
|
|
184
|
+
capability: 'responseValidation',
|
|
185
|
+
expected: 'INVALID_RESPONSE',
|
|
186
|
+
transport: { type: 'success', choiceCount: 2 }
|
|
187
|
+
}),
|
|
188
|
+
defineCase({
|
|
189
|
+
id: 'response-missing-choices',
|
|
190
|
+
capability: 'responseValidation',
|
|
191
|
+
expected: 'INVALID_RESPONSE',
|
|
192
|
+
transport: { type: 'success', omitChoices: true }
|
|
193
|
+
}),
|
|
194
|
+
defineCase({
|
|
195
|
+
id: 'response-malformed-json',
|
|
196
|
+
capability: 'responseValidation',
|
|
197
|
+
expected: 'INVALID_RESPONSE',
|
|
198
|
+
transport: { type: 'malformedJson' }
|
|
199
|
+
}),
|
|
200
|
+
defineCase({
|
|
201
|
+
id: 'response-invalid-output-schema',
|
|
202
|
+
capability: 'responseValidation',
|
|
203
|
+
expected: 'INVALID_OUTPUT_SCHEMA',
|
|
204
|
+
transport: { type: 'success', content: '{"status":"wrong","protocol":"chat-completions"}' },
|
|
205
|
+
assertions: ['outputSchema']
|
|
206
|
+
}),
|
|
207
|
+
defineCase({
|
|
208
|
+
id: 'usage-mapping',
|
|
209
|
+
capability: 'usage',
|
|
210
|
+
transport: { type: 'success' },
|
|
211
|
+
assertions: ['usage']
|
|
212
|
+
}),
|
|
213
|
+
defineCase({
|
|
214
|
+
id: 'identity-match',
|
|
215
|
+
capability: 'identity',
|
|
216
|
+
transport: { type: 'success' },
|
|
217
|
+
requireExactModelIdentity: true,
|
|
218
|
+
assertions: ['identity']
|
|
219
|
+
}),
|
|
220
|
+
defineCase({
|
|
221
|
+
id: 'identity-mismatch',
|
|
222
|
+
capability: 'identity',
|
|
223
|
+
expected: 'IDENTITY_MISMATCH',
|
|
224
|
+
transport: { type: 'success', returnedModel: 'different-offline-model' },
|
|
225
|
+
requireExactModelIdentity: true
|
|
226
|
+
}),
|
|
227
|
+
defineCase({
|
|
228
|
+
id: 'content-type-invalid',
|
|
229
|
+
capability: 'contentType',
|
|
230
|
+
expected: 'INVALID_RESPONSE',
|
|
231
|
+
transport: { type: 'success', contentType: 'text/plain' }
|
|
232
|
+
}),
|
|
233
|
+
...HTTP_ERROR_CASES,
|
|
234
|
+
defineCase({
|
|
235
|
+
id: 'response-oversized',
|
|
236
|
+
capability: 'oversizedResponse',
|
|
237
|
+
expected: 'RESPONSE_TOO_LARGE',
|
|
238
|
+
transport: { type: 'oversized' }
|
|
239
|
+
}),
|
|
240
|
+
defineCase({
|
|
241
|
+
id: 'redirect-rejected',
|
|
242
|
+
capability: 'redirectProtection',
|
|
243
|
+
expected: 'NETWORK_ERROR',
|
|
244
|
+
transport: { type: 'redirect' },
|
|
245
|
+
assertions: ['redirectErrorMode']
|
|
246
|
+
}),
|
|
247
|
+
defineCase({
|
|
248
|
+
id: 'structured-output-unsupported',
|
|
249
|
+
capability: 'providerErrors',
|
|
250
|
+
expected: 'STRUCTURED_OUTPUT_UNSUPPORTED',
|
|
251
|
+
transport: {
|
|
252
|
+
type: 'httpError',
|
|
253
|
+
status: 422,
|
|
254
|
+
descriptor: 'response_format json_schema is not supported'
|
|
255
|
+
}
|
|
256
|
+
}),
|
|
257
|
+
defineCase({
|
|
258
|
+
id: 'provider-error-classification',
|
|
259
|
+
capability: 'providerErrors',
|
|
260
|
+
expected: 'PROVIDER_ERROR',
|
|
261
|
+
transport: { type: 'httpError', status: 418 }
|
|
262
|
+
}),
|
|
263
|
+
defineCase({
|
|
264
|
+
id: 'unknown-error-classification',
|
|
265
|
+
capability: 'providerErrors',
|
|
266
|
+
expected: 'UNKNOWN',
|
|
267
|
+
transport: { type: 'unknownError' }
|
|
268
|
+
})
|
|
269
|
+
]);
|
|
270
|
+
|
|
271
|
+
export function normalizeConformanceError(error) {
|
|
272
|
+
if (isAiRuntimeError(error)) return error.code;
|
|
273
|
+
if (error instanceof RuntimeConformanceValidationError) return error.code;
|
|
274
|
+
return 'UNKNOWN';
|
|
275
|
+
}
|
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
import { compareText, isSorted } from './portable.js';
|
|
4
|
+
import { canonicalizeSourceUrl, isCanonicalPublicSourceUrl } from './source-url.js';
|
|
5
|
+
|
|
6
|
+
const ROLE_ALIASES = new Map([
|
|
7
|
+
['homepage', 'homepage'],
|
|
8
|
+
['documentation', 'documentation'],
|
|
9
|
+
['repository', 'repository'],
|
|
10
|
+
['sourcerepository', 'repository'],
|
|
11
|
+
['issues', 'issues'],
|
|
12
|
+
['issuetracker', 'issues'],
|
|
13
|
+
['changelog', 'changelog'],
|
|
14
|
+
['releasenotes', 'releaseNotes'],
|
|
15
|
+
['releases', 'releases'],
|
|
16
|
+
['releasefeed', 'releases'],
|
|
17
|
+
['migration', 'migrationGuide'],
|
|
18
|
+
['migrationguide', 'migrationGuide'],
|
|
19
|
+
['upgradeguide', 'migrationGuide'],
|
|
20
|
+
['community', 'community']
|
|
21
|
+
]);
|
|
22
|
+
|
|
23
|
+
const ROLE_DETAILS = {
|
|
24
|
+
homepage: { group: 'documentation', kind: 'officialDocumentation' },
|
|
25
|
+
documentation: { group: 'documentation', kind: 'officialDocumentation' },
|
|
26
|
+
repository: { group: 'repository', kind: 'sourceRepository' },
|
|
27
|
+
issues: { group: 'issues', kind: 'sourceRepository' },
|
|
28
|
+
changelog: { group: 'changelog', kind: 'officialDocumentation' },
|
|
29
|
+
releaseNotes: { group: 'releaseNotes', kind: 'officialDocumentation' },
|
|
30
|
+
releases: { group: 'releaseFeed', kind: 'releaseFeed' },
|
|
31
|
+
migrationGuide: { group: 'migrationGuide', kind: 'officialDocumentation' },
|
|
32
|
+
community: { group: 'community', kind: 'community' }
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const SOURCE_KINDS = new Set(['registry', 'officialDocumentation', 'sourceRepository', 'releaseFeed', 'community']);
|
|
36
|
+
const AUTHORITIES = new Set(['registryAuthoritative', 'officialProject', 'publisherProvided', 'community']);
|
|
37
|
+
const TRUSTS = new Set(['official', 'publisher', 'verified', 'community', 'unknown']);
|
|
38
|
+
const STATUSES = new Set(['available', 'notFound', 'unavailable', 'unverified', 'stale']);
|
|
39
|
+
|
|
40
|
+
function digest(value) {
|
|
41
|
+
return createHash('sha256').update(value).digest('hex');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function sortedUnique(values) {
|
|
45
|
+
return [...new Set(values)].sort(compareText);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function sourceIdFor(packageId, group, url) {
|
|
49
|
+
return `${packageId}:${group}:${digest(url)}`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function comparableWarning(left, right) {
|
|
53
|
+
return compareText(left.packageId, right.packageId)
|
|
54
|
+
|| compareText(left.code, right.code)
|
|
55
|
+
|| compareText(left.sourceId, right.sourceId)
|
|
56
|
+
|| compareText(left.message, right.message);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function normalizeRole(value) {
|
|
60
|
+
if (typeof value !== 'string') return null;
|
|
61
|
+
return ROLE_ALIASES.get(value.replace(/[\s_-]/g, '').toLowerCase()) ?? null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function inputPackage(result) {
|
|
65
|
+
return result?.package ?? result;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function registrySource(result) {
|
|
69
|
+
return result?.source ?? null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function candidateFromMetadata(packageRecord, sourceId) {
|
|
73
|
+
const metadata = packageRecord.metadata;
|
|
74
|
+
if (!metadata || typeof metadata !== 'object' || Array.isArray(metadata)) return [];
|
|
75
|
+
return [
|
|
76
|
+
['homepage', 'homepageUrl'],
|
|
77
|
+
['documentation', 'documentationUrl'],
|
|
78
|
+
['repository', 'repositoryUrl'],
|
|
79
|
+
['issues', 'issueUrl']
|
|
80
|
+
].flatMap(([role, field]) => typeof metadata[field] === 'string'
|
|
81
|
+
? [{ role, url: metadata[field], discoveredFromSourceId: sourceId, discoveredFromField: `metadata.${field}` }]
|
|
82
|
+
: []);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function candidatesFromResult(result, packageRecord, sourceId) {
|
|
86
|
+
const candidates = candidateFromMetadata(packageRecord, sourceId);
|
|
87
|
+
for (const candidate of Array.isArray(result?.sourceCandidates) ? result.sourceCandidates : []) {
|
|
88
|
+
if (!candidate || typeof candidate !== 'object' || Array.isArray(candidate)) continue;
|
|
89
|
+
candidates.push({
|
|
90
|
+
role: candidate.role,
|
|
91
|
+
url: candidate.url,
|
|
92
|
+
discoveredFromSourceId: candidate.discoveredFromSourceId ?? sourceId,
|
|
93
|
+
discoveredFromField: candidate.discoveredFromField ?? 'sourceCandidates'
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
return candidates;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function cloneSource(source) {
|
|
100
|
+
return structuredClone(source);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function sourceInputIsValid(source, packageId) {
|
|
104
|
+
return source && typeof source === 'object' && !Array.isArray(source)
|
|
105
|
+
&& typeof source.id === 'string' && source.id.startsWith(`${packageId}:registry`)
|
|
106
|
+
&& source.kind === 'registry';
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function sameJson(left, right) {
|
|
110
|
+
return JSON.stringify(left) === JSON.stringify(right);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function candidateComparator(left, right) {
|
|
114
|
+
return compareText(left.packageId, right.packageId)
|
|
115
|
+
|| compareText(left.sourceId, right.sourceId)
|
|
116
|
+
|| compareText(left.discoveredFromSourceId, right.discoveredFromSourceId)
|
|
117
|
+
|| compareText(left.discoveredFromField, right.discoveredFromField)
|
|
118
|
+
|| compareText(left.role, right.role);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function provenanceComparator(left, right) {
|
|
122
|
+
return compareText(left.discoveredFromSourceId, right.discoveredFromSourceId)
|
|
123
|
+
|| compareText(left.discoveredFromField, right.discoveredFromField)
|
|
124
|
+
|| compareText(left.role, right.role);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function conflictMessage(packageId, role) {
|
|
128
|
+
return `Multiple publisher-provided ${role} sources were found for ${packageId}.`;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Build the small, internal provenance graph used before Knowledge Manifest
|
|
133
|
+
* assembly. It is deliberately package-scoped: equal URLs from unrelated
|
|
134
|
+
* package identities do not assert an equivalence relationship.
|
|
135
|
+
*/
|
|
136
|
+
export function resolveSourceProvenance(adapterResults) {
|
|
137
|
+
if (!Array.isArray(adapterResults)) {
|
|
138
|
+
throw new Error('Source provenance resolution requires an array of adapter results.');
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const packages = new Map();
|
|
142
|
+
const sources = new Map();
|
|
143
|
+
const candidates = [];
|
|
144
|
+
|
|
145
|
+
for (const result of adapterResults) {
|
|
146
|
+
const packageRecord = inputPackage(result);
|
|
147
|
+
const packageId = packageRecord?.id ?? result?.packageId;
|
|
148
|
+
const source = registrySource(result);
|
|
149
|
+
if (typeof packageId !== 'string' || !sourceInputIsValid(source, packageId)) {
|
|
150
|
+
throw new Error('Source provenance input must contain a package id and its registry source.');
|
|
151
|
+
}
|
|
152
|
+
if (!packages.has(packageId)) packages.set(packageId, new Set());
|
|
153
|
+
packages.get(packageId).add(source.id);
|
|
154
|
+
const cloned = cloneSource(source);
|
|
155
|
+
if (sources.has(cloned.id) && !sameJson(sources.get(cloned.id), cloned)) {
|
|
156
|
+
throw new Error(`Source provenance input contains conflicting registry source ${cloned.id}.`);
|
|
157
|
+
}
|
|
158
|
+
sources.set(cloned.id, cloned);
|
|
159
|
+
|
|
160
|
+
for (const rawCandidate of candidatesFromResult(result, packageRecord, source.id)) {
|
|
161
|
+
const role = normalizeRole(rawCandidate.role);
|
|
162
|
+
const details = role ? ROLE_DETAILS[role] : null;
|
|
163
|
+
const url = details ? canonicalizeSourceUrl(rawCandidate.url, { role }) : null;
|
|
164
|
+
const discoveredFromSourceId = typeof rawCandidate.discoveredFromSourceId === 'string'
|
|
165
|
+
&& sources.has(rawCandidate.discoveredFromSourceId)
|
|
166
|
+
? rawCandidate.discoveredFromSourceId
|
|
167
|
+
: source.id;
|
|
168
|
+
if (!details || !url) continue;
|
|
169
|
+
candidates.push({
|
|
170
|
+
packageId,
|
|
171
|
+
role,
|
|
172
|
+
group: details.group,
|
|
173
|
+
kind: details.kind,
|
|
174
|
+
url,
|
|
175
|
+
sourceId: sourceIdFor(packageId, details.group, url),
|
|
176
|
+
discoveredFromSourceId,
|
|
177
|
+
discoveredFromField: typeof rawCandidate.discoveredFromField === 'string'
|
|
178
|
+
? rawCandidate.discoveredFromField
|
|
179
|
+
: 'sourceCandidates'
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const generated = new Map();
|
|
185
|
+
for (const candidate of candidates.sort(candidateComparator)) {
|
|
186
|
+
if (!generated.has(candidate.sourceId)) {
|
|
187
|
+
generated.set(candidate.sourceId, {
|
|
188
|
+
packageId: candidate.packageId,
|
|
189
|
+
id: candidate.sourceId,
|
|
190
|
+
kind: candidate.kind,
|
|
191
|
+
authority: candidate.role === 'community' ? 'community' : 'publisherProvided',
|
|
192
|
+
trust: candidate.role === 'community' ? 'community' : 'publisher',
|
|
193
|
+
url: candidate.url,
|
|
194
|
+
status: 'unverified',
|
|
195
|
+
supports: new Set(),
|
|
196
|
+
provenance: [],
|
|
197
|
+
conflictsWith: new Set()
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
const record = generated.get(candidate.sourceId);
|
|
201
|
+
record.supports.add(candidate.role);
|
|
202
|
+
record.provenance.push(candidate);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
for (const record of generated.values()) {
|
|
206
|
+
const provenance = record.provenance.sort(provenanceComparator);
|
|
207
|
+
const evidence = sortedUnique(provenance.map((item) => item.discoveredFromSourceId));
|
|
208
|
+
const independentRegistryEvidence = evidence.filter((id) => sources.get(id)?.kind === 'registry');
|
|
209
|
+
if (record.authority === 'publisherProvided' && independentRegistryEvidence.length >= 2) {
|
|
210
|
+
record.trust = 'verified';
|
|
211
|
+
}
|
|
212
|
+
const source = {
|
|
213
|
+
id: record.id,
|
|
214
|
+
kind: record.kind,
|
|
215
|
+
authority: record.authority,
|
|
216
|
+
trust: record.trust,
|
|
217
|
+
url: record.url,
|
|
218
|
+
status: record.status,
|
|
219
|
+
supports: [...record.supports].sort(compareText),
|
|
220
|
+
discoveredFrom: provenance[0].discoveredFromSourceId,
|
|
221
|
+
trustEvidenceSourceIds: evidence,
|
|
222
|
+
snapshot: null,
|
|
223
|
+
conflictsWith: []
|
|
224
|
+
};
|
|
225
|
+
sources.set(source.id, source);
|
|
226
|
+
packages.get(record.packageId).add(source.id);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const byPackageAndRole = new Map();
|
|
230
|
+
for (const record of generated.values()) {
|
|
231
|
+
for (const role of record.supports) {
|
|
232
|
+
const key = `${record.packageId}\u0000${role}`;
|
|
233
|
+
if (!byPackageAndRole.has(key)) byPackageAndRole.set(key, []);
|
|
234
|
+
byPackageAndRole.get(key).push(record.id);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
for (const ids of byPackageAndRole.values()) {
|
|
238
|
+
const uniqueIds = sortedUnique(ids);
|
|
239
|
+
if (uniqueIds.length < 2) continue;
|
|
240
|
+
for (const id of uniqueIds) {
|
|
241
|
+
const record = generated.get(id);
|
|
242
|
+
for (const otherId of uniqueIds) if (otherId !== id) record.conflictsWith.add(otherId);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const warnings = [];
|
|
247
|
+
for (const record of generated.values()) {
|
|
248
|
+
const source = sources.get(record.id);
|
|
249
|
+
source.conflictsWith = [...record.conflictsWith].sort(compareText);
|
|
250
|
+
if (source.conflictsWith.length > 0) {
|
|
251
|
+
const role = source.supports[0];
|
|
252
|
+
warnings.push({
|
|
253
|
+
code: 'SOURCE_CONFLICT',
|
|
254
|
+
packageId: record.packageId,
|
|
255
|
+
sourceId: source.id,
|
|
256
|
+
message: conflictMessage(record.packageId, role),
|
|
257
|
+
retryable: false
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const graph = {
|
|
263
|
+
packages: [...packages.entries()].map(([packageId, sourceIds]) => ({
|
|
264
|
+
packageId,
|
|
265
|
+
sourceIds: [...sourceIds].sort(compareText)
|
|
266
|
+
})).sort((left, right) => compareText(left.packageId, right.packageId)),
|
|
267
|
+
sources: [...sources.values()].sort((left, right) => compareText(left.id, right.id)),
|
|
268
|
+
warnings: warnings.sort(comparableWarning)
|
|
269
|
+
};
|
|
270
|
+
validateSourceGraph(graph);
|
|
271
|
+
return graph;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function fail(errors) {
|
|
275
|
+
if (errors.length > 0) {
|
|
276
|
+
throw new Error(`Source provenance invariant violation: ${errors.sort(compareText).join(' ')}`);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/** Validate canonical ordering and relational rules for the internal graph. */
|
|
281
|
+
export function validateSourceGraph(graph) {
|
|
282
|
+
const errors = [];
|
|
283
|
+
if (!graph || typeof graph !== 'object' || Array.isArray(graph)) {
|
|
284
|
+
throw new Error('Source provenance invariant violation: graph must be an object.');
|
|
285
|
+
}
|
|
286
|
+
const packages = Array.isArray(graph.packages) ? graph.packages : null;
|
|
287
|
+
const sources = Array.isArray(graph.sources) ? graph.sources : null;
|
|
288
|
+
const warnings = Array.isArray(graph.warnings) ? graph.warnings : null;
|
|
289
|
+
if (!packages || !sources || !warnings) {
|
|
290
|
+
throw new Error('Source provenance invariant violation: graph requires packages, sources, and warnings arrays.');
|
|
291
|
+
}
|
|
292
|
+
if (!isSorted(packages, (left, right) => compareText(left.packageId, right.packageId))) errors.push('packages must be sorted by packageId.');
|
|
293
|
+
if (!isSorted(sources, (left, right) => compareText(left.id, right.id))) errors.push('sources must be sorted by id.');
|
|
294
|
+
if (!isSorted(warnings, comparableWarning)) errors.push('warnings must be sorted canonically.');
|
|
295
|
+
|
|
296
|
+
const sourceIds = new Set();
|
|
297
|
+
for (const source of sources) {
|
|
298
|
+
if (!source || typeof source.id !== 'string' || /\s/.test(source.id) || sourceIds.has(source.id)) {
|
|
299
|
+
errors.push('source IDs must be unique non-whitespace strings.');
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
sourceIds.add(source.id);
|
|
303
|
+
}
|
|
304
|
+
const packageIds = new Set();
|
|
305
|
+
for (const packageRecord of packages) {
|
|
306
|
+
if (!packageRecord || typeof packageRecord.packageId !== 'string' || packageIds.has(packageRecord.packageId)) {
|
|
307
|
+
errors.push('package IDs must be unique strings.');
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
packageIds.add(packageRecord.packageId);
|
|
311
|
+
if (!Array.isArray(packageRecord.sourceIds)
|
|
312
|
+
|| !isSorted(packageRecord.sourceIds, compareText)
|
|
313
|
+
|| new Set(packageRecord.sourceIds).size !== packageRecord.sourceIds.length) {
|
|
314
|
+
errors.push(`package ${packageRecord.packageId} sourceIds must be sorted and unique.`);
|
|
315
|
+
}
|
|
316
|
+
for (const sourceId of packageRecord.sourceIds ?? []) {
|
|
317
|
+
if (!sourceIds.has(sourceId)) errors.push(`package ${packageRecord.packageId} references unknown source ${sourceId}.`);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const sourcesById = new Map(sources.map((source) => [source.id, source]));
|
|
322
|
+
for (const source of sources) {
|
|
323
|
+
if (!SOURCE_KINDS.has(source.kind) || !AUTHORITIES.has(source.authority)
|
|
324
|
+
|| !TRUSTS.has(source.trust) || !STATUSES.has(source.status)) {
|
|
325
|
+
errors.push(`source ${source.id} has an unsupported classification.`);
|
|
326
|
+
}
|
|
327
|
+
const portableUrl = source.kind === 'registry'
|
|
328
|
+
? canonicalizeSourceUrl(source.url) !== null
|
|
329
|
+
: isCanonicalPublicSourceUrl(source.url);
|
|
330
|
+
if (!portableUrl) errors.push(`source ${source.id} has a non-portable URL.`);
|
|
331
|
+
if (!Array.isArray(source.supports) || source.supports.length === 0
|
|
332
|
+
|| !isSorted(source.supports, compareText)
|
|
333
|
+
|| new Set(source.supports).size !== source.supports.length) {
|
|
334
|
+
errors.push(`source ${source.id} supports must be sorted, unique, and non-empty.`);
|
|
335
|
+
}
|
|
336
|
+
if (!Array.isArray(source.trustEvidenceSourceIds)
|
|
337
|
+
|| !isSorted(source.trustEvidenceSourceIds, compareText)
|
|
338
|
+
|| new Set(source.trustEvidenceSourceIds).size !== source.trustEvidenceSourceIds.length) {
|
|
339
|
+
errors.push(`source ${source.id} trust evidence must be sorted and unique.`);
|
|
340
|
+
}
|
|
341
|
+
for (const evidenceId of source.trustEvidenceSourceIds ?? []) {
|
|
342
|
+
if (!sourceIds.has(evidenceId)) errors.push(`source ${source.id} references unknown trust evidence ${evidenceId}.`);
|
|
343
|
+
}
|
|
344
|
+
if (source.discoveredFrom !== null && !sourceIds.has(source.discoveredFrom)) {
|
|
345
|
+
errors.push(`source ${source.id} references unknown provenance source.`);
|
|
346
|
+
}
|
|
347
|
+
if (source.kind === 'registry') {
|
|
348
|
+
if (source.discoveredFrom !== null || !source.id.includes(':registry')) {
|
|
349
|
+
errors.push(`registry source ${source.id} must preserve adapter-owned provenance.`);
|
|
350
|
+
}
|
|
351
|
+
} else {
|
|
352
|
+
if (!source.id.endsWith(`:${digest(source.url)}`)) errors.push(`source ${source.id} does not match its canonical URL digest.`);
|
|
353
|
+
if (source.authority === 'officialProject' || source.trust === 'official') {
|
|
354
|
+
errors.push(`source ${source.id} cannot claim official project control without an explicit rule.`);
|
|
355
|
+
}
|
|
356
|
+
if (source.status === 'unverified' && source.snapshot !== null) {
|
|
357
|
+
errors.push(`unfetched source ${source.id} must not have a snapshot.`);
|
|
358
|
+
}
|
|
359
|
+
if (['available', 'stale'].includes(source.status) && source.snapshot === null) {
|
|
360
|
+
errors.push(`fetched source ${source.id} must have a snapshot.`);
|
|
361
|
+
}
|
|
362
|
+
if (['notFound', 'unavailable'].includes(source.status) && source.snapshot !== null) {
|
|
363
|
+
errors.push(`unavailable source ${source.id} must not have a snapshot.`);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
if (source.trust === 'verified') {
|
|
367
|
+
const independent = source.trustEvidenceSourceIds
|
|
368
|
+
.filter((id) => sourcesById.get(id)?.kind === 'registry');
|
|
369
|
+
if (independent.length < 2) errors.push(`verified source ${source.id} lacks independent registry evidence.`);
|
|
370
|
+
}
|
|
371
|
+
if (source.conflictsWith !== undefined) {
|
|
372
|
+
if (!Array.isArray(source.conflictsWith) || !isSorted(source.conflictsWith, compareText)
|
|
373
|
+
|| new Set(source.conflictsWith).size !== source.conflictsWith.length) {
|
|
374
|
+
errors.push(`source ${source.id} conflicts must be sorted and unique.`);
|
|
375
|
+
}
|
|
376
|
+
for (const conflictId of source.conflictsWith ?? []) {
|
|
377
|
+
const conflict = sourcesById.get(conflictId);
|
|
378
|
+
if (!conflict || conflictId === source.id || !conflict.conflictsWith?.includes(source.id)) {
|
|
379
|
+
errors.push(`source ${source.id} has an invalid conflict reference.`);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
for (const warning of warnings) {
|
|
386
|
+
if (warning?.code !== 'SOURCE_CONFLICT' || !packageIds.has(warning.packageId)
|
|
387
|
+
|| !sourceIds.has(warning.sourceId) || warning.retryable !== false) {
|
|
388
|
+
errors.push('warning must be a resolved non-retryable SOURCE_CONFLICT.');
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
fail(errors);
|
|
392
|
+
return graph;
|
|
393
|
+
}
|