@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,346 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { mkdir, open, readFile, rename, rm } from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
import Ajv2020 from 'ajv/dist/2020.js';
|
|
6
|
+
import addFormats from 'ajv-formats';
|
|
7
|
+
|
|
8
|
+
import { canonicalJsonBytes } from './canonical-json.js';
|
|
9
|
+
import {
|
|
10
|
+
CAPABILITY_PROFILE_FILENAME,
|
|
11
|
+
DEPLOYMENT_PROFILE_FILENAME,
|
|
12
|
+
QUALIFICATION_RECORD_FILENAME
|
|
13
|
+
} from './constants.js';
|
|
14
|
+
import { compareText } from './portable.js';
|
|
15
|
+
|
|
16
|
+
export const GOVERNANCE_SCHEMA_VERSION = '1.0.0';
|
|
17
|
+
export const OFFLINE_CONFORMANCE_ENDPOINT = 'https://offline-conformance.invalid/v1/chat/completions';
|
|
18
|
+
|
|
19
|
+
const [capabilitySchema, deploymentSchema, qualificationSchema] = await Promise.all([
|
|
20
|
+
readSchema('../schemas/capability-profile.schema.json'),
|
|
21
|
+
readSchema('../schemas/deployment-profile.schema.json'),
|
|
22
|
+
readSchema('../schemas/qualification-record.schema.json')
|
|
23
|
+
]);
|
|
24
|
+
const ajv = new Ajv2020({ allErrors: true, strict: true, strictRequired: false });
|
|
25
|
+
addFormats(ajv);
|
|
26
|
+
const validateCapabilitySchema = ajv.compile(capabilitySchema);
|
|
27
|
+
const validateDeploymentSchema = ajv.compile(deploymentSchema);
|
|
28
|
+
const validateQualificationSchema = ajv.compile(qualificationSchema);
|
|
29
|
+
|
|
30
|
+
async function readSchema(relativeUrl) {
|
|
31
|
+
return JSON.parse(await readFile(new URL(relativeUrl, import.meta.url), 'utf8'));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const FORBIDDEN_KEY = /(?:authorization|api[_-]?key|secret|token|password|credential|private[_-]?key|prompt|evidence|repository(?:data)?)/i;
|
|
35
|
+
const FORBIDDEN_VALUE = /(?:\bBearer\s+\S+|\bsk-[A-Za-z0-9_-]{8,}|-----BEGIN [A-Z ]+PRIVATE KEY-----)/i;
|
|
36
|
+
|
|
37
|
+
export function assertNoGovernanceSecrets(value, currentPath = '$') {
|
|
38
|
+
if (typeof value === 'string') {
|
|
39
|
+
if (FORBIDDEN_VALUE.test(value)) {
|
|
40
|
+
throw new Error(`Governance metadata validation error: secret-like value at ${currentPath}.`);
|
|
41
|
+
}
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
if (value === null || typeof value !== 'object') return;
|
|
45
|
+
if (Array.isArray(value)) {
|
|
46
|
+
value.forEach((item, index) => assertNoGovernanceSecrets(item, `${currentPath}[${index}]`));
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
for (const [key, item] of Object.entries(value)) {
|
|
50
|
+
if (FORBIDDEN_KEY.test(key)) {
|
|
51
|
+
throw new Error(`Governance metadata validation error: forbidden field ${currentPath}.${key}.`);
|
|
52
|
+
}
|
|
53
|
+
assertNoGovernanceSecrets(item, `${currentPath}.${key}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function schemaError(kind, validate) {
|
|
58
|
+
return new Error(`${kind} validation error: schema validation failed: ${ajv.errorsText(validate.errors, { separator: '; ' })}`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function validateVersion(value, kind) {
|
|
62
|
+
if (value?.schemaVersion !== GOVERNANCE_SCHEMA_VERSION) {
|
|
63
|
+
throw new Error(`${kind} validation error: unsupported schema version; expected ${GOVERNANCE_SCHEMA_VERSION}.`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function sortedUnique(values) {
|
|
68
|
+
return [...new Set(values)].sort(compareText);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function isSorted(values) {
|
|
72
|
+
return values.every((value, index) => index === 0 || compareText(values[index - 1], value) <= 0);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function buildCapabilityProfile({
|
|
76
|
+
capabilityId,
|
|
77
|
+
protocol,
|
|
78
|
+
structuredOutput,
|
|
79
|
+
jsonMode,
|
|
80
|
+
streaming,
|
|
81
|
+
toolCalling,
|
|
82
|
+
responsesApi,
|
|
83
|
+
usageMetadata,
|
|
84
|
+
identityVerification,
|
|
85
|
+
timeoutSupported
|
|
86
|
+
}) {
|
|
87
|
+
return validateCapabilityProfile({
|
|
88
|
+
schemaVersion: GOVERNANCE_SCHEMA_VERSION,
|
|
89
|
+
capabilityId,
|
|
90
|
+
protocol,
|
|
91
|
+
structuredOutput,
|
|
92
|
+
jsonMode,
|
|
93
|
+
streaming,
|
|
94
|
+
toolCalling,
|
|
95
|
+
responsesApi,
|
|
96
|
+
usageMetadata,
|
|
97
|
+
identityVerification,
|
|
98
|
+
timeoutSupported
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function validateCapabilityProfile(profile) {
|
|
103
|
+
assertNoGovernanceSecrets(profile);
|
|
104
|
+
validateCapabilityProfileSchema(profile);
|
|
105
|
+
if (profile.structuredOutput === 'jsonMode' && !profile.jsonMode) {
|
|
106
|
+
throw new Error('Capability Profile validation error: jsonMode must be true when it is the structured output mode.');
|
|
107
|
+
}
|
|
108
|
+
return profile;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function validateCapabilityProfileSchema(profile) {
|
|
112
|
+
validateVersion(profile, 'Capability Profile');
|
|
113
|
+
if (!validateCapabilitySchema(profile)) throw schemaError('Capability Profile', validateCapabilitySchema);
|
|
114
|
+
return profile;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function buildDeploymentProfile({
|
|
118
|
+
deploymentId,
|
|
119
|
+
provider,
|
|
120
|
+
endpoint,
|
|
121
|
+
model,
|
|
122
|
+
capabilityProfile,
|
|
123
|
+
capabilityProfileDigest,
|
|
124
|
+
modelRevision,
|
|
125
|
+
timeoutSeconds,
|
|
126
|
+
maxResponseBytes
|
|
127
|
+
}) {
|
|
128
|
+
return validateDeploymentProfile({
|
|
129
|
+
schemaVersion: GOVERNANCE_SCHEMA_VERSION,
|
|
130
|
+
deploymentId,
|
|
131
|
+
provider,
|
|
132
|
+
endpoint,
|
|
133
|
+
model,
|
|
134
|
+
capabilityProfile,
|
|
135
|
+
capabilityProfileDigest,
|
|
136
|
+
...(modelRevision === undefined ? {} : { modelRevision }),
|
|
137
|
+
timeoutSeconds,
|
|
138
|
+
maxResponseBytes
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function validateDeploymentProfile(profile) {
|
|
143
|
+
assertNoGovernanceSecrets(profile);
|
|
144
|
+
validateDeploymentProfileSchema(profile);
|
|
145
|
+
const endpoint = new URL(profile.endpoint);
|
|
146
|
+
if (!['http:', 'https:'].includes(endpoint.protocol)) {
|
|
147
|
+
throw new Error('Deployment Profile validation error: endpoint must use HTTP or HTTPS.');
|
|
148
|
+
}
|
|
149
|
+
if (endpoint.username || endpoint.password || endpoint.search || endpoint.hash) {
|
|
150
|
+
throw new Error('Deployment Profile validation error: endpoint must not contain credentials, query parameters, or fragments.');
|
|
151
|
+
}
|
|
152
|
+
return profile;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function validateDeploymentProfileSchema(profile) {
|
|
156
|
+
validateVersion(profile, 'Deployment Profile');
|
|
157
|
+
if (!validateDeploymentSchema(profile)) throw schemaError('Deployment Profile', validateDeploymentSchema);
|
|
158
|
+
return profile;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function buildQualificationRecord({
|
|
162
|
+
qualificationId,
|
|
163
|
+
deploymentProfileDigest,
|
|
164
|
+
capabilityProfileDigest,
|
|
165
|
+
conformanceReportDigest = null,
|
|
166
|
+
status,
|
|
167
|
+
qualifiedFor
|
|
168
|
+
}) {
|
|
169
|
+
return validateQualificationRecord({
|
|
170
|
+
schemaVersion: GOVERNANCE_SCHEMA_VERSION,
|
|
171
|
+
qualificationId,
|
|
172
|
+
deploymentProfileDigest,
|
|
173
|
+
capabilityProfileDigest,
|
|
174
|
+
conformanceReportDigest,
|
|
175
|
+
status,
|
|
176
|
+
qualifiedFor: sortedUnique(qualifiedFor)
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function validateQualificationRecord(record) {
|
|
181
|
+
assertNoGovernanceSecrets(record);
|
|
182
|
+
validateQualificationRecordSchema(record);
|
|
183
|
+
if (!isSorted(record.qualifiedFor)) {
|
|
184
|
+
throw new Error('Qualification Record validation error: qualifiedFor must use stable lexical ordering.');
|
|
185
|
+
}
|
|
186
|
+
if (['SUPPORTED', 'CERTIFIED'].includes(record.status) && record.conformanceReportDigest === null) {
|
|
187
|
+
throw new Error(`Qualification Record validation error: ${record.status} requires a conformanceReportDigest.`);
|
|
188
|
+
}
|
|
189
|
+
return record;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export function validateQualificationRecordSchema(record) {
|
|
193
|
+
validateVersion(record, 'Qualification Record');
|
|
194
|
+
if (!validateQualificationSchema(record)) throw schemaError('Qualification Record', validateQualificationSchema);
|
|
195
|
+
return record;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function metadataDigest(value, validate) {
|
|
199
|
+
validate(value);
|
|
200
|
+
return `sha256:${createHash('sha256').update(canonicalJsonBytes(value)).digest('hex')}`;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export function capabilityProfileDigest(profile) {
|
|
204
|
+
return metadataDigest(profile, validateCapabilityProfile);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export function deploymentProfileDigest(profile) {
|
|
208
|
+
return metadataDigest(profile, validateDeploymentProfile);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export function qualificationRecordDigest(record) {
|
|
212
|
+
return metadataDigest(record, validateQualificationRecord);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function serialize(value, validate) {
|
|
216
|
+
validate(value);
|
|
217
|
+
return `${JSON.stringify(value, null, 2)}\n`;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export function serializeCapabilityProfile(profile) {
|
|
221
|
+
return serialize(profile, validateCapabilityProfile);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export function serializeDeploymentProfile(profile) {
|
|
225
|
+
return serialize(profile, validateDeploymentProfile);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export function serializeQualificationRecord(record) {
|
|
229
|
+
return serialize(record, validateQualificationRecord);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
async function writePrivateJson(outputPath, value, serializer) {
|
|
233
|
+
const target = path.resolve(outputPath);
|
|
234
|
+
const temporary = `${target}.${process.pid}.tmp`;
|
|
235
|
+
await mkdir(path.dirname(target), { recursive: true });
|
|
236
|
+
let handle;
|
|
237
|
+
try {
|
|
238
|
+
handle = await open(temporary, 'w', 0o600);
|
|
239
|
+
await handle.writeFile(serializer(value), 'utf8');
|
|
240
|
+
await handle.sync();
|
|
241
|
+
await handle.close();
|
|
242
|
+
handle = null;
|
|
243
|
+
await rename(temporary, target);
|
|
244
|
+
} catch (error) {
|
|
245
|
+
await handle?.close().catch(() => {});
|
|
246
|
+
await rm(temporary, { force: true }).catch(() => {});
|
|
247
|
+
throw error;
|
|
248
|
+
}
|
|
249
|
+
return target;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export function writeCapabilityProfile(outputPath, profile) {
|
|
253
|
+
return writePrivateJson(outputPath, profile, serializeCapabilityProfile);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export function writeDeploymentProfile(outputPath, profile) {
|
|
257
|
+
return writePrivateJson(outputPath, profile, serializeDeploymentProfile);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export function writeQualificationRecord(outputPath, record) {
|
|
261
|
+
return writePrivateJson(outputPath, record, serializeQualificationRecord);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function portableId(value, fallback) {
|
|
265
|
+
const normalized = String(value ?? '')
|
|
266
|
+
.toLowerCase()
|
|
267
|
+
.replace(/[^a-z0-9._-]+/g, '-')
|
|
268
|
+
.replace(/^[^a-z0-9]+|[^a-z0-9]+$/g, '')
|
|
269
|
+
.slice(0, 96);
|
|
270
|
+
return normalized || fallback;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
export function createDefaultGovernanceArtifacts({
|
|
274
|
+
provider = 'openai-compatible',
|
|
275
|
+
endpoint = OFFLINE_CONFORMANCE_ENDPOINT,
|
|
276
|
+
model = 'offline-fixture',
|
|
277
|
+
timeoutSeconds = 180,
|
|
278
|
+
maxResponseBytes = 1_048_576,
|
|
279
|
+
qualifiedFor = ['MVP-03']
|
|
280
|
+
} = {}) {
|
|
281
|
+
const providerId = portableId(provider, 'runtime');
|
|
282
|
+
const modelId = portableId(model, 'model');
|
|
283
|
+
const capabilityProfile = buildCapabilityProfile({
|
|
284
|
+
capabilityId: `${providerId}-v1`,
|
|
285
|
+
protocol: 'chat-completions',
|
|
286
|
+
structuredOutput: 'jsonSchema',
|
|
287
|
+
jsonMode: false,
|
|
288
|
+
streaming: false,
|
|
289
|
+
toolCalling: false,
|
|
290
|
+
responsesApi: false,
|
|
291
|
+
usageMetadata: true,
|
|
292
|
+
identityVerification: true,
|
|
293
|
+
timeoutSupported: true
|
|
294
|
+
});
|
|
295
|
+
const deploymentId = portableId(`${providerId}-${modelId}`, 'offline-deployment');
|
|
296
|
+
const deploymentProfile = buildDeploymentProfile({
|
|
297
|
+
deploymentId,
|
|
298
|
+
provider,
|
|
299
|
+
endpoint,
|
|
300
|
+
model,
|
|
301
|
+
capabilityProfile: capabilityProfile.capabilityId,
|
|
302
|
+
capabilityProfileDigest: capabilityProfileDigest(capabilityProfile),
|
|
303
|
+
timeoutSeconds,
|
|
304
|
+
maxResponseBytes
|
|
305
|
+
});
|
|
306
|
+
const qualificationRecord = buildQualificationRecord({
|
|
307
|
+
qualificationId: portableId(`${deploymentProfile.deploymentId}-mvp-03`, 'runtime-mvp-03'),
|
|
308
|
+
deploymentProfileDigest: deploymentProfileDigest(deploymentProfile),
|
|
309
|
+
capabilityProfileDigest: capabilityProfileDigest(capabilityProfile),
|
|
310
|
+
conformanceReportDigest: null,
|
|
311
|
+
status: 'EXPERIMENTAL',
|
|
312
|
+
qualifiedFor
|
|
313
|
+
});
|
|
314
|
+
return { capabilityProfile, deploymentProfile, qualificationRecord };
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
export function serializeGovernanceArtifacts(artifacts) {
|
|
318
|
+
validateCapabilityProfile(artifacts?.capabilityProfile);
|
|
319
|
+
validateDeploymentProfile(artifacts?.deploymentProfile);
|
|
320
|
+
validateQualificationRecord(artifacts?.qualificationRecord);
|
|
321
|
+
return `${JSON.stringify({
|
|
322
|
+
capabilityProfile: artifacts.capabilityProfile,
|
|
323
|
+
deploymentProfile: artifacts.deploymentProfile,
|
|
324
|
+
qualificationRecord: artifacts.qualificationRecord
|
|
325
|
+
}, null, 2)}\n`;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
export async function writeGovernanceArtifacts(outputDirectory, artifacts) {
|
|
329
|
+
validateCapabilityProfile(artifacts?.capabilityProfile);
|
|
330
|
+
validateDeploymentProfile(artifacts?.deploymentProfile);
|
|
331
|
+
validateQualificationRecord(artifacts?.qualificationRecord);
|
|
332
|
+
const directory = path.resolve(outputDirectory);
|
|
333
|
+
const capabilityProfile = await writeCapabilityProfile(
|
|
334
|
+
path.join(directory, CAPABILITY_PROFILE_FILENAME),
|
|
335
|
+
artifacts.capabilityProfile
|
|
336
|
+
);
|
|
337
|
+
const deploymentProfile = await writeDeploymentProfile(
|
|
338
|
+
path.join(directory, DEPLOYMENT_PROFILE_FILENAME),
|
|
339
|
+
artifacts.deploymentProfile
|
|
340
|
+
);
|
|
341
|
+
const qualificationRecord = await writeQualificationRecord(
|
|
342
|
+
path.join(directory, QUALIFICATION_RECORD_FILENAME),
|
|
343
|
+
artifacts.qualificationRecord
|
|
344
|
+
);
|
|
345
|
+
return { capabilityProfile, deploymentProfile, qualificationRecord };
|
|
346
|
+
}
|
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
import { canonicalJsonBytes } from './canonical-json.js';
|
|
4
|
+
import { validateConformanceReport } from './conformance-report.js';
|
|
5
|
+
import {
|
|
6
|
+
assertNoGovernanceSecrets,
|
|
7
|
+
validateCapabilityProfileSchema,
|
|
8
|
+
validateDeploymentProfileSchema,
|
|
9
|
+
validateQualificationRecordSchema
|
|
10
|
+
} from './governance-metadata.js';
|
|
11
|
+
import {
|
|
12
|
+
GOVERNANCE_VALIDATION_STAGES,
|
|
13
|
+
createGovernanceDiagnostic,
|
|
14
|
+
sortGovernanceDiagnostics
|
|
15
|
+
} from './governance-diagnostics.js';
|
|
16
|
+
import { compareText } from './portable.js';
|
|
17
|
+
|
|
18
|
+
const ARTIFACTS = Object.freeze([
|
|
19
|
+
{
|
|
20
|
+
key: 'capabilityProfile',
|
|
21
|
+
unknownCode: 'UNKNOWN_CAPABILITY',
|
|
22
|
+
validateSchema: validateCapabilityProfileSchema
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
key: 'deploymentProfile',
|
|
26
|
+
unknownCode: 'UNKNOWN_DEPLOYMENT',
|
|
27
|
+
validateSchema: validateDeploymentProfileSchema
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
key: 'conformanceReport',
|
|
31
|
+
unknownCode: 'UNKNOWN_CONFORMANCE',
|
|
32
|
+
validateSchema: validateConformanceReport
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
key: 'qualificationRecord',
|
|
36
|
+
unknownCode: 'UNKNOWN_QUALIFICATION',
|
|
37
|
+
validateSchema: validateQualificationRecordSchema
|
|
38
|
+
}
|
|
39
|
+
]);
|
|
40
|
+
|
|
41
|
+
function digest(value) {
|
|
42
|
+
return `sha256:${createHash('sha256').update(canonicalJsonBytes(value)).digest('hex')}`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function addDiagnostic(diagnostics, diagnostic) {
|
|
46
|
+
diagnostics.push(createGovernanceDiagnostic(diagnostic));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function hasInitialDiagnostic(initialDiagnostics, artifact) {
|
|
50
|
+
return initialDiagnostics.some((diagnostic) => diagnostic.artifact === artifact);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function validateSchemas(artifacts, initialDiagnostics, diagnostics) {
|
|
54
|
+
const valid = {};
|
|
55
|
+
for (const definition of ARTIFACTS) {
|
|
56
|
+
const value = artifacts?.[definition.key];
|
|
57
|
+
if (value === undefined) {
|
|
58
|
+
if (!hasInitialDiagnostic(initialDiagnostics, definition.key)) {
|
|
59
|
+
addDiagnostic(diagnostics, {
|
|
60
|
+
stage: GOVERNANCE_VALIDATION_STAGES.LOAD_JSON,
|
|
61
|
+
code: definition.unknownCode,
|
|
62
|
+
artifact: definition.key,
|
|
63
|
+
message: 'Required governance artifact is unavailable.'
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
valid[definition.key] = false;
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
try {
|
|
70
|
+
definition.validateSchema(value);
|
|
71
|
+
valid[definition.key] = true;
|
|
72
|
+
} catch {
|
|
73
|
+
addDiagnostic(diagnostics, {
|
|
74
|
+
stage: GOVERNANCE_VALIDATION_STAGES.SCHEMA_VALIDATION,
|
|
75
|
+
code: 'INVALID_SCHEMA',
|
|
76
|
+
artifact: definition.key,
|
|
77
|
+
message: 'Governance artifact failed schema or intrinsic invariant validation.'
|
|
78
|
+
});
|
|
79
|
+
valid[definition.key] = false;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return valid;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function calculateDigests(artifacts, valid, diagnostics) {
|
|
86
|
+
const digests = {};
|
|
87
|
+
for (const definition of ARTIFACTS) {
|
|
88
|
+
if (!valid[definition.key]) continue;
|
|
89
|
+
try {
|
|
90
|
+
digests[definition.key] = digest(artifacts[definition.key]);
|
|
91
|
+
} catch {
|
|
92
|
+
addDiagnostic(diagnostics, {
|
|
93
|
+
stage: GOVERNANCE_VALIDATION_STAGES.DIGEST_VERIFICATION,
|
|
94
|
+
code: 'DIGEST_MISMATCH',
|
|
95
|
+
artifact: definition.key,
|
|
96
|
+
message: 'Canonical SHA-256 digest could not be calculated.'
|
|
97
|
+
});
|
|
98
|
+
valid[definition.key] = false;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return digests;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function compareDigest(diagnostics, artifact, path, actual, referenced) {
|
|
105
|
+
if (actual === referenced) return;
|
|
106
|
+
addDiagnostic(diagnostics, {
|
|
107
|
+
stage: GOVERNANCE_VALIDATION_STAGES.CROSS_ARTIFACT_VALIDATION,
|
|
108
|
+
code: 'DIGEST_MISMATCH',
|
|
109
|
+
artifact,
|
|
110
|
+
path,
|
|
111
|
+
message: 'Referenced digest does not match the canonical upstream artifact digest.'
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function validateCrossArtifactLinks(artifacts, valid, digests, diagnostics) {
|
|
116
|
+
const capability = artifacts.capabilityProfile;
|
|
117
|
+
const deployment = artifacts.deploymentProfile;
|
|
118
|
+
const conformance = artifacts.conformanceReport;
|
|
119
|
+
const qualification = artifacts.qualificationRecord;
|
|
120
|
+
|
|
121
|
+
if (valid.capabilityProfile && valid.deploymentProfile) {
|
|
122
|
+
if (deployment.capabilityProfile !== capability.capabilityId) {
|
|
123
|
+
addDiagnostic(diagnostics, {
|
|
124
|
+
stage: GOVERNANCE_VALIDATION_STAGES.CROSS_ARTIFACT_VALIDATION,
|
|
125
|
+
code: 'UNKNOWN_CAPABILITY',
|
|
126
|
+
artifact: 'deploymentProfile',
|
|
127
|
+
path: '$.capabilityProfile',
|
|
128
|
+
message: 'Deployment Profile references a different Capability Profile ID.'
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
compareDigest(
|
|
132
|
+
diagnostics,
|
|
133
|
+
'deploymentProfile',
|
|
134
|
+
'$.capabilityProfileDigest',
|
|
135
|
+
digests.capabilityProfile,
|
|
136
|
+
deployment.capabilityProfileDigest
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (valid.capabilityProfile && valid.conformanceReport) {
|
|
141
|
+
compareDigest(
|
|
142
|
+
diagnostics,
|
|
143
|
+
'conformanceReport',
|
|
144
|
+
'$.capabilityProfileDigest',
|
|
145
|
+
digests.capabilityProfile,
|
|
146
|
+
conformance.capabilityProfileDigest
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
if (valid.capabilityProfile && valid.qualificationRecord) {
|
|
150
|
+
compareDigest(
|
|
151
|
+
diagnostics,
|
|
152
|
+
'qualificationRecord',
|
|
153
|
+
'$.capabilityProfileDigest',
|
|
154
|
+
digests.capabilityProfile,
|
|
155
|
+
qualification.capabilityProfileDigest
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (valid.deploymentProfile && valid.conformanceReport) {
|
|
160
|
+
compareDigest(
|
|
161
|
+
diagnostics,
|
|
162
|
+
'conformanceReport',
|
|
163
|
+
'$.deploymentProfileDigest',
|
|
164
|
+
digests.deploymentProfile,
|
|
165
|
+
conformance.deploymentProfileDigest
|
|
166
|
+
);
|
|
167
|
+
if (conformance.runtime.provider !== deployment.provider
|
|
168
|
+
|| conformance.runtime.model !== deployment.model) {
|
|
169
|
+
addDiagnostic(diagnostics, {
|
|
170
|
+
stage: GOVERNANCE_VALIDATION_STAGES.CROSS_ARTIFACT_VALIDATION,
|
|
171
|
+
code: 'UNKNOWN_DEPLOYMENT',
|
|
172
|
+
artifact: 'conformanceReport',
|
|
173
|
+
path: '$.runtime',
|
|
174
|
+
message: 'Conformance runtime identity does not match the Deployment Profile.'
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
if (valid.deploymentProfile && valid.qualificationRecord) {
|
|
179
|
+
compareDigest(
|
|
180
|
+
diagnostics,
|
|
181
|
+
'qualificationRecord',
|
|
182
|
+
'$.deploymentProfileDigest',
|
|
183
|
+
digests.deploymentProfile,
|
|
184
|
+
qualification.deploymentProfileDigest
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (valid.conformanceReport && valid.qualificationRecord) {
|
|
189
|
+
if (qualification.conformanceReportDigest === null) {
|
|
190
|
+
addDiagnostic(diagnostics, {
|
|
191
|
+
stage: GOVERNANCE_VALIDATION_STAGES.CROSS_ARTIFACT_VALIDATION,
|
|
192
|
+
code: 'UNKNOWN_CONFORMANCE',
|
|
193
|
+
artifact: 'qualificationRecord',
|
|
194
|
+
path: '$.conformanceReportDigest',
|
|
195
|
+
message: 'Qualification Record does not reference the loaded Conformance Report.'
|
|
196
|
+
});
|
|
197
|
+
} else {
|
|
198
|
+
compareDigest(
|
|
199
|
+
diagnostics,
|
|
200
|
+
'qualificationRecord',
|
|
201
|
+
'$.conformanceReportDigest',
|
|
202
|
+
digests.conformanceReport,
|
|
203
|
+
qualification.conformanceReportDigest
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function mutableModelAlias(model) {
|
|
210
|
+
return /(?:^|[:/@._-])(?:latest|main|master|stable|current)(?:$|[:/@._-])/i.test(model);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function validatePolicy(artifacts, valid, diagnostics) {
|
|
214
|
+
for (const definition of ARTIFACTS.filter((entry) => entry.key !== 'conformanceReport')) {
|
|
215
|
+
if (!valid[definition.key]) continue;
|
|
216
|
+
try {
|
|
217
|
+
assertNoGovernanceSecrets(artifacts[definition.key]);
|
|
218
|
+
} catch {
|
|
219
|
+
addDiagnostic(diagnostics, {
|
|
220
|
+
stage: GOVERNANCE_VALIDATION_STAGES.GOVERNANCE_POLICY_VALIDATION,
|
|
221
|
+
code: 'GOVERNANCE_POLICY_VIOLATION',
|
|
222
|
+
artifact: definition.key,
|
|
223
|
+
message: 'Governance artifact contains a forbidden field or secret-like value.'
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const capability = artifacts.capabilityProfile;
|
|
229
|
+
if (valid.capabilityProfile && capability.structuredOutput === 'jsonMode' && !capability.jsonMode) {
|
|
230
|
+
addDiagnostic(diagnostics, {
|
|
231
|
+
stage: GOVERNANCE_VALIDATION_STAGES.GOVERNANCE_POLICY_VALIDATION,
|
|
232
|
+
code: 'GOVERNANCE_POLICY_VIOLATION',
|
|
233
|
+
artifact: 'capabilityProfile',
|
|
234
|
+
path: '$.jsonMode',
|
|
235
|
+
message: 'jsonMode must be true when it is the declared structured output mode.'
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const deployment = artifacts.deploymentProfile;
|
|
240
|
+
if (valid.deploymentProfile) {
|
|
241
|
+
const endpoint = new URL(deployment.endpoint);
|
|
242
|
+
if (!['http:', 'https:'].includes(endpoint.protocol)
|
|
243
|
+
|| endpoint.username || endpoint.password || endpoint.search || endpoint.hash) {
|
|
244
|
+
addDiagnostic(diagnostics, {
|
|
245
|
+
stage: GOVERNANCE_VALIDATION_STAGES.GOVERNANCE_POLICY_VALIDATION,
|
|
246
|
+
code: 'GOVERNANCE_POLICY_VIOLATION',
|
|
247
|
+
artifact: 'deploymentProfile',
|
|
248
|
+
path: '$.endpoint',
|
|
249
|
+
message: 'Deployment endpoint violates portable credential-free URL policy.'
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const qualification = artifacts.qualificationRecord;
|
|
255
|
+
const conformance = artifacts.conformanceReport;
|
|
256
|
+
if (valid.qualificationRecord) {
|
|
257
|
+
if (!qualification.qualifiedFor.every((task, index, tasks) => (
|
|
258
|
+
index === 0 || compareText(tasks[index - 1], task) < 0
|
|
259
|
+
))) {
|
|
260
|
+
addDiagnostic(diagnostics, {
|
|
261
|
+
stage: GOVERNANCE_VALIDATION_STAGES.GOVERNANCE_POLICY_VALIDATION,
|
|
262
|
+
code: 'GOVERNANCE_POLICY_VIOLATION',
|
|
263
|
+
artifact: 'qualificationRecord',
|
|
264
|
+
path: '$.qualifiedFor',
|
|
265
|
+
message: 'Qualification task scope must be unique and lexically ordered.'
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
if (['SUPPORTED', 'CERTIFIED'].includes(qualification.status)
|
|
269
|
+
&& qualification.conformanceReportDigest === null) {
|
|
270
|
+
addDiagnostic(diagnostics, {
|
|
271
|
+
stage: GOVERNANCE_VALIDATION_STAGES.GOVERNANCE_POLICY_VALIDATION,
|
|
272
|
+
code: 'INVALID_CERTIFICATION_STATE',
|
|
273
|
+
artifact: 'qualificationRecord',
|
|
274
|
+
path: '$.status',
|
|
275
|
+
message: 'SUPPORTED and CERTIFIED require a Conformance Report digest.'
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (valid.qualificationRecord && valid.conformanceReport
|
|
281
|
+
&& ['SUPPORTED', 'CERTIFIED'].includes(qualification.status)
|
|
282
|
+
&& conformance.recommendation !== 'CONFORMANT') {
|
|
283
|
+
addDiagnostic(diagnostics, {
|
|
284
|
+
stage: GOVERNANCE_VALIDATION_STAGES.GOVERNANCE_POLICY_VALIDATION,
|
|
285
|
+
code: 'INVALID_CERTIFICATION_STATE',
|
|
286
|
+
artifact: 'qualificationRecord',
|
|
287
|
+
path: '$.status',
|
|
288
|
+
message: 'SUPPORTED and CERTIFIED require a CONFORMANT Conformance Report.'
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (valid.qualificationRecord && valid.deploymentProfile
|
|
293
|
+
&& qualification.status === 'CERTIFIED'
|
|
294
|
+
&& mutableModelAlias(deployment.model)
|
|
295
|
+
&& (typeof deployment.modelRevision !== 'string'
|
|
296
|
+
|| mutableModelAlias(deployment.modelRevision))) {
|
|
297
|
+
addDiagnostic(diagnostics, {
|
|
298
|
+
stage: GOVERNANCE_VALIDATION_STAGES.GOVERNANCE_POLICY_VALIDATION,
|
|
299
|
+
code: 'INVALID_CERTIFICATION_STATE',
|
|
300
|
+
artifact: 'qualificationRecord',
|
|
301
|
+
path: '$.status',
|
|
302
|
+
message: 'CERTIFIED is not allowed for a mutable model alias without modelRevision pinning.'
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function requestedTasks(options) {
|
|
308
|
+
const tasks = [
|
|
309
|
+
...(typeof options.task === 'string' ? [options.task] : []),
|
|
310
|
+
...(Array.isArray(options.tasks) ? options.tasks : [])
|
|
311
|
+
];
|
|
312
|
+
return [...new Set(tasks.filter((task) => typeof task === 'string' && task.length > 0))].sort(compareText);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function validateTaskScope(artifacts, valid, options, diagnostics) {
|
|
316
|
+
if (!valid.qualificationRecord) return;
|
|
317
|
+
const qualified = new Set(artifacts.qualificationRecord.qualifiedFor);
|
|
318
|
+
for (const [index, task] of requestedTasks(options).entries()) {
|
|
319
|
+
if (qualified.has(task)) continue;
|
|
320
|
+
addDiagnostic(diagnostics, {
|
|
321
|
+
stage: GOVERNANCE_VALIDATION_STAGES.TASK_SCOPE_VALIDATION,
|
|
322
|
+
code: 'TASK_SCOPE_MISMATCH',
|
|
323
|
+
artifact: 'qualificationRecord',
|
|
324
|
+
path: `$.requestedTasks[${index}]`,
|
|
325
|
+
message: 'Qualification Record does not explicitly cover the requested task.'
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
export function validateGovernanceArtifacts(artifacts, options = {}) {
|
|
331
|
+
const initialDiagnostics = Array.isArray(options.initialDiagnostics)
|
|
332
|
+
? options.initialDiagnostics
|
|
333
|
+
: [];
|
|
334
|
+
const diagnostics = [...initialDiagnostics];
|
|
335
|
+
const validArtifacts = validateSchemas(artifacts, initialDiagnostics, diagnostics);
|
|
336
|
+
const digests = calculateDigests(artifacts, validArtifacts, diagnostics);
|
|
337
|
+
validateCrossArtifactLinks(artifacts, validArtifacts, digests, diagnostics);
|
|
338
|
+
validatePolicy(artifacts, validArtifacts, diagnostics);
|
|
339
|
+
validateTaskScope(artifacts, validArtifacts, options, diagnostics);
|
|
340
|
+
const sortedDiagnostics = sortGovernanceDiagnostics(diagnostics);
|
|
341
|
+
const valid = sortedDiagnostics.length === 0;
|
|
342
|
+
return {
|
|
343
|
+
valid,
|
|
344
|
+
diagnostics: sortedDiagnostics,
|
|
345
|
+
bundle: valid
|
|
346
|
+
? {
|
|
347
|
+
capabilityProfile: artifacts.capabilityProfile,
|
|
348
|
+
deploymentProfile: artifacts.deploymentProfile,
|
|
349
|
+
conformanceReport: artifacts.conformanceReport,
|
|
350
|
+
qualificationRecord: artifacts.qualificationRecord,
|
|
351
|
+
digests: {
|
|
352
|
+
capabilityProfile: digests.capabilityProfile,
|
|
353
|
+
deploymentProfile: digests.deploymentProfile,
|
|
354
|
+
conformanceReport: digests.conformanceReport,
|
|
355
|
+
qualificationRecord: digests.qualificationRecord
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
: null
|
|
359
|
+
};
|
|
360
|
+
}
|