archctx-contracts 0.7.2 → 0.8.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 (56) hide show
  1. package/fixtures/invalid/hook-debt-checkpoint-failed-read.json +54 -0
  2. package/fixtures/invalid/native-context-invalid-model-summary.json +635 -0
  3. package/fixtures/invalid/native-context-missing-contract-bytes.json +636 -0
  4. package/fixtures/invalid/native-context-missing-input-bytes.json +636 -0
  5. package/fixtures/invalid/native-context-mixed-snapshot.json +637 -0
  6. package/fixtures/invalid/projection-result-applied-incomplete-coverage.json +78 -0
  7. package/fixtures/invalid/refactor-decision-open-status.json +38 -0
  8. package/fixtures/valid/archctx-capabilities.json +3 -3
  9. package/fixtures/valid/capability-dependencies.json +1 -1
  10. package/fixtures/valid/docs-anchors.json +2 -1
  11. package/fixtures/valid/flow-selector-check.json +1 -1
  12. package/fixtures/valid/hook-debt-checkpoint.json +54 -0
  13. package/fixtures/valid/native-context-export.json +636 -0
  14. package/fixtures/valid/product-version-manifest.json +8 -8
  15. package/fixtures/valid/projection-manifest.json +47 -169
  16. package/fixtures/valid/projection-result.json +5 -5
  17. package/fixtures/valid/refactor-decision.json +38 -0
  18. package/fixtures/valid/runtime-recommendation-show.json +1 -0
  19. package/fixtures/valid/runtime-refactor-scan.json +6 -0
  20. package/package.json +1 -1
  21. package/schemas/repo/refactor-decision.schema.json +218 -0
  22. package/schemas/runtime/archctx-capabilities.schema.json +2 -2
  23. package/schemas/runtime/architecture-debt.schema.json +222 -0
  24. package/schemas/runtime/capability-dependencies.schema.json +10 -6
  25. package/schemas/runtime/changeset.schema.json +24 -1
  26. package/schemas/runtime/docs-anchors.schema.json +4 -3
  27. package/schemas/runtime/hook-debt-checkpoint.schema.json +69 -0
  28. package/schemas/runtime/native-context-export.schema.json +823 -0
  29. package/schemas/runtime/practice-checkpoint.schema.json +1 -0
  30. package/schemas/runtime/product-version-manifest.schema.json +2 -2
  31. package/schemas/runtime/projection-apply-readback.schema.json +470 -7
  32. package/schemas/runtime/projection-apply-recovery.schema.json +2 -2
  33. package/schemas/runtime/projection-manifest.schema.json +81 -186
  34. package/schemas/runtime/projection-result.schema.json +132 -5
  35. package/schemas/runtime/projection-target.schema.json +4 -1
  36. package/schemas/runtime/recommendation-v3.schema.json +106 -0
  37. package/schemas/runtime/review-result.schema.json +1 -0
  38. package/schemas/runtime/runtime-recommendation-show.schema.json +234 -13
  39. package/schemas/runtime/runtime-refactor-scan.schema.json +171 -0
  40. package/schemas/runtime/structural-observation-payload.schema.json +106 -0
  41. package/src/architecture-debt.ts +115 -0
  42. package/src/changeset-recovery.ts +49 -0
  43. package/src/docs-anchors.ts +2 -1
  44. package/src/failure.ts +776 -0
  45. package/src/index.ts +6 -0
  46. package/src/ledger.ts +14 -3
  47. package/src/native-context.ts +277 -0
  48. package/src/ownership-dependencies.ts +39 -1
  49. package/src/practices.ts +2 -0
  50. package/src/product-version.ts +37 -4
  51. package/src/projection-manifest.ts +372 -130
  52. package/src/projection-recovery.ts +111 -0
  53. package/src/projection.ts +443 -11
  54. package/src/refactor.ts +525 -2
  55. package/src/schema.ts +6 -0
  56. package/src/validator.ts +8 -3
package/src/index.ts CHANGED
@@ -1,15 +1,21 @@
1
1
  export * from "./control-plane-routes";
2
2
  export * from "./architecture";
3
+ export * from "./architecture-debt";
3
4
  export * from "./docs-anchors";
4
5
  export * from "./external-docs";
5
6
  export * from "./github-governance";
6
7
  export * from "./ledger";
8
+ export * from "./native-context";
7
9
  export * from "./ownership-dependencies";
8
10
  export * from "./ports";
9
11
  export * from "./practices";
10
12
  export * from "./projection";
13
+ export * from "./projection-recovery";
11
14
  export * from "./projection-manifest";
12
15
  export * from "./product-version";
13
16
  export * from "./refactor";
14
17
  export * from "./schema";
18
+ export * from "./failure";
15
19
  export * from "./validator";
20
+
21
+ export * from "./changeset-recovery";
package/src/ledger.ts CHANGED
@@ -600,14 +600,12 @@ export interface ProjectionTargetScopeV1 {
600
600
  entityKind?: string;
601
601
  }
602
602
 
603
- export interface ProjectionTargetV1 {
603
+ interface ProjectionTargetBaseV1 {
604
604
  schemaVersion: typeof PROJECTION_TARGET_SCHEMA_VERSION;
605
605
  targetId: string;
606
606
  type: ProjectionTargetType;
607
607
  scope: ProjectionTargetScopeV1;
608
608
  path: string;
609
- ownership: ProjectionTargetOwnership;
610
- generatedRegion: ProjectionTargetGeneratedRegionV1;
611
609
  rendererVersion: string;
612
610
  format: ProjectionTargetFormat;
613
611
  sourceDigest: string;
@@ -615,6 +613,19 @@ export interface ProjectionTargetV1 {
615
613
  extensions?: Record<string, Json>;
616
614
  }
617
615
 
616
+ /**
617
+ * A `mixed` target owns one marked region inside a human-owned document, so it carries the region
618
+ * markers. A `generated` target owns its whole file and carries no marker: the projection manifest
619
+ * row's `outputDigest` is what proves the file is projection output.
620
+ */
621
+ export type ProjectionTargetV1 = ProjectionTargetBaseV1 & (
622
+ | { ownership: Extract<ProjectionTargetOwnership, "mixed">; generatedRegion: ProjectionTargetGeneratedRegionV1 }
623
+ | { ownership: Extract<ProjectionTargetOwnership, "generated">; generatedRegion?: never }
624
+ );
625
+
626
+ /** A target that owns one marked region inside a document. */
627
+ export type ProjectionMixedTargetV1 = Extract<ProjectionTargetV1, { ownership: "mixed" }>;
628
+
618
629
  export interface RecommendationRunV1 {
619
630
  schemaVersion: typeof RECOMMENDATION_RUN_SCHEMA_VERSION;
620
631
  runId: string;
@@ -0,0 +1,277 @@
1
+ import { FailureError, knownFailure, toEnvelope, unclassifiedFailure } from "./failure";
2
+ import { canonicalize, errorEnvelope, type ArchContextErrorCode, type Json, type JsonEnvelope } from "./schema";
3
+ import { jsonSchemaResolver, validateJsonSchema, type JsonSchemaResolver } from "./validator";
4
+
5
+ export const NATIVE_CONTEXT_SCHEMA_VERSION = "archcontext.native-context-export/v1" as const;
6
+ export const NATIVE_CONTEXT_PROFILE = "repo-harness/v1" as const;
7
+ export const NATIVE_CONTEXT_LIMITS = Object.freeze({
8
+ inputFiles: 10_000,
9
+ fileBytes: 8 * 1024 * 1024,
10
+ inputBytes: 32 * 1024 * 1024,
11
+ responseBytes: 64 * 1024 * 1024,
12
+ treeListingBytes: 32 * 1024 * 1024,
13
+ gitTimeoutMs: 10_000,
14
+ batchOverheadBytes: 2 * 1024 * 1024
15
+ });
16
+
17
+ export const NATIVE_CONTEXT_REASONS = [
18
+ "git-worktree-required", "native-context-arguments-invalid", "native-context-profile-invalid",
19
+ "native-context-model-missing", "native-context-model-invalid", "native-context-budget-invalid",
20
+ "native-context-profile-incomplete", "native-context-text-invalid", "native-context-input-ambiguous",
21
+ "native-context-path-unsafe", "native-context-entry-kind-unsupported", "native-context-ref-unsafe",
22
+ "native-context-ref-unavailable", "native-context-blob-unavailable", "native-context-input-missing",
23
+ "native-context-input-unreadable", "native-context-input-changed", "native-document-marker-invalid",
24
+ "native-document-section-ambiguous", "native-context-limit-exceeded", "native-context-git-read-failed"
25
+ ] as const;
26
+ export type NativeContextReasonCode = (typeof NATIVE_CONTEXT_REASONS)[number];
27
+ export type NativeModelRecord = Record<string, Json>;
28
+ export interface NativeContextModel {
29
+ nodes: NativeModelRecord[];
30
+ relations: NativeModelRecord[];
31
+ flows: NativeModelRecord[];
32
+ sourcePaths: { nodes: Record<string, string>; relations: Record<string, string>; flows: Record<string, string> };
33
+ }
34
+ export interface NativeContextInput {
35
+ path: string;
36
+ role: "manifest" | "node" | "relation" | "flow" | "contract" | "document";
37
+ requirement: "required" | "declared" | "optional";
38
+ presence: "present" | "missing";
39
+ mode: "100644" | "100755" | null;
40
+ byteLength: number | null;
41
+ contentDigest: string | null;
42
+ }
43
+ export interface NativeTextRange {
44
+ startByte: number;
45
+ endByte: number;
46
+ startLine: number;
47
+ endLine: number;
48
+ }
49
+ export interface NativeHumanRegion extends NativeTextRange { text: string }
50
+ export interface NativeDocumentSection {
51
+ state: "missing" | "absent" | "empty" | "present";
52
+ heading: { text: string; range: NativeTextRange } | null;
53
+ bodyRanges: NativeTextRange[];
54
+ body: string | null;
55
+ }
56
+ export interface NativeContextDocument {
57
+ path: string;
58
+ targetId: string;
59
+ capabilityNodeIds: string[];
60
+ presence: "present" | "missing";
61
+ markerState: "present" | "absent" | "missing";
62
+ humanRegions: NativeHumanRegion[];
63
+ section3: NativeDocumentSection;
64
+ backlog: NativeDocumentSection;
65
+ }
66
+ export interface NativeCapabilityProfile {
67
+ nodeId: string;
68
+ status: string;
69
+ domain: string;
70
+ name: string;
71
+ modulePath: string;
72
+ contractFiles: { agents: string; claude: string };
73
+ includeAnchors: string[];
74
+ lspProfile: string | null;
75
+ verification: string[] | null;
76
+ directChildNodeIds: string[];
77
+ componentNodeIds: string[];
78
+ }
79
+ export interface NativeContextContract {
80
+ path: string;
81
+ roles: ("agents" | "claude")[];
82
+ capabilityNodeIds: string[];
83
+ presence: "present" | "missing";
84
+ text: string | null;
85
+ byteLength: number | null;
86
+ byteDigest: string | null;
87
+ }
88
+ export type NativeContextSnapshot =
89
+ | { source: "ref"; commitSha: string; treeOid: string; validation: "immutable-tree" }
90
+ | { source: "worktree"; headSha: string; headTreeOid: string; validation: "stable-read" };
91
+ export interface NativeContextExportV1 {
92
+ schemaVersion: typeof NATIVE_CONTEXT_SCHEMA_VERSION;
93
+ profile: typeof NATIVE_CONTEXT_PROFILE;
94
+ snapshot: NativeContextSnapshot;
95
+ inputDigest: string;
96
+ inputs: NativeContextInput[];
97
+ allowedNodeKinds: string[];
98
+ contextBudgetBytes: number;
99
+ model: NativeContextModel;
100
+ capabilityProfiles: NativeCapabilityProfile[];
101
+ contracts: NativeContextContract[];
102
+ documents: NativeContextDocument[];
103
+ }
104
+
105
+ const PATH_REASONS = new Set<NativeContextReasonCode>(["native-context-path-unsafe", "native-context-entry-kind-unsupported", "native-context-ref-unsafe"]);
106
+ const PRECONDITION_REASONS = new Set<NativeContextReasonCode>(["native-context-ref-unavailable", "native-context-blob-unavailable", "native-context-input-missing", "native-context-input-unreadable", "native-context-input-changed", "native-context-limit-exceeded", "native-context-git-read-failed"]);
107
+
108
+ /** Compatible native class, backed by the single strict failure authority. */
109
+ export class NativeContextError extends FailureError {
110
+ constructor(reasonCode: NativeContextReasonCode, details: Record<string, Json> = {}) {
111
+ let failure;
112
+ try {
113
+ const code: ArchContextErrorCode = reasonCode === "git-worktree-required" ? "AC_REPO_NOT_FOUND"
114
+ : reasonCode === "native-context-model-missing" ? "AC_MODEL_ADOPTION_REQUIRED"
115
+ : PATH_REASONS.has(reasonCode) ? "AC_PATH_DENIED"
116
+ : PRECONDITION_REASONS.has(reasonCode) ? "AC_PRECONDITION_FAILED" : "AC_SCHEMA_INVALID";
117
+ failure = knownFailure(code, reasonCode, details);
118
+ } catch { failure = unclassifiedFailure("failure-metadata"); }
119
+ super(failure.code, failure.reasonCode, failure.details, failure);
120
+ this.name = "NativeContextError";
121
+ }
122
+ }
123
+
124
+ export function nativeContextErrorEnvelope(error: NativeContextError): JsonEnvelope {
125
+ return toEnvelope("export.native", error, "native-export") as JsonEnvelope;
126
+ }
127
+
128
+ export function assertNativeContextLimit(limit: keyof typeof NATIVE_CONTEXT_LIMITS, observed: number): void {
129
+ if (!Number.isSafeInteger(observed) || observed < 0 || observed > NATIVE_CONTEXT_LIMITS[limit]) {
130
+ throw new NativeContextError("native-context-limit-exceeded", { limit, maximum: NATIVE_CONTEXT_LIMITS[limit], observed });
131
+ }
132
+ }
133
+
134
+ /** A lexical contract. It does not expand globs, resolve ownership or open any path. */
135
+ export function isNativeContextPath(path: string): boolean {
136
+ return path.length > 0 && !/^[A-Za-z]:/.test(path) && !path.startsWith("/")
137
+ && Buffer.from(path, "utf8").toString("utf8") === path
138
+ && !/[\\\u0000-\u001f\u007f-\u009f]/.test(path)
139
+ && path.split("/").every(segment => segment !== "" && segment !== "." && segment !== "..");
140
+ }
141
+
142
+ export function assertNativeContextPath(path: string): void {
143
+ if (!isNativeContextPath(path)) throw new NativeContextError("native-context-path-unsafe", { path });
144
+ }
145
+
146
+ export function nativeByteCompare(left: string, right: string): number {
147
+ return Buffer.compare(Buffer.from(left, "utf8"), Buffer.from(right, "utf8"));
148
+ }
149
+
150
+ type SchemaDocument = Parameters<typeof validateJsonSchema>[0];
151
+ export interface NativeContextSchemas { node: SchemaDocument; relation: SchemaDocument; flow: SchemaDocument }
152
+ export interface NativeModelIssue { entityId: string; field: string; invariant: string }
153
+
154
+ /** Fixed canonical documents are supplied by the bundled reader, never by repository input. */
155
+ export function nativeContextSchemaResolver(schemas: NativeContextSchemas): JsonSchemaResolver {
156
+ return jsonSchemaResolver([schemas.node, schemas.relation, schemas.flow]);
157
+ }
158
+
159
+ export function nativeAllowedNodeKinds(schemas: NativeContextSchemas): string[] {
160
+ const kinds = schemas.node.properties?.kind?.enum;
161
+ if (!kinds || kinds.some(kind => typeof kind !== "string")) throw new Error("canonical-node-kind-enum-unavailable");
162
+ return [...kinds] as string[];
163
+ }
164
+
165
+ /** Complete native shape/reference validation; no filesystem, symbols, runtime or mutation. */
166
+ export function nativeContextModelIssues(model: NativeContextModel, schemas: NativeContextSchemas): NativeModelIssue[] {
167
+ const issues: NativeModelIssue[] = [];
168
+ const add = (entityId: string, field: string, invariant: string) => issues.push({ entityId, field, invariant });
169
+ const resolveSchema = nativeContextSchemaResolver(schemas);
170
+ const ids = new Set<string>();
171
+ for (const [kind, records] of [["node", model.nodes], ["relation", model.relations], ["flow", model.flows]] as const) {
172
+ for (const record of records) {
173
+ const id = typeof record.id === "string" ? record.id : "(undeclared)";
174
+ // JSON Schema validates own data properties. Reject values the shared subset parser
175
+ // cannot represent losslessly as JSON, rather than serializing inherited fields away
176
+ // or coercing non-finite numbers to null. The shared parser itself is unchanged.
177
+ const pending: Array<{ value: unknown; field: string; exit?: boolean }> = [{ value: record, field: "$" }];
178
+ const active = new Set<object>();
179
+ const done = new Set<object>();
180
+ let jsonInvalid = false;
181
+ while (pending.length > 0) {
182
+ const item = pending.pop()!;
183
+ const value = item.value;
184
+ 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; }
186
+ if (value === null || typeof value !== "object") continue;
187
+ if (active.has(value)) { add(id, item.field, "non-json-cycle"); jsonInvalid = true; break; }
188
+ if (done.has(value)) continue;
189
+ 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; }
191
+ active.add(value); pending.push({ value, field: item.field, exit: true });
192
+ for (const [key, child] of Object.entries(value)) pending.push({ value: child, field: `${item.field}.${key}` });
193
+ }
194
+ if (jsonInvalid) continue;
195
+ 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");
198
+ ids.add(id);
199
+ }
200
+ }
201
+ // Invalid shapes cannot safely participate in reference validation.
202
+ if (issues.length > 0) return issues;
203
+ 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");
206
+ for (const [id, path] of Object.entries(model.sourcePaths[kind])) {
207
+ if (!expected.has(id)) add(id, `sourcePaths.${kind}`, "source-path-index-unknown-entity");
208
+ if (!isNativeContextPath(path)) add(id, `sourcePaths.${kind}`, "path-unsafe");
209
+ }
210
+ }
211
+ const nodes = new Map(model.nodes.map(node => [node.id as string, node]));
212
+ const checked = new Set<string>();
213
+ for (const node of model.nodes) {
214
+ const id = node.id as string;
215
+ const chain = new Set<string>();
216
+ let cursor: NativeModelRecord | undefined = node;
217
+ while (cursor) {
218
+ const current = cursor.id as string;
219
+ if (chain.has(current)) { add(id, "parent", "parent-cycle"); break; }
220
+ if (checked.has(current)) break;
221
+ chain.add(current);
222
+ if (cursor.parent === undefined) break;
223
+ const parent = nodes.get(cursor.parent as string);
224
+ if (!parent) { add(current, "parent", "parent-node-missing"); break; }
225
+ cursor = parent;
226
+ }
227
+ for (const current of chain) checked.add(current);
228
+ }
229
+ const selectors = new Set<string>();
230
+ for (const node of model.nodes) {
231
+ 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");
233
+ for (const entry of source?.entrypoints ?? []) {
234
+ if (!isNativeContextPath(entry.path)) add(node.id as string, "source.entrypoints.path", "path-unsafe");
235
+ for (const symbol of entry.symbols) {
236
+ for (const sink of symbol.sinks) {
237
+ if (!isNativeContextPath(sink.path)) add(node.id as string, "source.entrypoints.symbols.sinks.path", "path-unsafe");
238
+ 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");
240
+ selectors.add(selector);
241
+ }
242
+ }
243
+ }
244
+ }
245
+ 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");
247
+ }
248
+ 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");
251
+ if (flow.applicability !== "required") continue;
252
+ const participants = flow.participants as Array<{ id: string; nodeId: string }>;
253
+ const participantIds = new Set<string>();
254
+ for (const participant of participants) {
255
+ if (participantIds.has(participant.id)) add(id, "participants.id", "participant-id-ambiguous");
256
+ participantIds.add(participant.id);
257
+ if (!nodes.has(participant.nodeId)) add(id, "participants.nodeId", "participant-node-missing");
258
+ }
259
+ const definitions = new Map<string, string>();
260
+ const outcomes = flow.outcomes as Array<{ id: string; steps: NativeModelRecord[]; terminal: { participant: string } }>;
261
+ const outcomeIds = new Set<string>();
262
+ for (const outcome of outcomes) {
263
+ if (outcomeIds.has(outcome.id)) add(id, "outcomes.id", "outcome-id-ambiguous");
264
+ outcomeIds.add(outcome.id);
265
+ if (!participantIds.has(outcome.terminal.participant)) add(id, "outcomes.terminal", "terminal-participant-missing");
266
+ }
267
+ 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");
269
+ 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");
271
+ definitions.set(step.id as string, definition);
272
+ 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");
274
+ }
275
+ }
276
+ return issues;
277
+ }
@@ -73,6 +73,27 @@ export const CAPABILITY_DEPENDENCIES_SCHEMA_VERSION = "archcontext.capability-de
73
73
  export const CAPABILITY_DEPENDENCIES_EDGE_LIMIT = 200;
74
74
  /** The CodeGraph adapter has no bounded repository-wide call-edge read, so calls are not counted. */
75
75
  export const CAPABILITY_DEPENDENCIES_CALLS_UNAVAILABLE_REASON = "call-edges-not-exposed" as const;
76
+ /**
77
+ * The relation kinds that declare an import dependency: every `archcontext.relation/v1` kind in
78
+ * which the source uses the target. `deployed-on` places a node on infrastructure and declares no
79
+ * code dependency. A kind added to the relation schema later declares nothing until it is listed.
80
+ *
81
+ * The rule: an import from a file owned by capability A into a file owned by capability B, A != B,
82
+ * is declared when the model has a relation of one of these kinds whose `source` rolls up to A and
83
+ * whose `target` rolls up to B. A node rolls up to itself when it is a capability, else to the
84
+ * nearest `kind: capability` ancestor on its `parent` chain, exactly like a file owner. Direction
85
+ * matters: a relation B -> A does not declare an import A -> B. A relation endpoint that rolls up
86
+ * to no capability declares nothing.
87
+ */
88
+ export const CAPABILITY_DEPENDENCY_DECLARING_RELATION_KINDS = [
89
+ "calls",
90
+ "depends-on",
91
+ "implements",
92
+ "publishes",
93
+ "reads",
94
+ "subscribes",
95
+ "writes"
96
+ ] as const;
76
97
 
77
98
  export interface CapabilityDependencyCellV1 {
78
99
  /** Owning capability of the importing file. */
@@ -83,6 +104,15 @@ export interface CapabilityDependencyCellV1 {
83
104
  imports: number;
84
105
  /** Always `null` in v1: call edges are not read (see `calls`). */
85
106
  calls: null;
107
+ /**
108
+ * Whether the model declares this dependency (rule: CAPABILITY_DEPENDENCY_DECLARING_RELATION_KINDS).
109
+ * A cell with `from === to` stays inside one capability and needs no relation: it is always
110
+ * declared, with no relation ids. A cross-capability cell is declared exactly when
111
+ * `relationIds` is non-empty.
112
+ */
113
+ declared: boolean;
114
+ /** Ids of the relations that declare a cross-capability cell, sorted. Always empty when `from === to`. */
115
+ relationIds: string[];
86
116
  }
87
117
 
88
118
  export interface CapabilityDependencyEdgeV1 {
@@ -102,8 +132,16 @@ export interface CapabilityDependenciesV1 {
102
132
  /** Non-zero cells only, sorted by `from` then `to`. A cell with `from === to` counts edges inside one capability. */
103
133
  matrix: CapabilityDependencyCellV1[];
104
134
  imports: {
135
+ /**
136
+ * `archctx deps` never answers `unknown`: without code facts that attest to the measured tree it
137
+ * refuses with `AC_CODE_FACTS_UNAVAILABLE`, so an empty matrix always means no attributed edge.
138
+ */
105
139
  coverage: EvidenceCoverageLevelV2;
106
- /** `code-facts-unavailable`, `code-facts-stale`, `code-facts-import-limit`, `code-facts-truncated`. */
140
+ /**
141
+ * `code-facts-import-limit` or `code-facts-truncated` on `archctx deps` output. An index that
142
+ * is missing or has pending changes measures as `code-facts-unavailable`, which `deps` turns
143
+ * into the `AC_CODE_FACTS_UNAVAILABLE` refusal.
144
+ */
107
145
  reasonCodes: string[];
108
146
  /** True when the index read is not the whole population for a reason other than the limit. */
109
147
  truncated: boolean;
package/src/practices.ts CHANGED
@@ -389,6 +389,8 @@ export interface PracticeDeltaV1 {
389
389
 
390
390
  export interface PracticeCheckpointResultV1 {
391
391
  schemaVersion: "archcontext.practice-checkpoint/v1";
392
+ /** Runtime checkpoints attach the same read-only debt measurement as `archctx debt`. */
393
+ debt?: import("./architecture-debt").ArchitectureDebtReportV1;
392
394
  taskSessionId: string;
393
395
  event: PracticeCheckpointEvent;
394
396
  headSha: string;
@@ -1,8 +1,8 @@
1
1
  export const ARCHCONTEXT_PRODUCT_NAME = "archctx";
2
- export const ARCHCONTEXT_PRODUCT_VERSION = "0.7.2";
2
+ export const ARCHCONTEXT_PRODUCT_VERSION = "0.8.0";
3
3
  export const ARCHCONTEXT_PACKAGE_MANAGER = "bun@1.4.3";
4
4
  export const ARCHCONTEXT_NODE_RANGE = ">=22.22 <26";
5
- export const LOCAL_RUNTIME_RPC_SCHEMA_VERSION = "archcontext.runtime-rpc/v1";
5
+ export const LOCAL_RUNTIME_RPC_SCHEMA_VERSION = "archcontext.runtime-rpc/v2";
6
6
  export const ARCHCONTEXT_SCHEMA_SET_VERSION = "2026-07-11.explorer-view-v2";
7
7
 
8
8
  export interface ProductVersionManifest {
@@ -41,7 +41,7 @@ export interface ProductVersionManifest {
41
41
  sqliteMigrations: string;
42
42
  codeGraph: {
43
43
  packageName: "@colbymchenry/codegraph";
44
- requiredVersion: "1.6.1";
44
+ requiredVersion: "1.6.2";
45
45
  adapter: "codegraph-cli";
46
46
  };
47
47
  };
@@ -100,9 +100,42 @@ export function productVersionManifest(): ProductVersionManifest {
100
100
  sqliteMigrations: "0001_runtime_state..0012_explorer_projection_index",
101
101
  codeGraph: {
102
102
  packageName: "@colbymchenry/codegraph",
103
- requiredVersion: "1.6.1",
103
+ requiredVersion: "1.6.2",
104
104
  adapter: "codegraph-cli"
105
105
  }
106
106
  }
107
107
  };
108
108
  }
109
+
110
+ /**
111
+ * Whether a Node.js version (`process.versions.node`) satisfies `range`, a space-separated list of
112
+ * `>=`, `>`, `<=`, `<` or `=` comparators over `major[.minor[.patch]]` (missing parts are 0), the
113
+ * only form `ARCHCONTEXT_NODE_RANGE` uses. A version that does not start with `major.minor.patch`
114
+ * does not satisfy any range; a prerelease suffix is ignored, so `26.0.0-pre` counts as `26.0.0`
115
+ * and fails `<26`. A comparator outside that form throws: the range is product metadata, not input.
116
+ */
117
+ export function nodeVersionSatisfiesRange(nodeVersion: string, range: string = ARCHCONTEXT_NODE_RANGE): boolean {
118
+ const comparators = range.trim().split(/\s+/).map((token) => {
119
+ const match = /^(>=|<=|>|<|=)?(\d+)(?:\.(\d+))?(?:\.(\d+))?$/.exec(token);
120
+ if (!match) throw new Error(`unsupported node engine comparator: ${token}`);
121
+ return { operator: match[1] ?? "=", version: [Number(match[2]), Number(match[3] ?? 0), Number(match[4] ?? 0)] };
122
+ });
123
+ const parsed = /^v?(\d+)\.(\d+)\.(\d+)/.exec(nodeVersion);
124
+ if (!parsed) return false;
125
+ const version = [Number(parsed[1]), Number(parsed[2]), Number(parsed[3])];
126
+ return comparators.every(({ operator, version: bound }) => {
127
+ const order = compareVersionTriples(version, bound);
128
+ if (operator === ">=") return order >= 0;
129
+ if (operator === ">") return order > 0;
130
+ if (operator === "<=") return order <= 0;
131
+ if (operator === "<") return order < 0;
132
+ return order === 0;
133
+ });
134
+ }
135
+
136
+ function compareVersionTriples(left: number[], right: number[]): number {
137
+ for (let index = 0; index < 3; index += 1) {
138
+ if (left[index] !== right[index]) return left[index] - right[index];
139
+ }
140
+ return 0;
141
+ }