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/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
- }