archctx-contracts 0.2.2 → 0.4.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.
Files changed (43) hide show
  1. package/fixtures/boundary/architecture-node-extension.json +1 -1
  2. package/fixtures/boundary/explorer-projection-v2-budget.json +87 -0
  3. package/fixtures/invalid/architecture-snapshot-unknown-mode.json +22 -4
  4. package/fixtures/invalid/explorer-projection-query-v2-caller-scope.json +7 -0
  5. package/fixtures/invalid/explorer-projection-v2-derived-subject.json +101 -0
  6. package/fixtures/invalid/node-unknown-field.json +1 -1
  7. package/fixtures/valid/archctx-capabilities.json +14 -0
  8. package/fixtures/valid/architecture-flow.json +53 -0
  9. package/fixtures/valid/architecture-node.json +19 -2
  10. package/fixtures/valid/architecture-refresh-signal.json +35 -0
  11. package/fixtures/valid/architecture-snapshot.json +22 -4
  12. package/fixtures/valid/explorer-delta-query.json +11 -0
  13. package/fixtures/valid/explorer-projection-delta.json +32 -0
  14. package/fixtures/valid/explorer-projection-query-v2.json +6 -0
  15. package/fixtures/valid/explorer-projection-v2.json +138 -0
  16. package/fixtures/valid/product-version-manifest.json +7 -7
  17. package/fixtures/valid/projection-request.json +14 -0
  18. package/fixtures/valid/projection-result.json +47 -0
  19. package/package.json +1 -1
  20. package/schemas/repo/architecture-flow.schema.json +97 -0
  21. package/schemas/repo/architecture-node.schema.json +39 -2
  22. package/schemas/runtime/archctx-capabilities.schema.json +45 -0
  23. package/schemas/runtime/architecture-event.schema.json +172 -2
  24. package/schemas/runtime/architecture-refresh-signal.schema.json +88 -0
  25. package/schemas/runtime/architecture-snapshot.schema.json +37 -4
  26. package/schemas/runtime/changeset.schema.json +1 -1
  27. package/schemas/runtime/explorer-delta-query.schema.json +24 -0
  28. package/schemas/runtime/explorer-projection-delta.schema.json +79 -0
  29. package/schemas/runtime/explorer-projection-query-v2.schema.json +42 -0
  30. package/schemas/runtime/explorer-projection-v2.schema.json +538 -0
  31. package/schemas/runtime/projection-request.schema.json +61 -0
  32. package/schemas/runtime/projection-result.schema.json +162 -0
  33. package/src/architecture.ts +73 -0
  34. package/src/index.ts +2 -0
  35. package/src/ledger.ts +125 -3
  36. package/src/ports.ts +399 -32
  37. package/src/product-version.ts +5 -5
  38. package/src/projection.ts +301 -0
  39. package/src/schema.ts +3 -2
  40. package/src/validator.ts +70 -6
  41. package/fixtures/invalid/explorer-projection-write-field.json +0 -21
  42. package/fixtures/valid/explorer-projection.json +0 -53
  43. package/schemas/runtime/explorer-projection.schema.json +0 -92
@@ -0,0 +1,301 @@
1
+ import { digestJson, type Json } from "./schema";
2
+
3
+ export const PROJECTION_REQUEST_SCHEMA_VERSION = "archcontext.projection-request/v1" as const;
4
+ export const PROJECTION_RESULT_SCHEMA_VERSION = "archcontext.projection-result/v1" as const;
5
+ export const ARCHITECTURE_REFRESH_SIGNAL_SCHEMA_VERSION = "archcontext.architecture-refresh-signal/v1" as const;
6
+ export const ARCHCTX_CAPABILITIES_SCHEMA_VERSION = "archcontext.capabilities/v1" as const;
7
+ export const ARCHITECTURE_DOCS_RENDERER_VERSION = "archcontext.docs-renderer/v2" as const;
8
+ export const AGENT_CONTEXT_RENDERER_VERSION = "archcontext.agent-context-renderer/v1" as const;
9
+
10
+ export const PROJECTION_MODES = ["check", "plan", "apply", "adopt"] as const;
11
+ export const PROJECTION_TARGETS = ["agent-context", "architecture-docs"] as const;
12
+ export const PROJECTION_RESULT_STATUSES = [
13
+ "adoption-required",
14
+ "applied",
15
+ "blocked",
16
+ "human-action-required",
17
+ "noop",
18
+ "permanent-failure",
19
+ "planned",
20
+ "retryable-failure"
21
+ ] as const;
22
+ export const PROJECTION_HUMAN_ACTION_REASON_CODES = [
23
+ "adoption-required",
24
+ "manual-region-conflict",
25
+ "target-collision",
26
+ "unprovable-required-flow",
27
+ "unresolved-major-change"
28
+ ] as const;
29
+ export const ARCHITECTURE_MAJOR_CHANGE_REASON_CODES = [
30
+ "constraint-changed",
31
+ "entrypoint-changed",
32
+ "interface-changed",
33
+ "lifecycle-changed",
34
+ "node-added",
35
+ "node-moved",
36
+ "node-removed",
37
+ "node-renamed",
38
+ "ownership-changed",
39
+ "relation-changed",
40
+ "responsibility-changed",
41
+ "risk-boundary-changed",
42
+ "verified-flow-proof-changed"
43
+ ] as const;
44
+ export const ARCHITECTURE_REFRESH_TARGETS = [
45
+ "architecture-contract-context",
46
+ "architecture-readiness",
47
+ "architecture-request-index",
48
+ "capability-context",
49
+ "capability-index"
50
+ ] as const;
51
+ export const ARCHCTX_FEATURES = [
52
+ "architecture-docs-renderer-v2",
53
+ "architecture-refresh-signal-v1",
54
+ "projection-protocol-v1"
55
+ ] as const;
56
+
57
+ export type ProjectionMode = (typeof PROJECTION_MODES)[number];
58
+ export type ProjectionTarget = (typeof PROJECTION_TARGETS)[number];
59
+ export type ProjectionResultStatus = (typeof PROJECTION_RESULT_STATUSES)[number];
60
+ export type ProjectionHumanActionReasonCode = (typeof PROJECTION_HUMAN_ACTION_REASON_CODES)[number];
61
+ export type ArchitectureMajorChangeReasonCode = (typeof ARCHITECTURE_MAJOR_CHANGE_REASON_CODES)[number];
62
+ export type ArchitectureRefreshTarget = (typeof ARCHITECTURE_REFRESH_TARGETS)[number];
63
+ export type ArchctxFeature = (typeof ARCHCTX_FEATURES)[number];
64
+ export type Sha256Digest = `sha256:${string}`;
65
+
66
+ export interface ProjectionExpectedSnapshotV1 {
67
+ repositoryId: string;
68
+ workspaceId: string;
69
+ headSha: string;
70
+ worktreeDigest: Sha256Digest;
71
+ }
72
+
73
+ export interface ProjectionRequestV1 {
74
+ schemaVersion: typeof PROJECTION_REQUEST_SCHEMA_VERSION;
75
+ requestId: string;
76
+ profile: "repo-harness/v1";
77
+ mode: ProjectionMode;
78
+ targets: ProjectionTarget[];
79
+ changedPaths: string[];
80
+ expected: ProjectionExpectedSnapshotV1;
81
+ adoptionPlanId?: string;
82
+ }
83
+
84
+ export interface ProjectionSnapshotV1 extends ProjectionExpectedSnapshotV1 {
85
+ baseHeadSha: string;
86
+ sourceTreeDigest: Sha256Digest;
87
+ modelDigest: Sha256Digest;
88
+ codeGraphDigest: Sha256Digest;
89
+ indexedWorktreeDigest: Sha256Digest | null;
90
+ projectionInputDigest: Sha256Digest;
91
+ rendererVersion: typeof ARCHITECTURE_DOCS_RENDERER_VERSION;
92
+ layoutVersion: "archcontext.docs-layout/v1";
93
+ generatedFrom: {
94
+ codeGraphPackage: "@colbymchenry/codegraph";
95
+ codeGraphVersion: "1.5.0";
96
+ codeGraphBinaryDigest: Sha256Digest;
97
+ codeGraphStatus: "ready" | "unavailable";
98
+ };
99
+ }
100
+
101
+ export interface ProjectionFileResultV1 {
102
+ path: string;
103
+ action: "create" | "delete" | "unchanged" | "update";
104
+ preimageDigest: Sha256Digest | null;
105
+ outputDigest: Sha256Digest | null;
106
+ }
107
+
108
+ export interface ProjectionHumanActionV1 {
109
+ reasonCode: ProjectionHumanActionReasonCode;
110
+ affectedNodeIds: string[];
111
+ requestPayloadDigest: Sha256Digest;
112
+ }
113
+
114
+ export interface ArchitectureDigestSetV1 {
115
+ modelDigest: Sha256Digest;
116
+ sourceTreeDigest: Sha256Digest;
117
+ flowProofDigest: Sha256Digest;
118
+ projectionDigest: Sha256Digest;
119
+ }
120
+
121
+ export interface AcceptedArchitectureChangeReferenceV1 {
122
+ changeSetId: string;
123
+ eventId: string;
124
+ reasonCodes: ArchitectureMajorChangeReasonCode[];
125
+ affectedNodeIds: string[];
126
+ }
127
+
128
+ export interface ArchitectureRefreshSignalV1 {
129
+ schemaVersion: typeof ARCHITECTURE_REFRESH_SIGNAL_SCHEMA_VERSION;
130
+ signalId: Sha256Digest;
131
+ idempotencyKey: Sha256Digest;
132
+ mode: "human-action-required" | "refresh-required";
133
+ repository: { repositoryId: string };
134
+ worktree: { workspaceId: string; headSha: string; worktreeDigest: Sha256Digest };
135
+ cause: "accepted-semantic-delta" | "unresolved-major-candidate" | "verified-flow-proof-delta";
136
+ acceptedChange?: AcceptedArchitectureChangeReferenceV1;
137
+ reasonCodes: ArchitectureMajorChangeReasonCode[];
138
+ affectedNodeIds: string[];
139
+ refreshTargets: ArchitectureRefreshTarget[];
140
+ baseDigests: ArchitectureDigestSetV1;
141
+ resultingDigests: ArchitectureDigestSetV1;
142
+ projectionReceiptDigest: Sha256Digest;
143
+ }
144
+
145
+ export interface ProjectionResultV1 {
146
+ schemaVersion: typeof PROJECTION_RESULT_SCHEMA_VERSION;
147
+ requestId: string;
148
+ status: ProjectionResultStatus;
149
+ inputSnapshot: ProjectionSnapshotV1;
150
+ outputSnapshot: ProjectionSnapshotV1;
151
+ affectedNodeIds: string[];
152
+ files: ProjectionFileResultV1[];
153
+ humanActions: ProjectionHumanActionV1[];
154
+ refreshSignals: ArchitectureRefreshSignalV1[];
155
+ receiptDigest: Sha256Digest;
156
+ }
157
+
158
+ export interface ArchctxCapabilitiesV1 {
159
+ schemaVersion: typeof ARCHCTX_CAPABILITIES_SCHEMA_VERSION;
160
+ package: {
161
+ name: "archctx";
162
+ version: string;
163
+ };
164
+ protocols: {
165
+ projectionRequest: typeof PROJECTION_REQUEST_SCHEMA_VERSION;
166
+ projectionResult: typeof PROJECTION_RESULT_SCHEMA_VERSION;
167
+ architectureRefreshSignal: typeof ARCHITECTURE_REFRESH_SIGNAL_SCHEMA_VERSION;
168
+ };
169
+ renderers: {
170
+ architectureDocs: typeof ARCHITECTURE_DOCS_RENDERER_VERSION;
171
+ agentContext: typeof AGENT_CONTEXT_RENDERER_VERSION;
172
+ };
173
+ features: ArchctxFeature[];
174
+ }
175
+
176
+ export function archctxCapabilities(packageVersion: string): ArchctxCapabilitiesV1 {
177
+ return {
178
+ schemaVersion: ARCHCTX_CAPABILITIES_SCHEMA_VERSION,
179
+ package: { name: "archctx", version: packageVersion },
180
+ protocols: {
181
+ projectionRequest: PROJECTION_REQUEST_SCHEMA_VERSION,
182
+ projectionResult: PROJECTION_RESULT_SCHEMA_VERSION,
183
+ architectureRefreshSignal: ARCHITECTURE_REFRESH_SIGNAL_SCHEMA_VERSION
184
+ },
185
+ renderers: {
186
+ architectureDocs: ARCHITECTURE_DOCS_RENDERER_VERSION,
187
+ agentContext: AGENT_CONTEXT_RENDERER_VERSION
188
+ },
189
+ features: [...ARCHCTX_FEATURES]
190
+ };
191
+ }
192
+
193
+ export function projectionRequestInvariantIssues(input: ProjectionRequestV1): string[] {
194
+ const issues = [
195
+ ...sortedUniqueIssues("targets", input.targets),
196
+ ...sortedUniqueIssues("changedPaths", input.changedPaths)
197
+ ];
198
+ if (input.targets.length === 0) issues.push("targets must contain at least one projection target");
199
+ if (!/^[a-zA-Z0-9_.:-]+$/.test(input.requestId)) issues.push("requestId must use the stable identifier character set");
200
+ if (input.mode === "adopt" && !input.adoptionPlanId) issues.push("adoptionPlanId is required when mode=adopt");
201
+ if (input.mode !== "adopt" && input.adoptionPlanId !== undefined) issues.push("adoptionPlanId is only allowed when mode=adopt");
202
+ return issues;
203
+ }
204
+
205
+ export function projectionResultInvariantIssues(input: ProjectionResultV1): string[] {
206
+ const issues = [
207
+ ...sortedUniqueIssues("affectedNodeIds", input.affectedNodeIds),
208
+ ...sortedUniqueIssues("files.path", input.files.map((file) => file.path)),
209
+ ...sortedUniqueIssues("refreshSignals.signalId", input.refreshSignals.map((signal) => signal.signalId)),
210
+ ...input.humanActions.flatMap((action, index) => sortedUniqueIssues(`humanActions[${index}].affectedNodeIds`, action.affectedNodeIds)),
211
+ ...input.refreshSignals.flatMap((signal, index) => architectureRefreshSignalInvariantIssues(signal, `refreshSignals[${index}]`))
212
+ ];
213
+ if (!/^[a-zA-Z0-9_.:-]+$/.test(input.requestId)) issues.push("requestId must use the stable identifier character set");
214
+ if (input.inputSnapshot.repositoryId !== input.outputSnapshot.repositoryId) issues.push("outputSnapshot.repositoryId must match inputSnapshot.repositoryId");
215
+ if (input.inputSnapshot.workspaceId !== input.outputSnapshot.workspaceId) issues.push("outputSnapshot.workspaceId must match inputSnapshot.workspaceId");
216
+ if (input.inputSnapshot.baseHeadSha !== input.outputSnapshot.baseHeadSha) issues.push("outputSnapshot.baseHeadSha must match inputSnapshot.baseHeadSha");
217
+ const statusRequiresHumanAction = input.status === "adoption-required" || input.status === "human-action-required";
218
+ if (statusRequiresHumanAction && input.humanActions.length === 0) issues.push(`${input.status} status requires at least one human action`);
219
+ if (!statusRequiresHumanAction && input.humanActions.length > 0) issues.push(`human actions are not allowed when status=${input.status}`);
220
+ for (const [index, file] of input.files.entries()) {
221
+ const prefix = `files[${index}]`;
222
+ if (file.action === "create" && (file.preimageDigest !== null || file.outputDigest === null)) {
223
+ issues.push(`${prefix} create requires null preimageDigest and non-null outputDigest`);
224
+ }
225
+ if (file.action === "delete" && (file.preimageDigest === null || file.outputDigest !== null)) {
226
+ issues.push(`${prefix} delete requires non-null preimageDigest and null outputDigest`);
227
+ }
228
+ if (file.action === "update" && (file.preimageDigest === null || file.outputDigest === null || file.preimageDigest === file.outputDigest)) {
229
+ issues.push(`${prefix} update requires two different non-null digests`);
230
+ }
231
+ if (file.action === "unchanged" && (file.preimageDigest === null || file.outputDigest === null || file.preimageDigest !== file.outputDigest)) {
232
+ issues.push(`${prefix} unchanged requires equal non-null digests`);
233
+ }
234
+ }
235
+ const { receiptDigest, ...receiptPayload } = input;
236
+ if (projectionResultReceiptDigest(receiptPayload) !== receiptDigest) issues.push("receiptDigest must match the canonical projection result payload");
237
+ for (const [index, signal] of input.refreshSignals.entries()) {
238
+ const prefix = `refreshSignals[${index}]`;
239
+ if (signal.projectionReceiptDigest !== receiptDigest) issues.push(`${prefix}.projectionReceiptDigest must match receiptDigest`);
240
+ if (signal.repository.repositoryId !== input.outputSnapshot.repositoryId) issues.push(`${prefix}.repositoryId must match outputSnapshot.repositoryId`);
241
+ if (signal.worktree.workspaceId !== input.outputSnapshot.workspaceId) issues.push(`${prefix}.workspaceId must match outputSnapshot.workspaceId`);
242
+ if (signal.worktree.headSha !== input.outputSnapshot.headSha) issues.push(`${prefix}.headSha must match outputSnapshot.headSha`);
243
+ if (signal.worktree.worktreeDigest !== input.outputSnapshot.worktreeDigest) issues.push(`${prefix}.worktreeDigest must match outputSnapshot.worktreeDigest`);
244
+ }
245
+ return issues;
246
+ }
247
+
248
+ /**
249
+ * Computes the accepted projection receipt. Signal back-references are omitted from the
250
+ * receipt payload to avoid a circular hash; every signal must then bind that receipt via
251
+ * projectionReceiptDigest, which projectionResultInvariantIssues verifies.
252
+ */
253
+ export function projectionResultReceiptDigest(input: Omit<ProjectionResultV1, "receiptDigest">): Sha256Digest {
254
+ const refreshSignals = input.refreshSignals.map(({ projectionReceiptDigest: _projectionReceiptDigest, ...signal }) => signal);
255
+ return digestJson({ ...input, refreshSignals } as unknown as Json) as Sha256Digest;
256
+ }
257
+
258
+ export function architectureRefreshSignalInvariantIssues(input: ArchitectureRefreshSignalV1, prefix = "signal"): string[] {
259
+ const issues = [
260
+ ...sortedUniqueIssues(`${prefix}.reasonCodes`, input.reasonCodes),
261
+ ...sortedUniqueIssues(`${prefix}.affectedNodeIds`, input.affectedNodeIds),
262
+ ...sortedUniqueIssues(`${prefix}.refreshTargets`, input.refreshTargets)
263
+ ];
264
+ if (input.acceptedChange) {
265
+ issues.push(
266
+ ...sortedUniqueIssues(`${prefix}.acceptedChange.reasonCodes`, input.acceptedChange.reasonCodes),
267
+ ...sortedUniqueIssues(`${prefix}.acceptedChange.affectedNodeIds`, input.acceptedChange.affectedNodeIds)
268
+ );
269
+ if (input.acceptedChange.changeSetId.trim() === "") issues.push(`${prefix}.acceptedChange.changeSetId must not be empty`);
270
+ if (input.acceptedChange.eventId.trim() === "") issues.push(`${prefix}.acceptedChange.eventId must not be empty`);
271
+ if (input.acceptedChange.reasonCodes.join("\u0000") !== input.reasonCodes.join("\u0000")) {
272
+ issues.push(`${prefix}.acceptedChange.reasonCodes must match signal reasonCodes`);
273
+ }
274
+ if (input.acceptedChange.affectedNodeIds.join("\u0000") !== input.affectedNodeIds.join("\u0000")) {
275
+ issues.push(`${prefix}.acceptedChange.affectedNodeIds must match signal affectedNodeIds`);
276
+ }
277
+ }
278
+ if (input.reasonCodes.length === 0) issues.push(`${prefix}.reasonCodes must contain at least one reason`);
279
+ if (input.affectedNodeIds.length === 0) issues.push(`${prefix}.affectedNodeIds must contain at least one node`);
280
+ if (input.refreshTargets.length === 0) issues.push(`${prefix}.refreshTargets must contain at least one target`);
281
+ if (input.cause === "unresolved-major-candidate" && input.mode !== "human-action-required") {
282
+ issues.push(`${prefix}.unresolved-major-candidate requires human-action-required mode`);
283
+ }
284
+ if (input.cause !== "unresolved-major-candidate" && input.mode !== "refresh-required") {
285
+ issues.push(`${prefix}.${input.cause} requires refresh-required mode`);
286
+ }
287
+ if (input.mode === "refresh-required" && !input.acceptedChange) {
288
+ issues.push(`${prefix}.refresh-required requires acceptedChange`);
289
+ }
290
+ if (input.mode === "human-action-required" && input.acceptedChange) {
291
+ issues.push(`${prefix}.human-action-required forbids acceptedChange`);
292
+ }
293
+ return issues;
294
+ }
295
+
296
+ function sortedUniqueIssues(label: string, values: readonly string[]): string[] {
297
+ const expected = [...new Set(values)].sort();
298
+ return expected.length === values.length && expected.every((value, index) => value === values[index])
299
+ ? []
300
+ : [`${label} must be sorted and unique`];
301
+ }
package/src/schema.ts CHANGED
@@ -36,6 +36,7 @@ export interface ArchContextError {
36
36
  severity: Severity;
37
37
  retryable: boolean;
38
38
  action: string;
39
+ reasonCode?: string;
39
40
  }
40
41
 
41
42
  export interface JsonEnvelope<T extends Json = Json> {
@@ -71,12 +72,12 @@ export function okEnvelope<T extends Json>(requestId: string, data: T): JsonEnve
71
72
  return { schemaVersion: "archcontext.envelope/v1", ok: true, requestId, data };
72
73
  }
73
74
 
74
- export function errorEnvelope(requestId: string, code: ArchContextErrorCode, message: string): JsonEnvelope {
75
+ export function errorEnvelope(requestId: string, code: ArchContextErrorCode, message: string, reasonCode?: string): JsonEnvelope {
75
76
  return {
76
77
  schemaVersion: "archcontext.envelope/v1",
77
78
  ok: false,
78
79
  requestId,
79
- error: { ...ERROR_CATALOG[code], message }
80
+ error: { ...ERROR_CATALOG[code], message, ...(reasonCode ? { reasonCode } : {}) }
80
81
  };
81
82
  }
82
83
 
package/src/validator.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { Json } from "./schema";
1
+ import { canonicalize, type Json } from "./schema";
2
2
 
3
3
  export interface ValidationIssue {
4
4
  path: string;
@@ -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,42 @@ type JsonSchema = {
19
21
  properties?: Record<string, JsonSchema>;
20
22
  items?: JsonSchema;
21
23
  oneOf?: JsonSchema[];
24
+ anyOf?: JsonSchema[];
25
+ allOf?: JsonSchema[];
26
+ if?: JsonSchema;
27
+ then?: JsonSchema;
28
+ else?: JsonSchema;
29
+ not?: JsonSchema;
22
30
  additionalProperties?: boolean | JsonSchema;
23
31
  minItems?: number;
32
+ maxItems?: number;
33
+ uniqueItems?: boolean;
34
+ minLength?: number;
35
+ maxLength?: number;
24
36
  minimum?: number;
25
37
  maximum?: number;
26
38
  };
27
39
 
28
40
  export function validateJsonSchema(schema: JsonSchema, value: Json): ValidationResult {
29
41
  const issues: ValidationIssue[] = [];
30
- visit(schema, value, "$", issues);
42
+ visit(schema, value, "$", issues, schema);
31
43
  return { valid: issues.length === 0, issues };
32
44
  }
33
45
 
34
- function visit(schema: JsonSchema, value: Json, path: string, issues: ValidationIssue[]): void {
46
+ function visit(schema: JsonSchema, value: Json, path: string, issues: ValidationIssue[], root: JsonSchema): void {
47
+ if (schema.$ref) {
48
+ if (!schema.$ref.startsWith("#/")) return;
49
+ const resolved = resolveLocalRef(root, schema.$ref);
50
+ if (!resolved) {
51
+ issues.push({ path, message: `unresolved schema reference ${schema.$ref}` });
52
+ return;
53
+ }
54
+ visit(resolved, value, path, issues, root);
55
+ }
35
56
  if (schema.oneOf) {
36
57
  const matched = schema.oneOf.filter((candidate) => {
37
58
  const candidateIssues: ValidationIssue[] = [];
38
- visit(candidate, value, path, candidateIssues);
59
+ visit(candidate, value, path, candidateIssues, root);
39
60
  return candidateIssues.length === 0;
40
61
  }).length;
41
62
  if (matched !== 1) {
@@ -43,6 +64,26 @@ function visit(schema: JsonSchema, value: Json, path: string, issues: Validation
43
64
  return;
44
65
  }
45
66
  }
67
+ if (schema.anyOf) {
68
+ const matched = schema.anyOf.some((candidate) => {
69
+ const candidateIssues: ValidationIssue[] = [];
70
+ visit(candidate, value, path, candidateIssues, root);
71
+ return candidateIssues.length === 0;
72
+ });
73
+ if (!matched) issues.push({ path, message: "expected at least one matching schema" });
74
+ }
75
+ for (const candidate of schema.allOf ?? []) visit(candidate, value, path, issues, root);
76
+ if (schema.if) {
77
+ const conditionIssues: ValidationIssue[] = [];
78
+ visit(schema.if, value, path, conditionIssues, root);
79
+ const branch = conditionIssues.length === 0 ? schema.then : schema.else;
80
+ if (branch) visit(branch, value, path, issues, root);
81
+ }
82
+ if (schema.not) {
83
+ const candidateIssues: ValidationIssue[] = [];
84
+ visit(schema.not, value, path, candidateIssues, root);
85
+ if (candidateIssues.length === 0) issues.push({ path, message: "matched forbidden schema" });
86
+ }
46
87
  if (schema.const !== undefined && JSON.stringify(value) !== JSON.stringify(schema.const)) {
47
88
  issues.push({ path, message: `expected const ${JSON.stringify(schema.const)}` });
48
89
  return;
@@ -57,6 +98,12 @@ function visit(schema: JsonSchema, value: Json, path: string, issues: Validation
57
98
  if (typeof value === "string" && schema.pattern && !new RegExp(schema.pattern).test(value)) {
58
99
  issues.push({ path, message: `does not match ${schema.pattern}` });
59
100
  }
101
+ if (typeof value === "string" && schema.minLength !== undefined && value.length < schema.minLength) {
102
+ issues.push({ path, message: `shorter than minimum length ${schema.minLength}` });
103
+ }
104
+ if (typeof value === "string" && schema.maxLength !== undefined && value.length > schema.maxLength) {
105
+ issues.push({ path, message: `longer than maximum length ${schema.maxLength}` });
106
+ }
60
107
  if (typeof value === "number") {
61
108
  if (schema.minimum !== undefined && value < schema.minimum) issues.push({ path, message: `below minimum ${schema.minimum}` });
62
109
  if (schema.maximum !== undefined && value > schema.maximum) issues.push({ path, message: `above maximum ${schema.maximum}` });
@@ -65,7 +112,13 @@ function visit(schema: JsonSchema, value: Json, path: string, issues: Validation
65
112
  if (schema.minItems !== undefined && value.length < schema.minItems) {
66
113
  issues.push({ path, message: `expected at least ${schema.minItems} items` });
67
114
  }
68
- if (schema.items) value.forEach((item, index) => visit(schema.items!, item, `${path}[${index}]`, issues));
115
+ if (schema.maxItems !== undefined && value.length > schema.maxItems) {
116
+ issues.push({ path, message: `expected at most ${schema.maxItems} items` });
117
+ }
118
+ if (schema.uniqueItems && new Set(value.map(canonicalize)).size !== value.length) {
119
+ issues.push({ path, message: "expected unique items" });
120
+ }
121
+ if (schema.items) value.forEach((item, index) => visit(schema.items!, item, `${path}[${index}]`, issues, root));
69
122
  }
70
123
  if (value !== null && typeof value === "object" && !Array.isArray(value)) {
71
124
  const objectValue = value as Record<string, Json>;
@@ -73,7 +126,7 @@ function visit(schema: JsonSchema, value: Json, path: string, issues: Validation
73
126
  if (!(key in objectValue)) issues.push({ path: `${path}.${key}`, message: "required" });
74
127
  }
75
128
  for (const [key, child] of Object.entries(schema.properties ?? {})) {
76
- if (key in objectValue) visit(child, objectValue[key], `${path}.${key}`, issues);
129
+ if (key in objectValue) visit(child, objectValue[key], `${path}.${key}`, issues, root);
77
130
  }
78
131
  if (schema.additionalProperties === false && schema.properties) {
79
132
  for (const key of Object.keys(objectValue)) {
@@ -83,6 +136,17 @@ function visit(schema: JsonSchema, value: Json, path: string, issues: Validation
83
136
  }
84
137
  }
85
138
 
139
+ function resolveLocalRef(root: JsonSchema, ref: string): JsonSchema | undefined {
140
+ if (!ref.startsWith("#/")) return undefined;
141
+ let current: unknown = root;
142
+ for (const rawSegment of ref.slice(2).split("/")) {
143
+ const segment = rawSegment.replace(/~1/g, "/").replace(/~0/g, "~");
144
+ if (!current || typeof current !== "object" || Array.isArray(current) || !(segment in current)) return undefined;
145
+ current = (current as Record<string, unknown>)[segment];
146
+ }
147
+ return current && typeof current === "object" && !Array.isArray(current) ? current as JsonSchema : undefined;
148
+ }
149
+
86
150
  function matchesType(type: string | string[], value: Json): boolean {
87
151
  const allowed = Array.isArray(type) ? type : [type];
88
152
  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
- }