archctx-contracts 0.2.2 → 0.3.0
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/fixtures/boundary/explorer-projection-v2-budget.json +87 -0
- package/fixtures/invalid/architecture-snapshot-unknown-mode.json +22 -4
- package/fixtures/invalid/explorer-projection-query-v2-caller-scope.json +7 -0
- package/fixtures/invalid/explorer-projection-v2-derived-subject.json +101 -0
- package/fixtures/valid/architecture-snapshot.json +22 -4
- package/fixtures/valid/explorer-delta-query.json +11 -0
- package/fixtures/valid/explorer-projection-delta.json +32 -0
- package/fixtures/valid/explorer-projection-query-v2.json +6 -0
- package/fixtures/valid/explorer-projection-v2.json +138 -0
- package/fixtures/valid/product-version-manifest.json +7 -7
- package/package.json +1 -1
- package/schemas/runtime/architecture-event.schema.json +172 -2
- package/schemas/runtime/architecture-snapshot.schema.json +37 -4
- package/schemas/runtime/explorer-delta-query.schema.json +24 -0
- package/schemas/runtime/explorer-projection-delta.schema.json +79 -0
- package/schemas/runtime/explorer-projection-query-v2.schema.json +42 -0
- package/schemas/runtime/explorer-projection-v2.schema.json +538 -0
- package/src/ledger.ts +125 -3
- package/src/ports.ts +399 -32
- package/src/product-version.ts +5 -5
- package/src/schema.ts +3 -2
- package/src/validator.ts +52 -5
- package/fixtures/invalid/explorer-projection-write-field.json +0 -21
- package/fixtures/valid/explorer-projection.json +0 -53
- package/schemas/runtime/explorer-projection.schema.json +0 -92
package/src/validator.ts
CHANGED
|
@@ -11,6 +11,8 @@ export interface ValidationResult {
|
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
type JsonSchema = {
|
|
14
|
+
$ref?: string;
|
|
15
|
+
$defs?: Record<string, JsonSchema>;
|
|
14
16
|
type?: string | string[];
|
|
15
17
|
const?: Json;
|
|
16
18
|
enum?: Json[];
|
|
@@ -19,23 +21,37 @@ type JsonSchema = {
|
|
|
19
21
|
properties?: Record<string, JsonSchema>;
|
|
20
22
|
items?: JsonSchema;
|
|
21
23
|
oneOf?: JsonSchema[];
|
|
24
|
+
anyOf?: JsonSchema[];
|
|
25
|
+
allOf?: JsonSchema[];
|
|
26
|
+
not?: JsonSchema;
|
|
22
27
|
additionalProperties?: boolean | JsonSchema;
|
|
23
28
|
minItems?: number;
|
|
29
|
+
minLength?: number;
|
|
30
|
+
maxLength?: number;
|
|
24
31
|
minimum?: number;
|
|
25
32
|
maximum?: number;
|
|
26
33
|
};
|
|
27
34
|
|
|
28
35
|
export function validateJsonSchema(schema: JsonSchema, value: Json): ValidationResult {
|
|
29
36
|
const issues: ValidationIssue[] = [];
|
|
30
|
-
visit(schema, value, "$", issues);
|
|
37
|
+
visit(schema, value, "$", issues, schema);
|
|
31
38
|
return { valid: issues.length === 0, issues };
|
|
32
39
|
}
|
|
33
40
|
|
|
34
|
-
function visit(schema: JsonSchema, value: Json, path: string, issues: ValidationIssue[]): void {
|
|
41
|
+
function visit(schema: JsonSchema, value: Json, path: string, issues: ValidationIssue[], root: JsonSchema): void {
|
|
42
|
+
if (schema.$ref) {
|
|
43
|
+
if (!schema.$ref.startsWith("#/")) return;
|
|
44
|
+
const resolved = resolveLocalRef(root, schema.$ref);
|
|
45
|
+
if (!resolved) {
|
|
46
|
+
issues.push({ path, message: `unresolved schema reference ${schema.$ref}` });
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
visit(resolved, value, path, issues, root);
|
|
50
|
+
}
|
|
35
51
|
if (schema.oneOf) {
|
|
36
52
|
const matched = schema.oneOf.filter((candidate) => {
|
|
37
53
|
const candidateIssues: ValidationIssue[] = [];
|
|
38
|
-
visit(candidate, value, path, candidateIssues);
|
|
54
|
+
visit(candidate, value, path, candidateIssues, root);
|
|
39
55
|
return candidateIssues.length === 0;
|
|
40
56
|
}).length;
|
|
41
57
|
if (matched !== 1) {
|
|
@@ -43,6 +59,20 @@ function visit(schema: JsonSchema, value: Json, path: string, issues: Validation
|
|
|
43
59
|
return;
|
|
44
60
|
}
|
|
45
61
|
}
|
|
62
|
+
if (schema.anyOf) {
|
|
63
|
+
const matched = schema.anyOf.some((candidate) => {
|
|
64
|
+
const candidateIssues: ValidationIssue[] = [];
|
|
65
|
+
visit(candidate, value, path, candidateIssues, root);
|
|
66
|
+
return candidateIssues.length === 0;
|
|
67
|
+
});
|
|
68
|
+
if (!matched) issues.push({ path, message: "expected at least one matching schema" });
|
|
69
|
+
}
|
|
70
|
+
for (const candidate of schema.allOf ?? []) visit(candidate, value, path, issues, root);
|
|
71
|
+
if (schema.not) {
|
|
72
|
+
const candidateIssues: ValidationIssue[] = [];
|
|
73
|
+
visit(schema.not, value, path, candidateIssues, root);
|
|
74
|
+
if (candidateIssues.length === 0) issues.push({ path, message: "matched forbidden schema" });
|
|
75
|
+
}
|
|
46
76
|
if (schema.const !== undefined && JSON.stringify(value) !== JSON.stringify(schema.const)) {
|
|
47
77
|
issues.push({ path, message: `expected const ${JSON.stringify(schema.const)}` });
|
|
48
78
|
return;
|
|
@@ -57,6 +87,12 @@ function visit(schema: JsonSchema, value: Json, path: string, issues: Validation
|
|
|
57
87
|
if (typeof value === "string" && schema.pattern && !new RegExp(schema.pattern).test(value)) {
|
|
58
88
|
issues.push({ path, message: `does not match ${schema.pattern}` });
|
|
59
89
|
}
|
|
90
|
+
if (typeof value === "string" && schema.minLength !== undefined && value.length < schema.minLength) {
|
|
91
|
+
issues.push({ path, message: `shorter than minimum length ${schema.minLength}` });
|
|
92
|
+
}
|
|
93
|
+
if (typeof value === "string" && schema.maxLength !== undefined && value.length > schema.maxLength) {
|
|
94
|
+
issues.push({ path, message: `longer than maximum length ${schema.maxLength}` });
|
|
95
|
+
}
|
|
60
96
|
if (typeof value === "number") {
|
|
61
97
|
if (schema.minimum !== undefined && value < schema.minimum) issues.push({ path, message: `below minimum ${schema.minimum}` });
|
|
62
98
|
if (schema.maximum !== undefined && value > schema.maximum) issues.push({ path, message: `above maximum ${schema.maximum}` });
|
|
@@ -65,7 +101,7 @@ function visit(schema: JsonSchema, value: Json, path: string, issues: Validation
|
|
|
65
101
|
if (schema.minItems !== undefined && value.length < schema.minItems) {
|
|
66
102
|
issues.push({ path, message: `expected at least ${schema.minItems} items` });
|
|
67
103
|
}
|
|
68
|
-
if (schema.items) value.forEach((item, index) => visit(schema.items!, item, `${path}[${index}]`, issues));
|
|
104
|
+
if (schema.items) value.forEach((item, index) => visit(schema.items!, item, `${path}[${index}]`, issues, root));
|
|
69
105
|
}
|
|
70
106
|
if (value !== null && typeof value === "object" && !Array.isArray(value)) {
|
|
71
107
|
const objectValue = value as Record<string, Json>;
|
|
@@ -73,7 +109,7 @@ function visit(schema: JsonSchema, value: Json, path: string, issues: Validation
|
|
|
73
109
|
if (!(key in objectValue)) issues.push({ path: `${path}.${key}`, message: "required" });
|
|
74
110
|
}
|
|
75
111
|
for (const [key, child] of Object.entries(schema.properties ?? {})) {
|
|
76
|
-
if (key in objectValue) visit(child, objectValue[key], `${path}.${key}`, issues);
|
|
112
|
+
if (key in objectValue) visit(child, objectValue[key], `${path}.${key}`, issues, root);
|
|
77
113
|
}
|
|
78
114
|
if (schema.additionalProperties === false && schema.properties) {
|
|
79
115
|
for (const key of Object.keys(objectValue)) {
|
|
@@ -83,6 +119,17 @@ function visit(schema: JsonSchema, value: Json, path: string, issues: Validation
|
|
|
83
119
|
}
|
|
84
120
|
}
|
|
85
121
|
|
|
122
|
+
function resolveLocalRef(root: JsonSchema, ref: string): JsonSchema | undefined {
|
|
123
|
+
if (!ref.startsWith("#/")) return undefined;
|
|
124
|
+
let current: unknown = root;
|
|
125
|
+
for (const rawSegment of ref.slice(2).split("/")) {
|
|
126
|
+
const segment = rawSegment.replace(/~1/g, "/").replace(/~0/g, "~");
|
|
127
|
+
if (!current || typeof current !== "object" || Array.isArray(current) || !(segment in current)) return undefined;
|
|
128
|
+
current = (current as Record<string, unknown>)[segment];
|
|
129
|
+
}
|
|
130
|
+
return current && typeof current === "object" && !Array.isArray(current) ? current as JsonSchema : undefined;
|
|
131
|
+
}
|
|
132
|
+
|
|
86
133
|
function matchesType(type: string | string[], value: Json): boolean {
|
|
87
134
|
const allowed = Array.isArray(type) ? type : [type];
|
|
88
135
|
return allowed.some((candidate) => {
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"schemaVersion": "archcontext.explorer-projection/v1",
|
|
3
|
-
"generatedAt": "2026-06-20T00:00:00.000Z",
|
|
4
|
-
"repository": {
|
|
5
|
-
"repositoryId": "repo.local",
|
|
6
|
-
"headSha": "abc123",
|
|
7
|
-
"worktreeDigest": "sha256:1111111111111111111111111111111111111111111111111111111111111111"
|
|
8
|
-
},
|
|
9
|
-
"nodes": [],
|
|
10
|
-
"relations": [],
|
|
11
|
-
"verification": [],
|
|
12
|
-
"pressure": [],
|
|
13
|
-
"interventions": [],
|
|
14
|
-
"capabilities": {
|
|
15
|
-
"readOnly": true,
|
|
16
|
-
"mutationMode": "forbidden",
|
|
17
|
-
"egress": "none",
|
|
18
|
-
"tokenRequired": true
|
|
19
|
-
},
|
|
20
|
-
"mutationEndpoint": "/changesets/apply"
|
|
21
|
-
}
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"schemaVersion": "archcontext.explorer-projection/v1",
|
|
3
|
-
"generatedAt": "2026-06-20T00:00:00.000Z",
|
|
4
|
-
"repository": {
|
|
5
|
-
"repositoryId": "repo.local",
|
|
6
|
-
"headSha": "abc123",
|
|
7
|
-
"worktreeDigest": "sha256:1111111111111111111111111111111111111111111111111111111111111111",
|
|
8
|
-
"modelDigest": "sha256:2222222222222222222222222222222222222222222222222222222222222222"
|
|
9
|
-
},
|
|
10
|
-
"nodes": [
|
|
11
|
-
{
|
|
12
|
-
"id": "module.runtime-daemon",
|
|
13
|
-
"name": "Runtime Daemon",
|
|
14
|
-
"kind": "module",
|
|
15
|
-
"repositoryId": "repo.local",
|
|
16
|
-
"verificationStatus": "MATCHED",
|
|
17
|
-
"pressure": {
|
|
18
|
-
"level": "low",
|
|
19
|
-
"score": 12,
|
|
20
|
-
"signals": []
|
|
21
|
-
},
|
|
22
|
-
"sourceSelectors": [
|
|
23
|
-
{
|
|
24
|
-
"path": "packages/local-runtime/runtime-daemon/src/index.ts",
|
|
25
|
-
"symbolId": "ArchctxDaemon",
|
|
26
|
-
"startLine": 41,
|
|
27
|
-
"endLine": 300
|
|
28
|
-
}
|
|
29
|
-
]
|
|
30
|
-
}
|
|
31
|
-
],
|
|
32
|
-
"relations": [
|
|
33
|
-
{
|
|
34
|
-
"id": "relation.cli-runtime",
|
|
35
|
-
"source": "module.cli",
|
|
36
|
-
"target": "module.runtime-daemon",
|
|
37
|
-
"kind": "uses",
|
|
38
|
-
"verificationStatus": "MATCHED"
|
|
39
|
-
}
|
|
40
|
-
],
|
|
41
|
-
"landscape": {
|
|
42
|
-
"repositories": ["repo.local"]
|
|
43
|
-
},
|
|
44
|
-
"verification": [],
|
|
45
|
-
"pressure": [],
|
|
46
|
-
"interventions": [],
|
|
47
|
-
"capabilities": {
|
|
48
|
-
"readOnly": true,
|
|
49
|
-
"mutationMode": "forbidden",
|
|
50
|
-
"egress": "none",
|
|
51
|
-
"tokenRequired": true
|
|
52
|
-
}
|
|
53
|
-
}
|
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
-
"$id": "https://archctx.repoharness.com/schemas/runtime/explorer-projection.schema.json",
|
|
4
|
-
"title": "ExplorerProjection",
|
|
5
|
-
"type": "object",
|
|
6
|
-
"additionalProperties": false,
|
|
7
|
-
"required": ["schemaVersion", "generatedAt", "repository", "nodes", "relations", "verification", "pressure", "interventions", "capabilities"],
|
|
8
|
-
"properties": {
|
|
9
|
-
"schemaVersion": { "const": "archcontext.explorer-projection/v1" },
|
|
10
|
-
"generatedAt": { "type": "string" },
|
|
11
|
-
"repository": {
|
|
12
|
-
"type": "object",
|
|
13
|
-
"additionalProperties": false,
|
|
14
|
-
"required": ["repositoryId", "headSha", "worktreeDigest"],
|
|
15
|
-
"properties": {
|
|
16
|
-
"repositoryId": { "type": "string" },
|
|
17
|
-
"headSha": { "type": "string" },
|
|
18
|
-
"worktreeDigest": { "type": "string" },
|
|
19
|
-
"modelDigest": { "type": "string" }
|
|
20
|
-
}
|
|
21
|
-
},
|
|
22
|
-
"nodes": {
|
|
23
|
-
"type": "array",
|
|
24
|
-
"items": {
|
|
25
|
-
"type": "object",
|
|
26
|
-
"additionalProperties": false,
|
|
27
|
-
"required": ["id", "name", "kind", "verificationStatus", "pressure", "sourceSelectors"],
|
|
28
|
-
"properties": {
|
|
29
|
-
"id": { "type": "string" },
|
|
30
|
-
"name": { "type": "string" },
|
|
31
|
-
"kind": { "type": "string" },
|
|
32
|
-
"repositoryId": { "type": "string" },
|
|
33
|
-
"verificationStatus": { "type": "string", "enum": ["MATCHED", "DRIFT", "UNKNOWN", "VERIFIED"] },
|
|
34
|
-
"pressure": {
|
|
35
|
-
"type": "object",
|
|
36
|
-
"additionalProperties": false,
|
|
37
|
-
"required": ["level", "score", "signals"],
|
|
38
|
-
"properties": {
|
|
39
|
-
"level": { "type": "string", "enum": ["low", "medium", "high"] },
|
|
40
|
-
"score": { "type": "number", "minimum": 0, "maximum": 100 },
|
|
41
|
-
"signals": { "type": "array", "items": { "type": "string" } }
|
|
42
|
-
}
|
|
43
|
-
},
|
|
44
|
-
"sourceSelectors": {
|
|
45
|
-
"type": "array",
|
|
46
|
-
"items": {
|
|
47
|
-
"type": "object",
|
|
48
|
-
"additionalProperties": false,
|
|
49
|
-
"required": ["path"],
|
|
50
|
-
"properties": {
|
|
51
|
-
"path": { "type": "string" },
|
|
52
|
-
"symbolId": { "type": "string" },
|
|
53
|
-
"startLine": { "type": "integer", "minimum": 1 },
|
|
54
|
-
"endLine": { "type": "integer", "minimum": 1 }
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
},
|
|
61
|
-
"relations": {
|
|
62
|
-
"type": "array",
|
|
63
|
-
"items": {
|
|
64
|
-
"type": "object",
|
|
65
|
-
"additionalProperties": false,
|
|
66
|
-
"required": ["id", "source", "target", "kind", "verificationStatus"],
|
|
67
|
-
"properties": {
|
|
68
|
-
"id": { "type": "string" },
|
|
69
|
-
"source": { "type": "string" },
|
|
70
|
-
"target": { "type": "string" },
|
|
71
|
-
"kind": { "type": "string" },
|
|
72
|
-
"verificationStatus": { "type": "string", "enum": ["MATCHED", "DRIFT", "UNKNOWN", "VERIFIED"] }
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
},
|
|
76
|
-
"landscape": { "type": "object", "additionalProperties": true },
|
|
77
|
-
"verification": { "type": "array", "items": { "type": "object", "additionalProperties": true } },
|
|
78
|
-
"pressure": { "type": "array", "items": { "type": "object", "additionalProperties": true } },
|
|
79
|
-
"interventions": { "type": "array", "items": { "type": "object", "additionalProperties": true } },
|
|
80
|
-
"capabilities": {
|
|
81
|
-
"type": "object",
|
|
82
|
-
"additionalProperties": false,
|
|
83
|
-
"required": ["readOnly", "mutationMode", "egress", "tokenRequired"],
|
|
84
|
-
"properties": {
|
|
85
|
-
"readOnly": { "const": true },
|
|
86
|
-
"mutationMode": { "type": "string", "enum": ["forbidden"] },
|
|
87
|
-
"egress": { "type": "string", "enum": ["none"] },
|
|
88
|
-
"tokenRequired": { "type": "boolean" }
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
}
|