archctx-contracts 0.8.0 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schemaVersion": "archcontext.capabilities/v1",
3
- "package": { "name": "archctx", "version": "0.8.0" },
3
+ "package": { "name": "archctx", "version": "0.8.1" },
4
4
  "protocols": {
5
5
  "projectionRequest": "archcontext.projection-request/v1",
6
6
  "projectionResult": "archcontext.projection-result/v2",
@@ -2,7 +2,7 @@
2
2
  "schemaVersion": "archcontext.product-version-manifest/v1",
3
3
  "product": {
4
4
  "name": "archctx",
5
- "version": "0.8.0",
5
+ "version": "0.8.1",
6
6
  "distribution": "one-package"
7
7
  },
8
8
  "packageManager": "bun@1.4.3",
@@ -12,25 +12,25 @@
12
12
  "surfaces": {
13
13
  "cli": {
14
14
  "packageName": "@archcontext/surfaces",
15
- "version": "0.8.0",
15
+ "version": "0.8.1",
16
16
  "entrypoint": "archctx"
17
17
  },
18
18
  "daemon": {
19
19
  "packageName": "@archcontext/local-runtime",
20
- "version": "0.8.0",
20
+ "version": "0.8.1",
21
21
  "entrypoint": "archctx daemon start",
22
22
  "rpcSchemaVersion": "archcontext.runtime-rpc/v2"
23
23
  },
24
24
  "mcp": {
25
25
  "packageName": "@archcontext/surfaces",
26
- "version": "0.8.0",
26
+ "version": "0.8.1",
27
27
  "entrypoint": "archctx mcp",
28
28
  "transport": "stdio"
29
29
  }
30
30
  },
31
31
  "schemas": {
32
32
  "schemaSetVersion": "2026-06-25.al0-ledger",
33
- "contractsPackageVersion": "0.8.0",
33
+ "contractsPackageVersion": "0.8.1",
34
34
  "reviewChallenge": "archcontext.review-challenge/v2",
35
35
  "attestation": "archcontext.attestation/v2",
36
36
  "deviceIdentity": "archcontext.device-identity/v1",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "archctx-contracts",
3
- "version": "0.8.0",
3
+ "version": "0.8.1",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -0,0 +1,18 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://archctx.repoharness.com/schemas/runtime/native-model-issue.schema.json",
4
+ "title": "NativeModelIssue",
5
+ "description": "A native model schema or reference issue. Source paths are optional for callers that do not supply parsed record paths.",
6
+ "type": "object",
7
+ "additionalProperties": false,
8
+ "required": ["entityId", "field", "invariant"],
9
+ "properties": {
10
+ "entityId": { "type": "string" },
11
+ "field": { "type": "string" },
12
+ "invariant": { "type": "string" },
13
+ "path": {
14
+ "type": "string",
15
+ "description": "The repository-relative path of the parsed source file that holds this record. It is not derived from the entity ID."
16
+ }
17
+ }
18
+ }
package/src/failure.ts CHANGED
@@ -23,6 +23,16 @@ export interface FailureEnvelope<C extends FailureCode = FailureCode> {
23
23
  error: StrictFailure<C>;
24
24
  }
25
25
  type Policy = { code: FailureCode; message: string; fields?: readonly string[]; action?: string; facts?: Readonly<Record<string, string>>; native?: boolean; recovery?: boolean; journalRecovery?: boolean; retryable?: boolean };
26
+ /** Closed repository diagnostics. Messages contain no process output or filesystem paths. */
27
+ export const CODE_FACTS_ISSUE_MESSAGES = Object.freeze({
28
+ "not-initialized": "CodeGraph repository status is not initialized",
29
+ "version-mismatch": "CodeGraph repository status version mismatch",
30
+ "project-path-mismatch": "CodeGraph repository project path mismatch",
31
+ "index-incomplete": "CodeGraph repository index is not complete and compatible",
32
+ "extraction-version-mismatch": "CodeGraph repository extraction version mismatch",
33
+ "worktree-mismatch": "CodeGraph repository worktree mismatch",
34
+ "pending-source-changes": "CodeGraph repository index has pending or unreported source changes"
35
+ });
26
36
  /** Each reason has a closed fact vocabulary. No source text or recovery binding is a summary fact. */
27
37
  const policies: Record<string, Policy> = {
28
38
  "git-worktree-required": { code: "AC_REPO_NOT_FOUND", message: "This operation requires a Git worktree.", fields: ["checks"] },
@@ -33,6 +43,7 @@ const policies: Record<string, Policy> = {
33
43
  "projection-manifest-invalid": { code: "AC_PRECONDITION_FAILED", message: "The committed projection manifest violates its contract." },
34
44
  "projection-footprint-uncovered": { code: "AC_PRECONDITION_FAILED", message: "Declared source footprints lack a projection target." },
35
45
  "index-missing": { code: "AC_CODE_FACTS_UNAVAILABLE", message: "Required code facts have no available index. Run codegraph init." },
46
+ "code-facts-not-ready": { code: "AC_CODE_FACTS_NOT_READY", message: "Repository code facts are not ready. Run archctx doctor --json and follow the reported action.", facts: { issues: "codeFactsIssues" } },
36
47
  "checkpoint-input-invalid": { code: "AC_SCHEMA_INVALID", message: "Checkpoint arguments violate the declared input schema.", fields: ["issueCount"], action: "correct-command-arguments" },
37
48
  "cli-input-invalid": { code: "AC_SCHEMA_INVALID", message: "Command arguments violate the declared input contract.", action: "correct-command-arguments" },
38
49
  "complete-input-invalid": { code: "AC_SCHEMA_INVALID", message: "Completion input violates the caller authority contract.", action: "correct-command-arguments" },
@@ -41,7 +52,7 @@ const policies: Record<string, Policy> = {
41
52
  "runtime-version-unsupported": { code: "AC_RUNTIME_VERSION_UNSUPPORTED", message: "The connected runtime version is unsupported." },
42
53
  "rpc-timeout": { code: "RPC_TIMEOUT", message: "The runtime RPC deadline elapsed without a response; a mutation outcome is unknown.", fields: ["method", "timeoutMs", "elapsedMs", "mutationOutcome"], action: "reconcile-before-retry" },
43
54
  "rpc-aborted": { code: "RPC_ABORTED", message: "The runtime RPC caller aborted without a response; a mutation outcome is unknown.", fields: ["method", "timeoutMs", "elapsedMs", "mutationOutcome"], action: "reconcile-before-retry" },
44
- "failure-unclassified": { code: "AC_UNCLASSIFIED_FAILURE", message: "The operation failed without a classified cause. Inspect local diagnostics before retrying.", action: "inspect-diagnostics" },
55
+ "failure-unclassified": { code: "AC_UNCLASSIFIED_FAILURE", message: "The operation failed without a classified cause. Run archctx doctor --json before retrying.", action: "inspect-diagnostics" },
45
56
  };
46
57
  // C1 policies are selected at explicit producer branches, never from diagnostic text.
47
58
  Object.assign(policies, {
@@ -278,7 +289,7 @@ for (const policy of Object.values(policies)) {
278
289
  }
279
290
  export const FAILURE_POLICIES: Readonly<Record<string, Readonly<Policy>>> = Object.freeze(policies);
280
291
  const catalogDefaults = Object.fromEntries(Object.entries(ERROR_CATALOG).map(([code, value]) => [code, Object.freeze({ severity: value.severity, retryable: value.retryable, action: value.action })])) as Readonly<Record<ArchContextErrorCode, Readonly<Pick<StrictFailure, "severity" | "retryable" | "action">>>>;
281
- const actions = new Set([...Object.values(catalogDefaults).map(value => value.action), "correct-command-arguments", "inspect-diagnostics", "reconcile-before-retry", ...Object.values(policies).flatMap(policy => policy.action ? [policy.action] : [])]);
292
+ const actions = new Set([...Object.values(catalogDefaults).map(value => value.action), "correct-command-arguments", "inspect-diagnostics", "reconcile-before-retry", "install-compatible-codegraph", ...Object.values(policies).flatMap(policy => policy.action ? [policy.action] : [])]);
282
293
  const bytes = (value: string) => new TextEncoder().encode(value).byteLength;
283
294
  const record = (value: unknown): value is Record<string, unknown> => value !== null && typeof value === "object" && !Array.isArray(value);
284
295
  const finiteNonnegative = (value: unknown): value is number => typeof value === "number" && Number.isFinite(value) && value >= 0;
@@ -400,6 +411,20 @@ const localEnums: Readonly<Record<string, readonly string[]>> = {
400
411
  ]
401
412
  };
402
413
  function localFactValue(rule: string, value: unknown): Json {
414
+ if (rule === "codeFactsIssues") {
415
+ if (!Array.isArray(value)) throw new Error("Code facts issues must be a list");
416
+ const length = Object.getOwnPropertyDescriptor(value, "length")?.value;
417
+ if (!Number.isSafeInteger(length) || length < 1 || length > 32 || Reflect.ownKeys(value).length !== length + 1) throw new Error("Code facts issues exceed their closed budget");
418
+ return Array.from({ length }, (_, index) => {
419
+ const entry = Object.getOwnPropertyDescriptor(value, String(index));
420
+ if (!entry || !("value" in entry)) throw new Error("Code facts issues must use own data");
421
+ const fields = dataRecord(entry.value);
422
+ const code = fields.code?.value, message = fields.message?.value;
423
+ if (Object.keys(fields).length !== 2 || typeof code !== "string" || !Object.hasOwn(CODE_FACTS_ISSUE_MESSAGES, code)
424
+ || message !== CODE_FACTS_ISSUE_MESSAGES[code as keyof typeof CODE_FACTS_ISSUE_MESSAGES]) throw new Error("Unknown code facts diagnostic");
425
+ return { code, message };
426
+ });
427
+ }
403
428
  if (rule === "commandSuggestion" && (value === null || (typeof value === "string" && bytes(value) <= 1024 && /^archctx (recommendations |refactor verify )[^\r\n]*$/.test(value)))) return value;
404
429
  if (rule === "publicConfirmationToken" && typeof value === "string" && bytes(value) <= 1024 && /^public:github\.com\/[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+:[a-f0-9]{40}:[A-Za-z0-9_.:-]+$/.test(value)) return value;
405
430
  if (rule === "diagnosticPaths" || rule === "diagnosticCommands") {
@@ -568,7 +593,7 @@ function nativeFactsFor(policy: Policy, input: unknown): {[key: string]: Json} {
568
593
  if (type === "diagnostic") { const s = text(value); if (bytes(s) > 256) { omit(s); return undefined; } return s; }
569
594
  const item = dataRecord(value);
570
595
  if (type === "issues") {
571
- if (Object.keys(item).length !== 3 || !["entityId", "field", "invariant"].every(key => Object.hasOwn(item, key))) throw new Error("Native issue schema is invalid");
596
+ if (!["entityId", "field", "invariant"].every(key => Object.hasOwn(item, key)) || Object.keys(item).some(key => !["entityId", "field", "invariant", "path"].includes(key))) throw new Error("Native issue schema is invalid");
572
597
  const clean = Object.fromEntries(Object.entries(item).map(([key, data]) => [key, text(data.value)]));
573
598
  if (Object.values(clean).some(v => bytes(v) > 256)) { omit(clean); return undefined; }
574
599
  return clean;
@@ -616,12 +641,21 @@ function nativeFactsFor(policy: Policy, input: unknown): {[key: string]: Json} {
616
641
  }
617
642
  result[key] = values;
618
643
  }
644
+ const sourceIssues = Array.isArray(result.issues) ? result.issues as Array<Record<string, Json>> : [];
645
+ // Legacy issues without source paths keep any explicit path supplied by the caller.
646
+ const summarizePath = sourceIssues.some(issue => typeof issue.path === "string");
619
647
  const counts = () => {
620
648
  if (!Number.isSafeInteger(omitted) || !Number.isSafeInteger(characters)) throw new Error("Native diagnostic counts exceed schema");
621
649
  if (omitted) result.omittedDiagnosticCount = omitted;
622
650
  if (characters) result.omittedDiagnosticCharacters = characters;
623
651
  };
624
652
  const fits = () => {
653
+ // Derive the summary from surviving whole issues, including after budget omission.
654
+ if (summarizePath) {
655
+ const path = sourceIssues[0]?.path;
656
+ if (typeof path === "string" && sourceIssues.every(issue => issue.path === path)) result.path = path;
657
+ else delete result.path;
658
+ }
625
659
  counts();
626
660
  const base = catalogDefaults[policy.code as ArchContextErrorCode];
627
661
  const error = {code: policy.code, message: policy.message, severity: base.severity, retryable: policy.retryable ?? base.retryable, action: policy.action ?? base.action, reasonCode: Object.entries(policies).find(([, p]) => p === policy)?.[0], details: result};
@@ -149,7 +149,7 @@ export function nativeByteCompare(left: string, right: string): number {
149
149
 
150
150
  type SchemaDocument = Parameters<typeof validateJsonSchema>[0];
151
151
  export interface NativeContextSchemas { node: SchemaDocument; relation: SchemaDocument; flow: SchemaDocument }
152
- export interface NativeModelIssue { entityId: string; field: string; invariant: string }
152
+ export interface NativeModelIssue { entityId: string; field: string; invariant: string; path?: string }
153
153
 
154
154
  /** Fixed canonical documents are supplied by the bundled reader, never by repository input. */
155
155
  export function nativeContextSchemaResolver(schemas: NativeContextSchemas): JsonSchemaResolver {
@@ -162,10 +162,14 @@ export function nativeAllowedNodeKinds(schemas: NativeContextSchemas): string[]
162
162
  return [...kinds] as string[];
163
163
  }
164
164
 
165
- /** Complete native shape/reference validation; no filesystem, symbols, runtime or mutation. */
166
- export function nativeContextModelIssues(model: NativeContextModel, schemas: NativeContextSchemas): NativeModelIssue[] {
165
+ /** Complete native shape/reference validation; optional paths bind parsed records, never IDs. */
166
+ export function nativeContextModelIssues(model: NativeContextModel, schemas: NativeContextSchemas, recordPaths?: ReadonlyMap<NativeModelRecord, string>): NativeModelIssue[] {
167
167
  const issues: NativeModelIssue[] = [];
168
- const add = (entityId: string, field: string, invariant: string) => issues.push({ entityId, field, invariant });
168
+ const add = (record: NativeModelRecord | string, field: string, invariant: string) => {
169
+ const entityId = typeof record === "string" ? record : typeof record.id === "string" ? record.id : "(undeclared)";
170
+ const path = typeof record === "string" ? undefined : recordPaths?.get(record);
171
+ issues.push({ entityId, field, invariant, ...(path === undefined ? {} : { path }) });
172
+ };
169
173
  const resolveSchema = nativeContextSchemaResolver(schemas);
170
174
  const ids = new Set<string>();
171
175
  for (const [kind, records] of [["node", model.nodes], ["relation", model.relations], ["flow", model.flows]] as const) {
@@ -182,46 +186,45 @@ export function nativeContextModelIssues(model: NativeContextModel, schemas: Nat
182
186
  const item = pending.pop()!;
183
187
  const value = item.value;
184
188
  if (item.exit) { active.delete(value as object); done.add(value as object); continue; }
185
- if (typeof value === "number" && !Number.isFinite(value)) { add(id, item.field, "non-json-number"); jsonInvalid = true; break; }
189
+ if (typeof value === "number" && !Number.isFinite(value)) { add(record, item.field, "non-json-number"); jsonInvalid = true; break; }
186
190
  if (value === null || typeof value !== "object") continue;
187
- if (active.has(value)) { add(id, item.field, "non-json-cycle"); jsonInvalid = true; break; }
191
+ if (active.has(value)) { add(record, item.field, "non-json-cycle"); jsonInvalid = true; break; }
188
192
  if (done.has(value)) continue;
189
193
  const prototype = Object.getPrototypeOf(value);
190
- if (Array.isArray(value) ? prototype !== Array.prototype : prototype !== Object.prototype) { add(id, item.field, "non-json-prototype"); jsonInvalid = true; break; }
194
+ if (Array.isArray(value) ? prototype !== Array.prototype : prototype !== Object.prototype) { add(record, item.field, "non-json-prototype"); jsonInvalid = true; break; }
191
195
  active.add(value); pending.push({ value, field: item.field, exit: true });
192
196
  for (const [key, child] of Object.entries(value)) pending.push({ value: child, field: `${item.field}.${key}` });
193
197
  }
194
198
  if (jsonInvalid) continue;
195
199
  const schema = kind === "node" ? schemas.node : kind === "relation" ? schemas.relation : schemas.flow;
196
- for (const issue of validateJsonSchema(schema, record, { resolveSchema }).issues) add(id, issue.path, issue.message);
197
- if (ids.has(id)) add(id, "id", "duplicate-entity-id");
200
+ for (const issue of validateJsonSchema(schema, record, { resolveSchema }).issues) add(record, issue.path, issue.message);
201
+ if (ids.has(id)) add(record, "id", "duplicate-entity-id");
198
202
  ids.add(id);
199
203
  }
200
204
  }
201
205
  // Invalid shapes cannot safely participate in reference validation.
202
206
  if (issues.length > 0) return issues;
203
207
  for (const kind of ["nodes", "relations", "flows"] as const) {
204
- const expected = new Set(model[kind].map(entity => entity.id as string));
205
- for (const id of expected) if (!Object.hasOwn(model.sourcePaths[kind], id)) add(id, `sourcePaths.${kind}`, "source-path-index-incomplete");
208
+ const expected = new Map(model[kind].map(entity => [entity.id as string, entity]));
209
+ for (const [id, entity] of expected) if (!Object.hasOwn(model.sourcePaths[kind], id)) add(entity, `sourcePaths.${kind}`, "source-path-index-incomplete");
206
210
  for (const [id, path] of Object.entries(model.sourcePaths[kind])) {
207
211
  if (!expected.has(id)) add(id, `sourcePaths.${kind}`, "source-path-index-unknown-entity");
208
- if (!isNativeContextPath(path)) add(id, `sourcePaths.${kind}`, "path-unsafe");
212
+ if (!isNativeContextPath(path)) add(expected.get(id) ?? id, `sourcePaths.${kind}`, "path-unsafe");
209
213
  }
210
214
  }
211
215
  const nodes = new Map(model.nodes.map(node => [node.id as string, node]));
212
216
  const checked = new Set<string>();
213
217
  for (const node of model.nodes) {
214
- const id = node.id as string;
215
218
  const chain = new Set<string>();
216
219
  let cursor: NativeModelRecord | undefined = node;
217
220
  while (cursor) {
218
221
  const current = cursor.id as string;
219
- if (chain.has(current)) { add(id, "parent", "parent-cycle"); break; }
222
+ if (chain.has(current)) { add(node, "parent", "parent-cycle"); break; }
220
223
  if (checked.has(current)) break;
221
224
  chain.add(current);
222
225
  if (cursor.parent === undefined) break;
223
226
  const parent = nodes.get(cursor.parent as string);
224
- if (!parent) { add(current, "parent", "parent-node-missing"); break; }
227
+ if (!parent) { add(cursor, "parent", "parent-node-missing"); break; }
225
228
  cursor = parent;
226
229
  }
227
230
  for (const current of chain) checked.add(current);
@@ -229,48 +232,47 @@ export function nativeContextModelIssues(model: NativeContextModel, schemas: Nat
229
232
  const selectors = new Set<string>();
230
233
  for (const node of model.nodes) {
231
234
  const source = node.source as { include?: string[]; exclude?: string[]; entrypoints?: Array<{ id: string; path: string; symbols: Array<{ name: string; sinks: Array<{ id: string; path: string; symbol: string }> }> }> } | undefined;
232
- for (const path of [...source?.include ?? [], ...source?.exclude ?? []]) if (!isNativeContextPath(path)) add(node.id as string, "source", "path-unsafe");
235
+ for (const path of [...source?.include ?? [], ...source?.exclude ?? []]) if (!isNativeContextPath(path)) add(node, "source", "path-unsafe");
233
236
  for (const entry of source?.entrypoints ?? []) {
234
- if (!isNativeContextPath(entry.path)) add(node.id as string, "source.entrypoints.path", "path-unsafe");
237
+ if (!isNativeContextPath(entry.path)) add(node, "source.entrypoints.path", "path-unsafe");
235
238
  for (const symbol of entry.symbols) {
236
239
  for (const sink of symbol.sinks) {
237
- if (!isNativeContextPath(sink.path)) add(node.id as string, "source.entrypoints.symbols.sinks.path", "path-unsafe");
240
+ if (!isNativeContextPath(sink.path)) add(node, "source.entrypoints.symbols.sinks.path", "path-unsafe");
238
241
  const selector = JSON.stringify([entry.id, symbol.name, sink.id]);
239
- if (selectors.has(selector)) add(node.id as string, "source.entrypoints.symbols.sinks.id", "selector-ambiguous");
242
+ if (selectors.has(selector)) add(node, "source.entrypoints.symbols.sinks.id", "selector-ambiguous");
240
243
  selectors.add(selector);
241
244
  }
242
245
  }
243
246
  }
244
247
  }
245
248
  for (const relation of model.relations) {
246
- for (const field of ["source", "target", "via"] as const) if (relation[field] !== undefined && !nodes.has(relation[field] as string)) add(relation.id as string, field, "relation-node-missing");
249
+ for (const field of ["source", "target", "via"] as const) if (relation[field] !== undefined && !nodes.has(relation[field] as string)) add(relation, field, "relation-node-missing");
247
250
  }
248
251
  for (const flow of model.flows) {
249
- const id = flow.id as string;
250
- if (nodes.get(flow.capabilityId as string)?.kind !== "capability") add(id, "capabilityId", "flow-capability-missing");
252
+ if (nodes.get(flow.capabilityId as string)?.kind !== "capability") add(flow, "capabilityId", "flow-capability-missing");
251
253
  if (flow.applicability !== "required") continue;
252
254
  const participants = flow.participants as Array<{ id: string; nodeId: string }>;
253
255
  const participantIds = new Set<string>();
254
256
  for (const participant of participants) {
255
- if (participantIds.has(participant.id)) add(id, "participants.id", "participant-id-ambiguous");
257
+ if (participantIds.has(participant.id)) add(flow, "participants.id", "participant-id-ambiguous");
256
258
  participantIds.add(participant.id);
257
- if (!nodes.has(participant.nodeId)) add(id, "participants.nodeId", "participant-node-missing");
259
+ if (!nodes.has(participant.nodeId)) add(flow, "participants.nodeId", "participant-node-missing");
258
260
  }
259
261
  const definitions = new Map<string, string>();
260
262
  const outcomes = flow.outcomes as Array<{ id: string; steps: NativeModelRecord[]; terminal: { participant: string } }>;
261
263
  const outcomeIds = new Set<string>();
262
264
  for (const outcome of outcomes) {
263
- if (outcomeIds.has(outcome.id)) add(id, "outcomes.id", "outcome-id-ambiguous");
265
+ if (outcomeIds.has(outcome.id)) add(flow, "outcomes.id", "outcome-id-ambiguous");
264
266
  outcomeIds.add(outcome.id);
265
- if (!participantIds.has(outcome.terminal.participant)) add(id, "outcomes.terminal", "terminal-participant-missing");
267
+ if (!participantIds.has(outcome.terminal.participant)) add(flow, "outcomes.terminal", "terminal-participant-missing");
266
268
  }
267
269
  for (const step of [...flow.steps as NativeModelRecord[], ...outcomes.flatMap(outcome => outcome.steps)]) {
268
- if (!participantIds.has(step.from as string) || !participantIds.has(step.to as string)) add(id, "steps", "step-participant-missing");
270
+ if (!participantIds.has(step.from as string) || !participantIds.has(step.to as string)) add(flow, "steps", "step-participant-missing");
269
271
  const definition = canonicalize(step);
270
- if (definitions.has(step.id as string) && definitions.get(step.id as string) !== definition) add(id, "steps.id", "step-definition-ambiguous");
272
+ if (definitions.has(step.id as string) && definitions.get(step.id as string) !== definition) add(flow, "steps.id", "step-definition-ambiguous");
271
273
  definitions.set(step.id as string, definition);
272
274
  const selector = step.evidence as { entrypointId: string; sourceSymbol: string; sinkId: string };
273
- if (!selectors.has(JSON.stringify([selector.entrypointId, selector.sourceSymbol, selector.sinkId]))) add(id, "steps.evidence", "selector-missing");
275
+ if (!selectors.has(JSON.stringify([selector.entrypointId, selector.sourceSymbol, selector.sinkId]))) add(flow, "steps.evidence", "selector-missing");
274
276
  }
275
277
  }
276
278
  return issues;
package/src/ports.ts CHANGED
@@ -121,6 +121,8 @@ export interface SourceSelector {
121
121
  }
122
122
 
123
123
  export interface CodeFactsPort {
124
+ /** Read-only compatibility check before the existing incremental sync. No retry or rebuild. */
125
+ assertCanSync?(workspaceRoot: string): void;
124
126
  ensureReady(workspace: WorkspaceRef): Promise<CodeFactsSnapshot>;
125
127
  sync(input: { workspace: WorkspaceRef; changedPaths?: string[] }): Promise<CodeFactsSnapshot>;
126
128
  buildTaskContext(input: { task: string; maxSymbols: number; includeSource: boolean; changedPaths?: string[] }): Promise<NormalizedCodeContext>;
@@ -148,6 +150,8 @@ export interface ModelValidationResult {
148
150
  * `appliesTo` id with no node. Omitted when empty.
149
151
  */
150
152
  referenceErrors?: string[];
153
+ /** Node schema errors with their file paths. Only a write to that node can repair them. */
154
+ nodeSchemaErrors?: { path: string; error: string }[];
151
155
  /** Problems that do not invalidate the model, such as an ignored legacy setting. Omitted when empty. */
152
156
  warnings?: string[];
153
157
  modelDigest: string;
@@ -1,5 +1,5 @@
1
1
  export const ARCHCONTEXT_PRODUCT_NAME = "archctx";
2
- export const ARCHCONTEXT_PRODUCT_VERSION = "0.8.0";
2
+ export const ARCHCONTEXT_PRODUCT_VERSION = "0.8.1";
3
3
  export const ARCHCONTEXT_PACKAGE_MANAGER = "bun@1.4.3";
4
4
  export const ARCHCONTEXT_NODE_RANGE = ">=22.22 <26";
5
5
  export const LOCAL_RUNTIME_RPC_SCHEMA_VERSION = "archcontext.runtime-rpc/v2";
package/src/schema.ts CHANGED
@@ -18,6 +18,7 @@ export type ArchContextErrorCode =
18
18
  | "AC_NODE_VERSION_UNSUPPORTED"
19
19
  | "AC_CODEGRAPH_UNAVAILABLE"
20
20
  | "AC_CODE_FACTS_UNAVAILABLE"
21
+ | "AC_CODE_FACTS_NOT_READY"
21
22
  | "AC_INDEX_STALE"
22
23
  | "AC_CONTEXT_STALE"
23
24
  | "AC_SCHEMA_INVALID"
@@ -68,6 +69,7 @@ export const ERROR_CATALOG: Record<ArchContextErrorCode, Omit<ArchContextError,
68
69
  AC_NODE_VERSION_UNSUPPORTED: { code: "AC_NODE_VERSION_UNSUPPORTED", severity: "error", retryable: false, action: "use-supported-node-version" },
69
70
  AC_CODEGRAPH_UNAVAILABLE: { code: "AC_CODEGRAPH_UNAVAILABLE", severity: "error", retryable: true, action: "run-diagnostics" },
70
71
  AC_CODE_FACTS_UNAVAILABLE: { code: "AC_CODE_FACTS_UNAVAILABLE", severity: "error", retryable: true, action: "codegraph-init" },
72
+ AC_CODE_FACTS_NOT_READY: { code: "AC_CODE_FACTS_NOT_READY", severity: "error", retryable: false, action: "codegraph-index" },
71
73
  AC_INDEX_STALE: { code: "AC_INDEX_STALE", severity: "warning", retryable: true, action: "sync-codefacts" },
72
74
  AC_CONTEXT_STALE: { code: "AC_CONTEXT_STALE", severity: "warning", retryable: true, action: "prepare-task-again" },
73
75
  AC_SCHEMA_INVALID: { code: "AC_SCHEMA_INVALID", severity: "error", retryable: false, action: "repair-model" },