archctx-contracts 0.7.2 → 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.
- package/fixtures/invalid/hook-debt-checkpoint-failed-read.json +54 -0
- package/fixtures/invalid/native-context-invalid-model-summary.json +635 -0
- package/fixtures/invalid/native-context-missing-contract-bytes.json +636 -0
- package/fixtures/invalid/native-context-missing-input-bytes.json +636 -0
- package/fixtures/invalid/native-context-mixed-snapshot.json +637 -0
- package/fixtures/invalid/projection-result-applied-incomplete-coverage.json +78 -0
- package/fixtures/invalid/refactor-decision-open-status.json +38 -0
- package/fixtures/valid/archctx-capabilities.json +3 -3
- package/fixtures/valid/capability-dependencies.json +1 -1
- package/fixtures/valid/docs-anchors.json +2 -1
- package/fixtures/valid/flow-selector-check.json +1 -1
- package/fixtures/valid/hook-debt-checkpoint.json +54 -0
- package/fixtures/valid/native-context-export.json +636 -0
- package/fixtures/valid/product-version-manifest.json +8 -8
- package/fixtures/valid/projection-manifest.json +47 -169
- package/fixtures/valid/projection-result.json +5 -5
- package/fixtures/valid/refactor-decision.json +38 -0
- package/fixtures/valid/runtime-recommendation-show.json +1 -0
- package/fixtures/valid/runtime-refactor-scan.json +6 -0
- package/package.json +1 -1
- package/schemas/repo/refactor-decision.schema.json +218 -0
- package/schemas/runtime/archctx-capabilities.schema.json +2 -2
- package/schemas/runtime/architecture-debt.schema.json +222 -0
- package/schemas/runtime/capability-dependencies.schema.json +10 -6
- package/schemas/runtime/changeset.schema.json +24 -1
- package/schemas/runtime/docs-anchors.schema.json +4 -3
- package/schemas/runtime/hook-debt-checkpoint.schema.json +69 -0
- package/schemas/runtime/native-context-export.schema.json +823 -0
- package/schemas/runtime/native-model-issue.schema.json +18 -0
- package/schemas/runtime/practice-checkpoint.schema.json +1 -0
- package/schemas/runtime/product-version-manifest.schema.json +2 -2
- package/schemas/runtime/projection-apply-readback.schema.json +470 -7
- package/schemas/runtime/projection-apply-recovery.schema.json +2 -2
- package/schemas/runtime/projection-manifest.schema.json +81 -186
- package/schemas/runtime/projection-result.schema.json +132 -5
- package/schemas/runtime/projection-target.schema.json +4 -1
- package/schemas/runtime/recommendation-v3.schema.json +106 -0
- package/schemas/runtime/review-result.schema.json +1 -0
- package/schemas/runtime/runtime-recommendation-show.schema.json +234 -13
- package/schemas/runtime/runtime-refactor-scan.schema.json +171 -0
- package/schemas/runtime/structural-observation-payload.schema.json +106 -0
- package/src/architecture-debt.ts +115 -0
- package/src/changeset-recovery.ts +49 -0
- package/src/docs-anchors.ts +2 -1
- package/src/failure.ts +810 -0
- package/src/index.ts +6 -0
- package/src/ledger.ts +14 -3
- package/src/native-context.ts +279 -0
- package/src/ownership-dependencies.ts +39 -1
- package/src/ports.ts +4 -0
- package/src/practices.ts +2 -0
- package/src/product-version.ts +37 -4
- package/src/projection-manifest.ts +372 -130
- package/src/projection-recovery.ts +111 -0
- package/src/projection.ts +443 -11
- package/src/refactor.ts +525 -2
- package/src/schema.ts +8 -0
- 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
|
-
|
|
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,279 @@
|
|
|
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; path?: 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; optional paths bind parsed records, never IDs. */
|
|
166
|
+
export function nativeContextModelIssues(model: NativeContextModel, schemas: NativeContextSchemas, recordPaths?: ReadonlyMap<NativeModelRecord, string>): NativeModelIssue[] {
|
|
167
|
+
const issues: NativeModelIssue[] = [];
|
|
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
|
+
};
|
|
173
|
+
const resolveSchema = nativeContextSchemaResolver(schemas);
|
|
174
|
+
const ids = new Set<string>();
|
|
175
|
+
for (const [kind, records] of [["node", model.nodes], ["relation", model.relations], ["flow", model.flows]] as const) {
|
|
176
|
+
for (const record of records) {
|
|
177
|
+
const id = typeof record.id === "string" ? record.id : "(undeclared)";
|
|
178
|
+
// JSON Schema validates own data properties. Reject values the shared subset parser
|
|
179
|
+
// cannot represent losslessly as JSON, rather than serializing inherited fields away
|
|
180
|
+
// or coercing non-finite numbers to null. The shared parser itself is unchanged.
|
|
181
|
+
const pending: Array<{ value: unknown; field: string; exit?: boolean }> = [{ value: record, field: "$" }];
|
|
182
|
+
const active = new Set<object>();
|
|
183
|
+
const done = new Set<object>();
|
|
184
|
+
let jsonInvalid = false;
|
|
185
|
+
while (pending.length > 0) {
|
|
186
|
+
const item = pending.pop()!;
|
|
187
|
+
const value = item.value;
|
|
188
|
+
if (item.exit) { active.delete(value as object); done.add(value as object); continue; }
|
|
189
|
+
if (typeof value === "number" && !Number.isFinite(value)) { add(record, item.field, "non-json-number"); jsonInvalid = true; break; }
|
|
190
|
+
if (value === null || typeof value !== "object") continue;
|
|
191
|
+
if (active.has(value)) { add(record, item.field, "non-json-cycle"); jsonInvalid = true; break; }
|
|
192
|
+
if (done.has(value)) continue;
|
|
193
|
+
const prototype = Object.getPrototypeOf(value);
|
|
194
|
+
if (Array.isArray(value) ? prototype !== Array.prototype : prototype !== Object.prototype) { add(record, item.field, "non-json-prototype"); jsonInvalid = true; break; }
|
|
195
|
+
active.add(value); pending.push({ value, field: item.field, exit: true });
|
|
196
|
+
for (const [key, child] of Object.entries(value)) pending.push({ value: child, field: `${item.field}.${key}` });
|
|
197
|
+
}
|
|
198
|
+
if (jsonInvalid) continue;
|
|
199
|
+
const schema = kind === "node" ? schemas.node : kind === "relation" ? schemas.relation : schemas.flow;
|
|
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");
|
|
202
|
+
ids.add(id);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
// Invalid shapes cannot safely participate in reference validation.
|
|
206
|
+
if (issues.length > 0) return issues;
|
|
207
|
+
for (const kind of ["nodes", "relations", "flows"] as const) {
|
|
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");
|
|
210
|
+
for (const [id, path] of Object.entries(model.sourcePaths[kind])) {
|
|
211
|
+
if (!expected.has(id)) add(id, `sourcePaths.${kind}`, "source-path-index-unknown-entity");
|
|
212
|
+
if (!isNativeContextPath(path)) add(expected.get(id) ?? id, `sourcePaths.${kind}`, "path-unsafe");
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
const nodes = new Map(model.nodes.map(node => [node.id as string, node]));
|
|
216
|
+
const checked = new Set<string>();
|
|
217
|
+
for (const node of model.nodes) {
|
|
218
|
+
const chain = new Set<string>();
|
|
219
|
+
let cursor: NativeModelRecord | undefined = node;
|
|
220
|
+
while (cursor) {
|
|
221
|
+
const current = cursor.id as string;
|
|
222
|
+
if (chain.has(current)) { add(node, "parent", "parent-cycle"); break; }
|
|
223
|
+
if (checked.has(current)) break;
|
|
224
|
+
chain.add(current);
|
|
225
|
+
if (cursor.parent === undefined) break;
|
|
226
|
+
const parent = nodes.get(cursor.parent as string);
|
|
227
|
+
if (!parent) { add(cursor, "parent", "parent-node-missing"); break; }
|
|
228
|
+
cursor = parent;
|
|
229
|
+
}
|
|
230
|
+
for (const current of chain) checked.add(current);
|
|
231
|
+
}
|
|
232
|
+
const selectors = new Set<string>();
|
|
233
|
+
for (const node of model.nodes) {
|
|
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;
|
|
235
|
+
for (const path of [...source?.include ?? [], ...source?.exclude ?? []]) if (!isNativeContextPath(path)) add(node, "source", "path-unsafe");
|
|
236
|
+
for (const entry of source?.entrypoints ?? []) {
|
|
237
|
+
if (!isNativeContextPath(entry.path)) add(node, "source.entrypoints.path", "path-unsafe");
|
|
238
|
+
for (const symbol of entry.symbols) {
|
|
239
|
+
for (const sink of symbol.sinks) {
|
|
240
|
+
if (!isNativeContextPath(sink.path)) add(node, "source.entrypoints.symbols.sinks.path", "path-unsafe");
|
|
241
|
+
const selector = JSON.stringify([entry.id, symbol.name, sink.id]);
|
|
242
|
+
if (selectors.has(selector)) add(node, "source.entrypoints.symbols.sinks.id", "selector-ambiguous");
|
|
243
|
+
selectors.add(selector);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
for (const relation of model.relations) {
|
|
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");
|
|
250
|
+
}
|
|
251
|
+
for (const flow of model.flows) {
|
|
252
|
+
if (nodes.get(flow.capabilityId as string)?.kind !== "capability") add(flow, "capabilityId", "flow-capability-missing");
|
|
253
|
+
if (flow.applicability !== "required") continue;
|
|
254
|
+
const participants = flow.participants as Array<{ id: string; nodeId: string }>;
|
|
255
|
+
const participantIds = new Set<string>();
|
|
256
|
+
for (const participant of participants) {
|
|
257
|
+
if (participantIds.has(participant.id)) add(flow, "participants.id", "participant-id-ambiguous");
|
|
258
|
+
participantIds.add(participant.id);
|
|
259
|
+
if (!nodes.has(participant.nodeId)) add(flow, "participants.nodeId", "participant-node-missing");
|
|
260
|
+
}
|
|
261
|
+
const definitions = new Map<string, string>();
|
|
262
|
+
const outcomes = flow.outcomes as Array<{ id: string; steps: NativeModelRecord[]; terminal: { participant: string } }>;
|
|
263
|
+
const outcomeIds = new Set<string>();
|
|
264
|
+
for (const outcome of outcomes) {
|
|
265
|
+
if (outcomeIds.has(outcome.id)) add(flow, "outcomes.id", "outcome-id-ambiguous");
|
|
266
|
+
outcomeIds.add(outcome.id);
|
|
267
|
+
if (!participantIds.has(outcome.terminal.participant)) add(flow, "outcomes.terminal", "terminal-participant-missing");
|
|
268
|
+
}
|
|
269
|
+
for (const step of [...flow.steps as NativeModelRecord[], ...outcomes.flatMap(outcome => outcome.steps)]) {
|
|
270
|
+
if (!participantIds.has(step.from as string) || !participantIds.has(step.to as string)) add(flow, "steps", "step-participant-missing");
|
|
271
|
+
const definition = canonicalize(step);
|
|
272
|
+
if (definitions.has(step.id as string) && definitions.get(step.id as string) !== definition) add(flow, "steps.id", "step-definition-ambiguous");
|
|
273
|
+
definitions.set(step.id as string, definition);
|
|
274
|
+
const selector = step.evidence as { entrypointId: string; sourceSymbol: string; sinkId: string };
|
|
275
|
+
if (!selectors.has(JSON.stringify([selector.entrypointId, selector.sourceSymbol, selector.sinkId]))) add(flow, "steps.evidence", "selector-missing");
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
return issues;
|
|
279
|
+
}
|
|
@@ -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
|
-
/**
|
|
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/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;
|
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;
|
package/src/product-version.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
export const ARCHCONTEXT_PRODUCT_NAME = "archctx";
|
|
2
|
-
export const ARCHCONTEXT_PRODUCT_VERSION = "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
|
-
export const LOCAL_RUNTIME_RPC_SCHEMA_VERSION = "archcontext.runtime-rpc/
|
|
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.
|
|
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.
|
|
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
|
+
}
|