@aiwg/cli 2026.9.0 → 2026.9.2
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/agentic/code/providers/capability-matrix.yaml +41 -0
- package/agentic/code/providers/model-capabilities.v1.json +11 -0
- package/agentic/code/providers/model-catalog.v1.json +8 -0
- package/agentic/code/providers/pi/aiwg-bridge.ts +26 -0
- package/bin/aiwg.mjs +15 -3
- package/dist/src/api/index.d.ts +3 -0
- package/dist/src/api/index.js +3 -0
- package/dist/src/auth/credential-store.js +6 -0
- package/dist/src/channel/manager.mjs +2 -2
- package/dist/src/cli/handlers/dataset.js +186 -0
- package/dist/src/cli/handlers/help.js +2 -1
- package/dist/src/cli/handlers/index.js +5 -1
- package/dist/src/cli/handlers/init.js +1 -0
- package/dist/src/cli/handlers/output-mode.js +18 -1
- package/dist/src/cli/handlers/run.js +11 -3
- package/dist/src/cli/handlers/schema.js +221 -0
- package/dist/src/cli/handlers/sessions.js +30 -12
- package/dist/src/cli/handlers/steward.js +11 -1
- package/dist/src/cli/handlers/use.js +6 -2
- package/dist/src/cli/hooks/builtin/activity-log-hook.js +6 -0
- package/dist/src/cli/router.js +1 -1
- package/dist/src/cli/scope-resolver.js +22 -0
- package/dist/src/dataset/adapter-sdk.d.ts +41 -0
- package/dist/src/dataset/adapter-sdk.js +147 -0
- package/dist/src/dataset/adapter-types.d.ts +179 -0
- package/dist/src/dataset/adapter-types.js +2 -0
- package/dist/src/dataset/adapters.d.ts +104 -0
- package/dist/src/dataset/adapters.js +518 -0
- package/dist/src/dataset/conformance-types.d.ts +84 -0
- package/dist/src/dataset/conformance-types.js +3 -0
- package/dist/src/dataset/conformance.d.ts +13 -0
- package/dist/src/dataset/conformance.js +90 -0
- package/dist/src/dataset/contracts.d.ts +17 -0
- package/dist/src/dataset/contracts.js +236 -0
- package/dist/src/dataset/file-orchestration-repository.d.ts +19 -0
- package/dist/src/dataset/file-orchestration-repository.js +68 -0
- package/dist/src/dataset/fortemi-execution-bridge.d.ts +33 -0
- package/dist/src/dataset/fortemi-execution-bridge.js +35 -0
- package/dist/src/dataset/index.d.ts +21 -0
- package/dist/src/dataset/index.js +21 -0
- package/dist/src/dataset/ledger-types.d.ts +141 -0
- package/dist/src/dataset/ledger-types.js +2 -0
- package/dist/src/dataset/ledger.d.ts +27 -0
- package/dist/src/dataset/ledger.js +100 -0
- package/dist/src/dataset/local-execution-backend.d.ts +10 -0
- package/dist/src/dataset/local-execution-backend.js +32 -0
- package/dist/src/dataset/orchestration-repository.d.ts +29 -0
- package/dist/src/dataset/orchestration-repository.js +34 -0
- package/dist/src/dataset/orchestration-service.d.ts +56 -0
- package/dist/src/dataset/orchestration-service.js +466 -0
- package/dist/src/dataset/orchestration-types.d.ts +83 -0
- package/dist/src/dataset/orchestration-types.js +2 -0
- package/dist/src/dataset/presentation.d.ts +3 -0
- package/dist/src/dataset/presentation.js +8 -0
- package/dist/src/dataset/projections.d.ts +42 -0
- package/dist/src/dataset/projections.js +192 -0
- package/dist/src/dataset/schema-governance.d.ts +71 -0
- package/dist/src/dataset/schema-governance.js +135 -0
- package/dist/src/dataset/standards-types.d.ts +66 -0
- package/dist/src/dataset/standards-types.js +10 -0
- package/dist/src/dataset/standards.d.ts +13 -0
- package/dist/src/dataset/standards.js +291 -0
- package/dist/src/dataset/types.d.ts +258 -0
- package/dist/src/dataset/types.js +2 -0
- package/dist/src/extensions/commands/definitions.js +27 -1
- package/dist/src/installation/manager.mjs +5 -1
- package/dist/src/models/model-capabilities.v1.json +11 -0
- package/dist/src/models/model-catalog.v1.json +8 -0
- package/dist/src/models/model-discovery.js +32 -0
- package/dist/src/models/provider-policy.js +3 -2
- package/dist/src/output-modes/index.js +4 -0
- package/dist/src/output-modes/registry.js +68 -24
- package/dist/src/output-modes/runtime.js +10 -8
- package/dist/src/plugin/skill-command-translator.js +1 -0
- package/dist/src/providers/capability-matrix.yaml +41 -0
- package/dist/src/providers/provider-definitions.js +72 -0
- package/dist/src/providers/provider-inventory.js +1 -0
- package/dist/src/schema/catalog.js +234 -0
- package/dist/src/schema/compatibility.js +42 -0
- package/dist/src/schema/diagnostics.js +36 -0
- package/dist/src/schema/index.js +8 -0
- package/dist/src/schema/policy.js +58 -0
- package/dist/src/schema/resolver.js +76 -0
- package/dist/src/schema/types.js +2 -0
- package/dist/src/schema/validator.js +82 -0
- package/dist/src/sessions/adapters/pi.js +141 -0
- package/dist/src/sessions/contracts.js +1 -1
- package/dist/src/sessions/index.js +1 -0
- package/dist/src/sessions/workspace-discovery.js +10 -0
- package/dist/src/storage/backends/fortemi.js +6 -0
- package/dist/src/storage/config.js +18 -4
- package/dist/src/storage/fortemi-qualification.js +106 -0
- package/dist/src/storage/index.js +1 -0
- package/dist/src/storage/types.js +1 -1
- package/package.json +3 -1
- package/schemas/dataset/conformance-manifest.v1.schema.json +35 -0
- package/schemas/dataset/conformance-receipt.v1.schema.json +22 -0
- package/schemas/dataset/dataset-contracts.v1.schema.json +117 -0
- package/schemas/dataset/dataset-deprecations.v1.schema.json +37 -0
- package/schemas/dataset/dataset-schema-governance.v1.schema.json +92 -0
- package/schemas/dataset/dataset-standards-exchange.v1.schema.json +59 -0
- package/schemas/dataset/profiles/openlineage-1.0.0.schema.json +15 -0
- package/schemas/dataset/profiles/prov-json-20130430.schema.json +12 -0
- package/schemas/dataset/run-ledger.v1.schema.json +39 -0
- package/schemas/dataset/source-adapter.v1.schema.json +112 -0
- package/tools/agents/deploy-agents.mjs +9 -3
- package/tools/agents/providers/pi.mjs +176 -0
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
3
|
+
import { dirname, isAbsolute, relative, resolve, sep } from 'node:path';
|
|
4
|
+
import { createDiagnostic, hasSchemaErrors, SCHEMA_DIAGNOSTIC_CODES } from './diagnostics.js';
|
|
5
|
+
import { diagnosePolicy, resolveEffectivePolicy } from './policy.js';
|
|
6
|
+
function parseJson(path) {
|
|
7
|
+
return JSON.parse(readFileSync(path, 'utf8'));
|
|
8
|
+
}
|
|
9
|
+
function isRecord(value) {
|
|
10
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
11
|
+
}
|
|
12
|
+
function isDomainManifest(value) {
|
|
13
|
+
return (isRecord(value) &&
|
|
14
|
+
value.schemaVersion === '1' &&
|
|
15
|
+
typeof value.domain === 'string' &&
|
|
16
|
+
isRecord(value.owner) &&
|
|
17
|
+
typeof value.owner.id === 'string' &&
|
|
18
|
+
Array.isArray(value.artifacts));
|
|
19
|
+
}
|
|
20
|
+
function isCatalogSource(value) {
|
|
21
|
+
return isRecord(value) && value.schemaVersion === '1' && Array.isArray(value.domains);
|
|
22
|
+
}
|
|
23
|
+
function safePath(rootDir, path) {
|
|
24
|
+
const absolute = resolve(rootDir, path);
|
|
25
|
+
const rel = relative(resolve(rootDir), absolute);
|
|
26
|
+
return rel === '' || (!rel.startsWith(`..${sep}`) && rel !== '..' && !isAbsolute(rel)) ? absolute : undefined;
|
|
27
|
+
}
|
|
28
|
+
function sha256(path) {
|
|
29
|
+
return `sha256:${createHash('sha256').update(readFileSync(path)).digest('hex')}`;
|
|
30
|
+
}
|
|
31
|
+
function validateArtifactShape(artifact, domain) {
|
|
32
|
+
const diagnostics = [];
|
|
33
|
+
const required = [
|
|
34
|
+
'logicalName',
|
|
35
|
+
'id',
|
|
36
|
+
'version',
|
|
37
|
+
'format',
|
|
38
|
+
'lifecycle',
|
|
39
|
+
'owner',
|
|
40
|
+
'authority',
|
|
41
|
+
];
|
|
42
|
+
for (const field of required) {
|
|
43
|
+
if (artifact[field] === undefined || artifact[field] === '') {
|
|
44
|
+
diagnostics.push(createDiagnostic(SCHEMA_DIAGNOSTIC_CODES.invalidManifest, `Artifact in domain ${domain} is missing required field ${field}`, { artifactId: artifact.id }));
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (artifact.format === 'json-schema' && !artifact.dialect) {
|
|
48
|
+
diagnostics.push(createDiagnostic(SCHEMA_DIAGNOSTIC_CODES.invalidManifest, 'JSON Schema artifacts require a dialect', {
|
|
49
|
+
artifactId: artifact.id,
|
|
50
|
+
}));
|
|
51
|
+
}
|
|
52
|
+
if (artifact.lifecycle === 'deprecated' && artifact.deprecation === undefined) {
|
|
53
|
+
diagnostics.push(createDiagnostic(SCHEMA_DIAGNOSTIC_CODES.invalidManifest, 'Deprecated artifacts require deprecation data', {
|
|
54
|
+
artifactId: artifact.id,
|
|
55
|
+
}));
|
|
56
|
+
}
|
|
57
|
+
return diagnostics;
|
|
58
|
+
}
|
|
59
|
+
function walkSchemaFiles(root) {
|
|
60
|
+
if (!existsSync(root))
|
|
61
|
+
return [];
|
|
62
|
+
const result = [];
|
|
63
|
+
for (const entry of readdirSync(root).sort()) {
|
|
64
|
+
const path = resolve(root, entry);
|
|
65
|
+
const stat = statSync(path);
|
|
66
|
+
if (stat.isDirectory())
|
|
67
|
+
result.push(...walkSchemaFiles(path));
|
|
68
|
+
else if (/\.schema\.json$/u.test(entry))
|
|
69
|
+
result.push(path);
|
|
70
|
+
}
|
|
71
|
+
return result;
|
|
72
|
+
}
|
|
73
|
+
function readSchemaId(path) {
|
|
74
|
+
try {
|
|
75
|
+
const value = parseJson(path);
|
|
76
|
+
return isRecord(value) && typeof value.$id === 'string' ? value.$id : undefined;
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
export function compileSchemaCatalog(source, manifests, options) {
|
|
83
|
+
const rootDir = resolve(options.rootDir);
|
|
84
|
+
const diagnostics = [];
|
|
85
|
+
const entries = [];
|
|
86
|
+
const ids = new Map();
|
|
87
|
+
const names = new Map();
|
|
88
|
+
const authorities = new Map();
|
|
89
|
+
const declaredPaths = new Map();
|
|
90
|
+
for (const manifest of [...manifests].sort((a, b) => a.domain.localeCompare(b.domain))) {
|
|
91
|
+
for (const artifact of [...manifest.artifacts].sort((a, b) => `${a.logicalName}\0${a.version}\0${a.id}`.localeCompare(`${b.logicalName}\0${b.version}\0${b.id}`))) {
|
|
92
|
+
diagnostics.push(...validateArtifactShape(artifact, manifest.domain));
|
|
93
|
+
const nameVersion = `${artifact.logicalName}@${artifact.version}`;
|
|
94
|
+
if (ids.has(artifact.id)) {
|
|
95
|
+
diagnostics.push(createDiagnostic(SCHEMA_DIAGNOSTIC_CODES.duplicateId, `Duplicate schema id ${artifact.id}`, { artifactId: artifact.id, details: { firstDomain: ids.get(artifact.id), secondDomain: manifest.domain } }));
|
|
96
|
+
}
|
|
97
|
+
else
|
|
98
|
+
ids.set(artifact.id, manifest.domain);
|
|
99
|
+
if (names.has(nameVersion)) {
|
|
100
|
+
diagnostics.push(createDiagnostic(SCHEMA_DIAGNOSTIC_CODES.duplicateNameVersion, `Duplicate schema name and version ${nameVersion}`, { artifactId: artifact.id }));
|
|
101
|
+
}
|
|
102
|
+
else
|
|
103
|
+
names.set(nameVersion, artifact.id);
|
|
104
|
+
let digest = artifact.digest;
|
|
105
|
+
if (artifact.authority.kind === 'canonical') {
|
|
106
|
+
const canonicalPath = artifact.authority.path;
|
|
107
|
+
if (!canonicalPath) {
|
|
108
|
+
diagnostics.push(createDiagnostic(SCHEMA_DIAGNOSTIC_CODES.invalidManifest, 'Canonical authority requires a path', { artifactId: artifact.id }));
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
const absolute = safePath(rootDir, canonicalPath);
|
|
112
|
+
if (!absolute)
|
|
113
|
+
diagnostics.push(createDiagnostic(SCHEMA_DIAGNOSTIC_CODES.pathOutsideRoot, `Authority path escapes repository root: ${canonicalPath}`, { artifactId: artifact.id, path: canonicalPath }));
|
|
114
|
+
else if (!existsSync(absolute))
|
|
115
|
+
diagnostics.push(createDiagnostic(SCHEMA_DIAGNOSTIC_CODES.missingPath, `Authority path does not exist: ${canonicalPath}`, { artifactId: artifact.id, path: canonicalPath }));
|
|
116
|
+
else {
|
|
117
|
+
const normalized = relative(rootDir, absolute).split(sep).join('/');
|
|
118
|
+
const previous = authorities.get(normalized);
|
|
119
|
+
if (previous)
|
|
120
|
+
diagnostics.push(createDiagnostic(SCHEMA_DIAGNOSTIC_CODES.duplicateAuthority, `Canonical path ${normalized} is claimed by ${previous} and ${artifact.id}`, { artifactId: artifact.id, path: normalized }));
|
|
121
|
+
else
|
|
122
|
+
authorities.set(normalized, artifact.id);
|
|
123
|
+
declaredPaths.set(normalized, { id: artifact.id, projection: false });
|
|
124
|
+
const actual = sha256(absolute);
|
|
125
|
+
if (digest && digest !== actual)
|
|
126
|
+
diagnostics.push(createDiagnostic(SCHEMA_DIAGNOSTIC_CODES.digestMismatch, `Digest mismatch for ${normalized}`, { artifactId: artifact.id, path: normalized, details: { declared: digest, actual } }));
|
|
127
|
+
digest = actual;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
for (const projection of artifact.projections ?? []) {
|
|
132
|
+
const absolute = safePath(rootDir, projection.path);
|
|
133
|
+
if (!absolute)
|
|
134
|
+
diagnostics.push(createDiagnostic(SCHEMA_DIAGNOSTIC_CODES.pathOutsideRoot, `Projection path escapes repository root: ${projection.path}`, { artifactId: artifact.id, path: projection.path }));
|
|
135
|
+
else if (!existsSync(absolute))
|
|
136
|
+
diagnostics.push(createDiagnostic(SCHEMA_DIAGNOSTIC_CODES.missingPath, `Projection path does not exist: ${projection.path}`, { artifactId: artifact.id, path: projection.path }));
|
|
137
|
+
else {
|
|
138
|
+
const normalized = relative(rootDir, absolute).split(sep).join('/');
|
|
139
|
+
declaredPaths.set(normalized, { id: artifact.id, projection: true });
|
|
140
|
+
if (projection.digest && projection.digest !== sha256(absolute))
|
|
141
|
+
diagnostics.push(createDiagnostic(SCHEMA_DIAGNOSTIC_CODES.digestMismatch, `Projection digest mismatch for ${normalized}`, { artifactId: artifact.id, path: normalized }));
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
const effectivePolicy = resolveEffectivePolicy(source.policy, manifest, artifact);
|
|
145
|
+
diagnostics.push(...diagnosePolicy(effectivePolicy, artifact.id, options.now));
|
|
146
|
+
if (effectivePolicy.requireDigest && artifact.digest === undefined) {
|
|
147
|
+
diagnostics.push(createDiagnostic(SCHEMA_DIAGNOSTIC_CODES.invalidManifest, 'Effective policy requires a declared digest', { artifactId: artifact.id }));
|
|
148
|
+
}
|
|
149
|
+
entries.push({ artifact, domain: manifest.domain, ...(digest === undefined ? {} : { digest }), effectivePolicy });
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
for (const entry of entries) {
|
|
153
|
+
for (const dependency of entry.artifact.dependencies ?? []) {
|
|
154
|
+
if (!ids.has(dependency.id))
|
|
155
|
+
diagnostics.push(createDiagnostic(SCHEMA_DIAGNOSTIC_CODES.unresolvedDependency, `Unresolved dependency ${dependency.id}`, { artifactId: entry.artifact.id, details: { dependency: dependency.id } }));
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
for (const inventoryRoot of options.inventoryRoots ?? []) {
|
|
159
|
+
const absoluteRoot = safePath(rootDir, inventoryRoot);
|
|
160
|
+
if (!absoluteRoot) {
|
|
161
|
+
diagnostics.push(createDiagnostic(SCHEMA_DIAGNOSTIC_CODES.pathOutsideRoot, `Inventory root escapes repository root: ${inventoryRoot}`, { path: inventoryRoot }));
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
for (const file of walkSchemaFiles(absoluteRoot)) {
|
|
165
|
+
const normalized = relative(rootDir, file).split(sep).join('/');
|
|
166
|
+
if (!declaredPaths.has(normalized)) {
|
|
167
|
+
const schemaId = readSchemaId(file);
|
|
168
|
+
const canonical = schemaId === undefined ? undefined : entries.find((entry) => entry.artifact.id === schemaId);
|
|
169
|
+
diagnostics.push(createDiagnostic(canonical ? SCHEMA_DIAGNOSTIC_CODES.undeclaredMirror : SCHEMA_DIAGNOSTIC_CODES.undeclaredCanonical, canonical
|
|
170
|
+
? `Schema file shares ${schemaId} but is not declared as a mirror: ${normalized}`
|
|
171
|
+
: `Schema file is not registered: ${normalized}`, { path: normalized, ...(schemaId === undefined ? {} : { details: { schemaId } }) }));
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
entries.sort((a, b) => `${a.artifact.id}\0${a.artifact.version}`.localeCompare(`${b.artifact.id}\0${b.artifact.version}`));
|
|
176
|
+
const catalog = { schemaVersion: '1', entries, domains: [...new Set(manifests.map((manifest) => manifest.domain))].sort() };
|
|
177
|
+
return { catalog, diagnostics, valid: !hasSchemaErrors(diagnostics) };
|
|
178
|
+
}
|
|
179
|
+
export function loadSchemaCatalog(options) {
|
|
180
|
+
const rootDir = resolve(options.rootDir);
|
|
181
|
+
const catalogPath = resolve(rootDir, options.catalogPath ?? 'schemas/catalog/catalog.json');
|
|
182
|
+
const diagnostics = [];
|
|
183
|
+
if (!existsSync(catalogPath)) {
|
|
184
|
+
diagnostics.push(createDiagnostic(SCHEMA_DIAGNOSTIC_CODES.missingPath, `Catalog does not exist: ${relative(rootDir, catalogPath)}`, { path: relative(rootDir, catalogPath) }));
|
|
185
|
+
return { diagnostics, valid: false };
|
|
186
|
+
}
|
|
187
|
+
let source;
|
|
188
|
+
try {
|
|
189
|
+
source = parseJson(catalogPath);
|
|
190
|
+
}
|
|
191
|
+
catch (error) {
|
|
192
|
+
diagnostics.push(createDiagnostic(SCHEMA_DIAGNOSTIC_CODES.invalidCatalog, `Cannot parse catalog: ${error instanceof Error ? error.message : String(error)}`, { path: relative(rootDir, catalogPath) }));
|
|
193
|
+
return { diagnostics, valid: false };
|
|
194
|
+
}
|
|
195
|
+
if (!isCatalogSource(source)) {
|
|
196
|
+
diagnostics.push(createDiagnostic(SCHEMA_DIAGNOSTIC_CODES.invalidCatalog, 'Catalog must use schemaVersion 1 and contain a domains array', { path: relative(rootDir, catalogPath) }));
|
|
197
|
+
return { diagnostics, valid: false };
|
|
198
|
+
}
|
|
199
|
+
const manifests = [];
|
|
200
|
+
for (const item of source.domains) {
|
|
201
|
+
let candidate = item;
|
|
202
|
+
let path;
|
|
203
|
+
if (typeof item === 'string') {
|
|
204
|
+
path = resolve(dirname(catalogPath), item);
|
|
205
|
+
if (!safePath(rootDir, relative(rootDir, path))) {
|
|
206
|
+
diagnostics.push(createDiagnostic(SCHEMA_DIAGNOSTIC_CODES.pathOutsideRoot, `Domain manifest escapes repository root: ${item}`, { path: item }));
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
if (!existsSync(path)) {
|
|
210
|
+
diagnostics.push(createDiagnostic(SCHEMA_DIAGNOSTIC_CODES.missingPath, `Domain manifest does not exist: ${item}`, { path: item }));
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
try {
|
|
214
|
+
candidate = parseJson(path);
|
|
215
|
+
}
|
|
216
|
+
catch (error) {
|
|
217
|
+
diagnostics.push(createDiagnostic(SCHEMA_DIAGNOSTIC_CODES.invalidManifest, `Cannot parse domain manifest ${item}: ${error instanceof Error ? error.message : String(error)}`, { path: item }));
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
if (!isDomainManifest(candidate)) {
|
|
222
|
+
diagnostics.push(createDiagnostic(SCHEMA_DIAGNOSTIC_CODES.invalidManifest, 'Domain manifest must use schemaVersion 1 and contain domain, owner, and artifacts', path ? { path: relative(rootDir, path) } : {}));
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
manifests.push(candidate);
|
|
226
|
+
}
|
|
227
|
+
const result = compileSchemaCatalog(source, manifests, options);
|
|
228
|
+
return { catalog: result.catalog, diagnostics: [...diagnostics, ...result.diagnostics], valid: diagnostics.length === 0 && result.valid };
|
|
229
|
+
}
|
|
230
|
+
/** Serialize compiler output without timestamps or filesystem-specific data. */
|
|
231
|
+
export function serializeCompiledSchemaCatalog(catalog) {
|
|
232
|
+
return `${JSON.stringify(catalog, null, 2)}\n`;
|
|
233
|
+
}
|
|
234
|
+
//# sourceMappingURL=catalog.js.map
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
function record(value) {
|
|
2
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value) ? value : undefined;
|
|
3
|
+
}
|
|
4
|
+
function strings(value) {
|
|
5
|
+
return new Set(Array.isArray(value) ? value.filter((item) => typeof item === 'string') : []);
|
|
6
|
+
}
|
|
7
|
+
/** Conservative backward-compatibility analysis: unknown means human review, never an optimistic pass. */
|
|
8
|
+
export function analyzeBackwardCompatibility(before, after) {
|
|
9
|
+
const left = record(before);
|
|
10
|
+
const right = record(after);
|
|
11
|
+
if (!left || !right)
|
|
12
|
+
return { status: 'unknown', direction: 'backward', reasons: ['schemas are not JSON objects'] };
|
|
13
|
+
if (JSON.stringify(left) === JSON.stringify(right))
|
|
14
|
+
return { status: 'compatible', direction: 'backward', reasons: ['schemas are identical'] };
|
|
15
|
+
const reasons = [];
|
|
16
|
+
if (left.type !== undefined && right.type !== undefined && JSON.stringify(left.type) !== JSON.stringify(right.type))
|
|
17
|
+
reasons.push('accepted root type changed');
|
|
18
|
+
const oldRequired = strings(left.required);
|
|
19
|
+
for (const field of strings(right.required))
|
|
20
|
+
if (!oldRequired.has(field))
|
|
21
|
+
reasons.push(`required property added: ${field}`);
|
|
22
|
+
const oldProperties = record(left.properties) ?? {};
|
|
23
|
+
const newProperties = record(right.properties) ?? {};
|
|
24
|
+
for (const field of Object.keys(oldProperties))
|
|
25
|
+
if (!(field in newProperties))
|
|
26
|
+
reasons.push(`property removed: ${field}`);
|
|
27
|
+
if (left.additionalProperties !== false && right.additionalProperties === false)
|
|
28
|
+
reasons.push('additional properties changed from allowed to forbidden');
|
|
29
|
+
const oldEnum = strings(left.enum);
|
|
30
|
+
const newEnum = strings(right.enum);
|
|
31
|
+
if (oldEnum.size && newEnum.size)
|
|
32
|
+
for (const member of oldEnum)
|
|
33
|
+
if (!newEnum.has(member))
|
|
34
|
+
reasons.push(`enum member removed: ${member}`);
|
|
35
|
+
return reasons.length
|
|
36
|
+
? { status: 'breaking', direction: 'backward', reasons }
|
|
37
|
+
: { status: 'unknown', direction: 'backward', reasons: ['no conservative breaking rule matched; review is required'] };
|
|
38
|
+
}
|
|
39
|
+
export function analyzeCompatibilityChain(schemas) {
|
|
40
|
+
return schemas.slice(1).map((schema, index) => analyzeBackwardCompatibility(schemas[index], schema));
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=compatibility.js.map
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export const SCHEMA_DIAGNOSTIC_CODES = {
|
|
2
|
+
invalidCatalog: 'SCHEMA_CATALOG_INVALID',
|
|
3
|
+
invalidManifest: 'SCHEMA_MANIFEST_INVALID',
|
|
4
|
+
duplicateId: 'SCHEMA_DUPLICATE_ID',
|
|
5
|
+
duplicateNameVersion: 'SCHEMA_DUPLICATE_NAME_VERSION',
|
|
6
|
+
duplicateAuthority: 'SCHEMA_DUPLICATE_AUTHORITY',
|
|
7
|
+
missingPath: 'SCHEMA_PATH_MISSING',
|
|
8
|
+
pathOutsideRoot: 'SCHEMA_PATH_OUTSIDE_ROOT',
|
|
9
|
+
digestMismatch: 'SCHEMA_DIGEST_MISMATCH',
|
|
10
|
+
unresolvedDependency: 'SCHEMA_DEPENDENCY_UNRESOLVED',
|
|
11
|
+
undeclaredCanonical: 'SCHEMA_CANONICAL_UNREGISTERED',
|
|
12
|
+
undeclaredMirror: 'SCHEMA_MIRROR_UNDECLARED',
|
|
13
|
+
expiredException: 'SCHEMA_POLICY_EXCEPTION_EXPIRED',
|
|
14
|
+
};
|
|
15
|
+
export function createDiagnostic(code, message, options = {}) {
|
|
16
|
+
return {
|
|
17
|
+
code,
|
|
18
|
+
severity: options.severity ?? 'error',
|
|
19
|
+
message,
|
|
20
|
+
...(options.artifactId === undefined ? {} : { artifactId: options.artifactId }),
|
|
21
|
+
...(options.path === undefined ? {} : { path: options.path }),
|
|
22
|
+
...(options.details === undefined ? {} : { details: options.details }),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
export function hasSchemaErrors(diagnostics) {
|
|
26
|
+
return diagnostics.some((diagnostic) => diagnostic.severity === 'error');
|
|
27
|
+
}
|
|
28
|
+
export class SchemaCatalogError extends Error {
|
|
29
|
+
diagnostics;
|
|
30
|
+
constructor(message, diagnostics) {
|
|
31
|
+
super(message);
|
|
32
|
+
this.name = 'SchemaCatalogError';
|
|
33
|
+
this.diagnostics = diagnostics;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=diagnostics.js.map
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export * from './types.js';
|
|
2
|
+
export * from './diagnostics.js';
|
|
3
|
+
export * from './policy.js';
|
|
4
|
+
export * from './catalog.js';
|
|
5
|
+
export * from './resolver.js';
|
|
6
|
+
export * from './validator.js';
|
|
7
|
+
export * from './compatibility.js';
|
|
8
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { createDiagnostic, SCHEMA_DIAGNOSTIC_CODES } from './diagnostics.js';
|
|
2
|
+
export const DEFAULT_SCHEMA_POLICY = {
|
|
3
|
+
strict: true,
|
|
4
|
+
remoteReferences: 'deny',
|
|
5
|
+
compatibility: 'unknown',
|
|
6
|
+
requireFixtures: false,
|
|
7
|
+
requireDigest: false,
|
|
8
|
+
exceptions: [],
|
|
9
|
+
};
|
|
10
|
+
export function resolveEffectivePolicy(repositoryPolicy, domain, artifact) {
|
|
11
|
+
const layers = [repositoryPolicy, domain.policy, artifact.policy];
|
|
12
|
+
const merged = {};
|
|
13
|
+
for (const layer of layers) {
|
|
14
|
+
if (layer === undefined)
|
|
15
|
+
continue;
|
|
16
|
+
if (layer.strict !== undefined)
|
|
17
|
+
merged.strict = layer.strict;
|
|
18
|
+
if (layer.remoteReferences !== undefined)
|
|
19
|
+
merged.remoteReferences = layer.remoteReferences;
|
|
20
|
+
if (layer.compatibility !== undefined)
|
|
21
|
+
merged.compatibility = layer.compatibility;
|
|
22
|
+
if (layer.requireFixtures !== undefined)
|
|
23
|
+
merged.requireFixtures = layer.requireFixtures;
|
|
24
|
+
if (layer.requireDigest !== undefined)
|
|
25
|
+
merged.requireDigest = layer.requireDigest;
|
|
26
|
+
if (layer.exceptions !== undefined)
|
|
27
|
+
merged.exceptions = [...(merged.exceptions ?? []), ...layer.exceptions];
|
|
28
|
+
}
|
|
29
|
+
if (artifact.compatibility?.mode !== undefined && artifact.policy?.compatibility === undefined) {
|
|
30
|
+
merged.compatibility = artifact.compatibility.mode;
|
|
31
|
+
}
|
|
32
|
+
return {
|
|
33
|
+
strict: merged.strict ?? DEFAULT_SCHEMA_POLICY.strict,
|
|
34
|
+
remoteReferences: merged.remoteReferences ?? DEFAULT_SCHEMA_POLICY.remoteReferences,
|
|
35
|
+
compatibility: merged.compatibility ?? DEFAULT_SCHEMA_POLICY.compatibility,
|
|
36
|
+
requireFixtures: merged.requireFixtures ?? DEFAULT_SCHEMA_POLICY.requireFixtures,
|
|
37
|
+
requireDigest: merged.requireDigest ?? DEFAULT_SCHEMA_POLICY.requireDigest,
|
|
38
|
+
exceptions: [...(merged.exceptions ?? [])].sort((left, right) => `${left.rule}\0${left.expires}`.localeCompare(`${right.rule}\0${right.expires}`)),
|
|
39
|
+
provenance: [
|
|
40
|
+
...(repositoryPolicy === undefined ? [] : [{ source: 'repository', value: repositoryPolicy }]),
|
|
41
|
+
...(domain.policy === undefined
|
|
42
|
+
? []
|
|
43
|
+
: [{ source: 'domain', domain: domain.domain, value: domain.policy }]),
|
|
44
|
+
...(artifact.policy === undefined ? [] : [{ source: 'artifact', value: artifact.policy }]),
|
|
45
|
+
],
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
export function diagnosePolicy(policy, artifactId, now = new Date()) {
|
|
49
|
+
const diagnostics = [];
|
|
50
|
+
for (const exception of policy.exceptions) {
|
|
51
|
+
const expires = new Date(exception.expires);
|
|
52
|
+
if (Number.isNaN(expires.getTime()) || expires.getTime() <= now.getTime()) {
|
|
53
|
+
diagnostics.push(createDiagnostic(SCHEMA_DIAGNOSTIC_CODES.expiredException, `Policy exception ${exception.rule} expired on ${exception.expires}`, { artifactId, details: { rule: exception.rule, owner: exception.owner, expires: exception.expires } }));
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return diagnostics;
|
|
57
|
+
}
|
|
58
|
+
//# sourceMappingURL=policy.js.map
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { relative, resolve, sep } from 'node:path';
|
|
2
|
+
export class SchemaResolver {
|
|
3
|
+
catalog;
|
|
4
|
+
rootDir;
|
|
5
|
+
index = new Map();
|
|
6
|
+
constructor(catalog, options = {}) {
|
|
7
|
+
this.catalog = catalog;
|
|
8
|
+
this.rootDir = options.rootDir === undefined ? undefined : resolve(options.rootDir);
|
|
9
|
+
for (const entry of catalog.entries) {
|
|
10
|
+
this.index.set(entry.artifact.id, entry);
|
|
11
|
+
this.index.set(`${entry.artifact.logicalName}@${entry.artifact.version}`, entry);
|
|
12
|
+
// The unqualified logical name resolves to the last deterministically
|
|
13
|
+
// ordered revision. Callers requiring reproducibility should use id or
|
|
14
|
+
// name@version.
|
|
15
|
+
this.index.set(entry.artifact.logicalName, entry);
|
|
16
|
+
for (const alias of entry.artifact.aliases ?? [])
|
|
17
|
+
this.index.set(alias, entry);
|
|
18
|
+
if (entry.artifact.authority.path) {
|
|
19
|
+
const normalized = entry.artifact.authority.path.split('\\').join('/');
|
|
20
|
+
this.index.set(normalized, entry);
|
|
21
|
+
if (this.rootDir)
|
|
22
|
+
this.index.set(relative(this.rootDir, resolve(this.rootDir, normalized)).split(sep).join('/'), entry);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
list(options = {}) {
|
|
27
|
+
return this.catalog.entries.filter((entry) => (options.domain === undefined || entry.domain === options.domain) &&
|
|
28
|
+
(options.lifecycle === undefined || entry.artifact.lifecycle === options.lifecycle) &&
|
|
29
|
+
(options.format === undefined || entry.artifact.format === options.format));
|
|
30
|
+
}
|
|
31
|
+
resolve(query) {
|
|
32
|
+
return this.index.get(query) ?? this.index.get(query.split('\\').join('/'));
|
|
33
|
+
}
|
|
34
|
+
require(query) {
|
|
35
|
+
const entry = this.resolve(query);
|
|
36
|
+
if (!entry)
|
|
37
|
+
throw new Error(`Unknown schema: ${query}`);
|
|
38
|
+
return entry;
|
|
39
|
+
}
|
|
40
|
+
policy(query) {
|
|
41
|
+
return this.require(query).effectivePolicy;
|
|
42
|
+
}
|
|
43
|
+
graph(query, options = {}) {
|
|
44
|
+
const direction = options.direction ?? 'both';
|
|
45
|
+
const allEdges = [];
|
|
46
|
+
for (const entry of this.catalog.entries) {
|
|
47
|
+
for (const dependency of entry.artifact.dependencies ?? [])
|
|
48
|
+
allEdges.push({ from: entry.artifact.id, to: dependency.id, kind: 'depends-on' });
|
|
49
|
+
for (const superseded of entry.artifact.supersedes ?? [])
|
|
50
|
+
allEdges.push({ from: entry.artifact.id, to: superseded, kind: 'supersedes' });
|
|
51
|
+
}
|
|
52
|
+
const selected = query === undefined ? new Set(this.catalog.entries.map((entry) => entry.artifact.id)) : this.collectConnected(this.require(query).artifact.id, allEdges, direction);
|
|
53
|
+
return {
|
|
54
|
+
nodes: this.catalog.entries.filter((entry) => selected.has(entry.artifact.id)).map((entry) => ({ id: entry.artifact.id, logicalName: entry.artifact.logicalName, version: entry.artifact.version, domain: entry.domain })).sort((a, b) => a.id.localeCompare(b.id)),
|
|
55
|
+
edges: allEdges.filter((edge) => selected.has(edge.from) && selected.has(edge.to)).sort((a, b) => `${a.from}\0${a.to}\0${a.kind}`.localeCompare(`${b.from}\0${b.to}\0${b.kind}`)),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
collectConnected(seed, edges, direction) {
|
|
59
|
+
const result = new Set([seed]);
|
|
60
|
+
const queue = [seed];
|
|
61
|
+
while (queue.length) {
|
|
62
|
+
const current = queue.shift();
|
|
63
|
+
if (!current)
|
|
64
|
+
continue;
|
|
65
|
+
for (const edge of edges) {
|
|
66
|
+
const next = edge.from === current && direction !== 'dependents' ? edge.to : edge.to === current && direction !== 'dependencies' ? edge.from : undefined;
|
|
67
|
+
if (next && !result.has(next)) {
|
|
68
|
+
result.add(next);
|
|
69
|
+
queue.push(next);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return result;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
//# sourceMappingURL=resolver.js.map
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
import Ajv from 'ajv';
|
|
4
|
+
import Ajv2020 from 'ajv/dist/2020.js';
|
|
5
|
+
import addFormats from 'ajv-formats';
|
|
6
|
+
function depth(value, seen = new Set()) {
|
|
7
|
+
if (typeof value !== 'object' || value === null)
|
|
8
|
+
return 0;
|
|
9
|
+
if (seen.has(value))
|
|
10
|
+
return 0;
|
|
11
|
+
seen.add(value);
|
|
12
|
+
const children = Array.isArray(value) ? value : Object.values(value);
|
|
13
|
+
return children.length === 0 ? 1 : 1 + Math.max(...children.map(item => depth(item, seen)));
|
|
14
|
+
}
|
|
15
|
+
function errorDiagnostic(artifactId, error) {
|
|
16
|
+
return {
|
|
17
|
+
code: 'SCHEMA_INSTANCE_INVALID',
|
|
18
|
+
severity: 'error',
|
|
19
|
+
message: error.message ?? 'validation failed',
|
|
20
|
+
artifactId,
|
|
21
|
+
path: error.instancePath,
|
|
22
|
+
details: { schemaPath: error.schemaPath, keyword: error.keyword, params: error.params },
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
/** Offline, catalog-backed validator with bounded inputs and compiled-schema caching. */
|
|
26
|
+
export class SchemaValidator {
|
|
27
|
+
resolver;
|
|
28
|
+
options;
|
|
29
|
+
cache = new Map();
|
|
30
|
+
constructor(resolver, options) {
|
|
31
|
+
this.resolver = resolver;
|
|
32
|
+
this.options = options;
|
|
33
|
+
}
|
|
34
|
+
compile(query) {
|
|
35
|
+
const entry = this.resolver.require(query);
|
|
36
|
+
if (entry.artifact.format !== 'json-schema') {
|
|
37
|
+
throw new Error(`Validation requires a JSON Schema artifact; got ${entry.artifact.format}`);
|
|
38
|
+
}
|
|
39
|
+
const key = `${entry.artifact.id}\0${entry.digest ?? entry.artifact.version}`;
|
|
40
|
+
const cached = this.cache.get(key);
|
|
41
|
+
if (cached)
|
|
42
|
+
return cached;
|
|
43
|
+
const draft2020 = entry.artifact.dialect?.includes('2020-12') ?? false;
|
|
44
|
+
const ajv = draft2020
|
|
45
|
+
? new Ajv2020({ strict: entry.effectivePolicy.strict, allErrors: true, loadSchema: undefined })
|
|
46
|
+
: new Ajv({ strict: entry.effectivePolicy.strict, allErrors: true, loadSchema: undefined });
|
|
47
|
+
addFormats(ajv);
|
|
48
|
+
for (const candidate of this.resolver.list())
|
|
49
|
+
this.addCompatibleSchema(ajv, candidate, draft2020);
|
|
50
|
+
const compiled = ajv.getSchema(entry.artifact.id);
|
|
51
|
+
if (!compiled)
|
|
52
|
+
throw new Error(`Could not compile schema ${entry.artifact.id}`);
|
|
53
|
+
this.cache.set(key, compiled);
|
|
54
|
+
return compiled;
|
|
55
|
+
}
|
|
56
|
+
validate(query, instance) {
|
|
57
|
+
const entry = this.resolver.require(query);
|
|
58
|
+
const diagnostics = [];
|
|
59
|
+
const serialized = JSON.stringify(instance);
|
|
60
|
+
const maxBytes = this.options.limits?.maxBytes ?? 10 * 1024 * 1024;
|
|
61
|
+
const maxDepth = this.options.limits?.maxDepth ?? 128;
|
|
62
|
+
if (Buffer.byteLength(serialized) > maxBytes)
|
|
63
|
+
diagnostics.push({ code: 'SCHEMA_RESOURCE_LIMIT', severity: 'error', message: `Instance exceeds ${maxBytes} byte limit`, artifactId: entry.artifact.id });
|
|
64
|
+
if (depth(instance) > maxDepth)
|
|
65
|
+
diagnostics.push({ code: 'SCHEMA_RESOURCE_LIMIT', severity: 'error', message: `Instance exceeds depth limit ${maxDepth}`, artifactId: entry.artifact.id });
|
|
66
|
+
if (diagnostics.length)
|
|
67
|
+
return { valid: false, artifactId: entry.artifact.id, diagnostics };
|
|
68
|
+
const check = this.compile(query);
|
|
69
|
+
const valid = check(instance);
|
|
70
|
+
return { valid, artifactId: entry.artifact.id, diagnostics: (check.errors ?? []).map(error => errorDiagnostic(entry.artifact.id, error)) };
|
|
71
|
+
}
|
|
72
|
+
addCompatibleSchema(ajv, entry, draft2020) {
|
|
73
|
+
if (entry.artifact.format !== 'json-schema' || !entry.artifact.authority.path)
|
|
74
|
+
return;
|
|
75
|
+
if ((entry.artifact.dialect?.includes('2020-12') ?? false) !== draft2020)
|
|
76
|
+
return;
|
|
77
|
+
const path = resolve(this.options.rootDir, entry.artifact.authority.path);
|
|
78
|
+
const schema = JSON.parse(readFileSync(path, 'utf8'));
|
|
79
|
+
ajv.addSchema(schema, entry.artifact.id);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
//# sourceMappingURL=validator.js.map
|