archctx-contracts 0.5.7 → 0.5.9

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.5.7" },
3
+ "package": { "name": "archctx", "version": "0.5.9" },
4
4
  "protocols": {
5
5
  "projectionRequest": "archcontext.projection-request/v1",
6
6
  "projectionResult": "archcontext.projection-result/v2",
@@ -10,5 +10,5 @@
10
10
  "architectureDocs": "archcontext.docs-renderer/v4",
11
11
  "agentContext": "archcontext.agent-context-renderer/v1"
12
12
  },
13
- "features": ["architecture-docs-renderer-v2", "architecture-refresh-signal-v1", "module-statistics-v1", "projection-apply-receipt-v1", "projection-apply-recovery-v1", "projection-protocol-v2", "recommendation-v3", "refactor-assessment-v1", "refactor-resolution-v1"]
13
+ "features": ["architecture-docs-renderer-v2", "architecture-refresh-signal-v1", "module-statistics-v1", "projection-apply-receipt-v1", "projection-apply-recovery-v1", "projection-prior-committed-applies-v1", "projection-protocol-v2", "recommendation-v3", "refactor-assessment-v1", "refactor-resolution-v1"]
14
14
  }
@@ -2,7 +2,7 @@
2
2
  "schemaVersion": "archcontext.product-version-manifest/v1",
3
3
  "product": {
4
4
  "name": "archctx",
5
- "version": "0.5.7",
5
+ "version": "0.5.9",
6
6
  "distribution": "one-package"
7
7
  },
8
8
  "packageManager": "bun@1.4.0",
@@ -12,25 +12,25 @@
12
12
  "surfaces": {
13
13
  "cli": {
14
14
  "packageName": "@archcontext/surfaces",
15
- "version": "0.5.7",
15
+ "version": "0.5.9",
16
16
  "entrypoint": "archctx"
17
17
  },
18
18
  "daemon": {
19
19
  "packageName": "@archcontext/local-runtime",
20
- "version": "0.5.7",
20
+ "version": "0.5.9",
21
21
  "entrypoint": "archctx daemon start",
22
22
  "rpcSchemaVersion": "archcontext.runtime-rpc/v1"
23
23
  },
24
24
  "mcp": {
25
25
  "packageName": "@archcontext/surfaces",
26
- "version": "0.5.7",
26
+ "version": "0.5.9",
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.5.7",
33
+ "contractsPackageVersion": "0.5.9",
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.5.7",
3
+ "version": "0.5.9",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -39,7 +39,7 @@
39
39
  "type": "array",
40
40
  "minItems": 3,
41
41
  "uniqueItems": true,
42
- "items": { "enum": ["architecture-docs-renderer-v2", "architecture-refresh-signal-v1", "module-statistics-v1", "projection-apply-receipt-v1", "projection-apply-recovery-v1", "projection-protocol-v2", "recommendation-v3", "refactor-assessment-v1", "refactor-resolution-v1"] }
42
+ "items": { "enum": ["architecture-docs-renderer-v2", "architecture-refresh-signal-v1", "module-statistics-v1", "projection-apply-receipt-v1", "projection-apply-recovery-v1", "projection-prior-committed-applies-v1", "projection-protocol-v2", "recommendation-v3", "refactor-assessment-v1", "refactor-resolution-v1"] }
43
43
  }
44
44
  }
45
45
  }
@@ -16,6 +16,7 @@
16
16
  "humanActions": { "type": "array", "items": { "$ref": "#/$defs/humanAction" } },
17
17
  "refreshSignals": { "type": "array", "description": "Canonical wire order is by signalId and cross-object receipt/snapshot bindings are enforced by projectionResultInvariantIssues.", "items": { "$ref": "#/$defs/refreshSignal" } },
18
18
  "applyReceipt": { "$ref": "#/$defs/applyIdentity" },
19
+ "priorCommittedApplies": { "type": "array", "minItems": 1, "description": "Omitted, never empty. Canonical wire order is by changeSetId and is enforced by projectionResultInvariantIssues.", "items": { "$ref": "#/$defs/priorCommittedApply" } },
19
20
  "receiptDigest": { "$ref": "#/$defs/digest" }
20
21
  },
21
22
  "allOf": [
@@ -166,6 +167,45 @@
166
167
  "affectedNodeIds": { "type": "array", "minItems": 1, "uniqueItems": true, "items": { "type": "string", "minLength": 1 } }
167
168
  }
168
169
  },
170
+ "priorCommittedApply": {
171
+ "type": "object",
172
+ "additionalProperties": false,
173
+ "required": ["requestId", "changeSetId", "committedAt", "files"],
174
+ "properties": {
175
+ "applyId": { "$ref": "#/$defs/digest" },
176
+ "lookupKey": { "$ref": "#/$defs/digest" },
177
+ "requestId": { "type": "string", "pattern": "^[a-zA-Z0-9_.:-]+$", "minLength": 1 },
178
+ "changeSetId": { "type": "string", "minLength": 1 },
179
+ "committedAt": { "type": "string", "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(?:\\.\\d{1,3})?Z$" },
180
+ "files": {
181
+ "type": "array",
182
+ "minItems": 1,
183
+ "description": "Canonical wire order is by path and is enforced by projectionResultInvariantIssues.",
184
+ "items": {
185
+ "type": "object",
186
+ "additionalProperties": false,
187
+ "required": ["path", "operation", "hash"],
188
+ "properties": {
189
+ "path": {
190
+ "type": "string",
191
+ "minLength": 1,
192
+ "not": { "anyOf": [{ "pattern": "^/" }, { "pattern": "\\\\" }, { "pattern": "(^|/)\\.\\.(/|$)" }] }
193
+ },
194
+ "operation": { "enum": ["delete", "write"] },
195
+ "hash": { "type": "string", "pattern": "^(?:sha256:[a-f0-9]{64}|missing)$" }
196
+ },
197
+ "allOf": [
198
+ {
199
+ "if": { "properties": { "operation": { "const": "delete" } }, "required": ["operation"] },
200
+ "then": { "properties": { "hash": { "const": "missing" } } },
201
+ "else": { "properties": { "hash": { "$ref": "#/$defs/digest" } } }
202
+ }
203
+ ]
204
+ }
205
+ }
206
+ },
207
+ "dependentRequired": { "applyId": ["lookupKey"], "lookupKey": ["applyId"] }
208
+ },
169
209
  "applyIdentity": {
170
210
  "type": "object",
171
211
  "additionalProperties": false,
@@ -1,5 +1,5 @@
1
1
  export const ARCHCONTEXT_PRODUCT_NAME = "archctx";
2
- export const ARCHCONTEXT_PRODUCT_VERSION = "0.5.7";
2
+ export const ARCHCONTEXT_PRODUCT_VERSION = "0.5.9";
3
3
  export const ARCHCONTEXT_PACKAGE_MANAGER = "bun@1.4.0";
4
4
  export const ARCHCONTEXT_NODE_RANGE = ">=22.22 <26";
5
5
  export const LOCAL_RUNTIME_RPC_SCHEMA_VERSION = "archcontext.runtime-rpc/v1";
package/src/projection.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { digestJson, type Json } from "./schema";
1
+ import { digestJson, isRepoRelativePosixPath, type Json } from "./schema";
2
2
 
3
3
  export const PROJECTION_REQUEST_SCHEMA_VERSION = "archcontext.projection-request/v1" as const;
4
4
  export const PROJECTION_RESULT_SCHEMA_VERSION = "archcontext.projection-result/v2" as const;
@@ -14,6 +14,7 @@ export const ARCHITECTURE_DOCS_RENDERER_VERSION = "archcontext.docs-renderer/v4"
14
14
  export const AGENT_CONTEXT_RENDERER_VERSION = "archcontext.agent-context-renderer/v1" as const;
15
15
 
16
16
  export const PROJECTION_MODES = ["check", "plan", "apply", "adopt"] as const;
17
+ export const PROJECTION_PRIOR_COMMITTED_APPLY_OPERATIONS = ["delete", "write"] as const;
17
18
  export const PROJECTION_TARGETS = ["agent-context", "architecture-docs"] as const;
18
19
  export const PROJECTION_RESULT_STATUSES = [
19
20
  "adoption-required",
@@ -61,6 +62,7 @@ export const ARCHCTX_FEATURES = [
61
62
  "module-statistics-v1",
62
63
  "projection-apply-receipt-v1",
63
64
  "projection-apply-recovery-v1",
65
+ "projection-prior-committed-applies-v1",
64
66
  "projection-protocol-v2",
65
67
  "recommendation-v3",
66
68
  "refactor-assessment-v1",
@@ -71,6 +73,7 @@ export type ProjectionMode = (typeof PROJECTION_MODES)[number];
71
73
  export type ProjectionTarget = (typeof PROJECTION_TARGETS)[number];
72
74
  export type ProjectionResultStatus = (typeof PROJECTION_RESULT_STATUSES)[number];
73
75
  export type ProjectionHumanActionReasonCode = (typeof PROJECTION_HUMAN_ACTION_REASON_CODES)[number];
76
+ export type ProjectionPriorCommittedApplyOperation = (typeof PROJECTION_PRIOR_COMMITTED_APPLY_OPERATIONS)[number];
74
77
  export type ArchitectureMajorChangeReasonCode = (typeof ARCHITECTURE_MAJOR_CHANGE_REASON_CODES)[number];
75
78
  export type ArchitectureRefreshTarget = (typeof ARCHITECTURE_REFRESH_TARGETS)[number];
76
79
  export type ArchctxFeature = (typeof ARCHCTX_FEATURES)[number];
@@ -171,6 +174,33 @@ export interface ArchitectureRefreshSignalV1 {
171
174
  projectionReceiptDigest: Sha256Digest;
172
175
  }
173
176
 
177
+ export interface ProjectionPriorCommittedApplyFileV1 {
178
+ path: string;
179
+ operation: ProjectionPriorCommittedApplyOperation;
180
+ /** `digestJson({ body })` of the body this ChangeSet wrote, or `"missing"` for a delete. */
181
+ hash: string;
182
+ }
183
+
184
+ /**
185
+ * A ChangeSet already committed under this request's requestId by an earlier attempt of the same
186
+ * request. The CLI is a short-lived RPC client while the daemon owns the write, so a caller whose
187
+ * process was killed after the commit sees only `status: noop` on retry; this array is the only
188
+ * surface that names what that lost attempt actually wrote.
189
+ */
190
+ export interface ProjectionPriorCommittedApplyV1 {
191
+ /**
192
+ * Present together with `lookupKey` only when the committed ChangeSet also carried a projection
193
+ * apply receipt, which happens only for an accepted-semantic-change apply. A plain drift-repair
194
+ * apply commits without one, and both fields are then absent rather than invented.
195
+ */
196
+ applyId?: Sha256Digest;
197
+ lookupKey?: Sha256Digest;
198
+ requestId: string;
199
+ changeSetId: string;
200
+ committedAt: string;
201
+ files: ProjectionPriorCommittedApplyFileV1[];
202
+ }
203
+
174
204
  export interface ProjectionResultV2 {
175
205
  schemaVersion: typeof PROJECTION_RESULT_SCHEMA_VERSION;
176
206
  requestId: string;
@@ -182,6 +212,8 @@ export interface ProjectionResultV2 {
182
212
  humanActions: ProjectionHumanActionV1[];
183
213
  refreshSignals: ArchitectureRefreshSignalV1[];
184
214
  applyReceipt?: ProjectionApplyIdentityV1;
215
+ /** Omitted, never `[]`, when no earlier attempt of this requestId committed. */
216
+ priorCommittedApplies?: ProjectionPriorCommittedApplyV1[];
185
217
  receiptDigest: Sha256Digest;
186
218
  }
187
219
 
@@ -359,6 +391,9 @@ export function projectionResultInvariantIssues(input: ProjectionResultV2): stri
359
391
  issues.push(`${prefix} unchanged requires equal non-null digests`);
360
392
  }
361
393
  }
394
+ if (input.priorCommittedApplies) {
395
+ issues.push(...projectionPriorCommittedAppliesIssues(input.priorCommittedApplies, input.requestId));
396
+ }
362
397
  const { receiptDigest, ...receiptPayload } = input;
363
398
  if (projectionResultReceiptDigest(receiptPayload) !== receiptDigest) issues.push("receiptDigest must match the canonical projection result payload");
364
399
  for (const [index, signal] of input.refreshSignals.entries()) {
@@ -372,6 +407,40 @@ export function projectionResultInvariantIssues(input: ProjectionResultV2): stri
372
407
  return issues;
373
408
  }
374
409
 
410
+ export function projectionPriorCommittedAppliesIssues(
411
+ input: readonly ProjectionPriorCommittedApplyV1[],
412
+ requestId: string
413
+ ): string[] {
414
+ const issues = sortedUniqueIssues("priorCommittedApplies.changeSetId", input.map((entry) => entry.changeSetId));
415
+ if (input.length === 0) issues.push("priorCommittedApplies must be omitted instead of empty");
416
+ for (const [index, entry] of input.entries()) {
417
+ const prefix = `priorCommittedApplies[${index}]`;
418
+ issues.push(...sortedUniqueIssues(`${prefix}.files.path`, entry.files.map((file) => file.path)));
419
+ if (entry.requestId !== requestId) issues.push(`${prefix}.requestId must match the projection result requestId`);
420
+ if (entry.changeSetId.trim() === "") issues.push(`${prefix}.changeSetId must not be empty`);
421
+ if (!ISO_INSTANT.test(entry.committedAt)) issues.push(`${prefix}.committedAt must be an ISO-8601 UTC instant`);
422
+ if (entry.files.length === 0) issues.push(`${prefix}.files must name at least one committed file`);
423
+ if ((entry.applyId === undefined) !== (entry.lookupKey === undefined)) {
424
+ issues.push(`${prefix}.applyId and lookupKey must be present together or both absent`);
425
+ }
426
+ for (const field of ["applyId", "lookupKey"] as const) {
427
+ const value = entry[field];
428
+ if (value !== undefined && !SHA256_DIGEST.test(value)) issues.push(`${prefix}.${field} must be a SHA-256 digest`);
429
+ }
430
+ for (const [fileIndex, file] of entry.files.entries()) {
431
+ const filePrefix = `${prefix}.files[${fileIndex}]`;
432
+ if (!isRepoRelativePosixPath(file.path)) issues.push(`${filePrefix}.path must be a repository-relative POSIX path`);
433
+ if (!(PROJECTION_PRIOR_COMMITTED_APPLY_OPERATIONS as readonly string[]).includes(file.operation)) {
434
+ issues.push(`${filePrefix}.operation must be a supported committed operation`);
435
+ }
436
+ const expectsMissing = file.operation === "delete";
437
+ if (expectsMissing && file.hash !== "missing") issues.push(`${filePrefix}.hash must be "missing" for a delete`);
438
+ if (!expectsMissing && !SHA256_DIGEST.test(file.hash)) issues.push(`${filePrefix}.hash must be a SHA-256 body digest`);
439
+ }
440
+ }
441
+ return issues;
442
+ }
443
+
375
444
  /**
376
445
  * Computes the accepted projection receipt. Signal back-references are omitted from the
377
446
  * receipt payload to avoid a circular hash; every signal must then bind that receipt via
@@ -615,6 +684,9 @@ export function architectureRefreshSignalInvariantIssues(input: ArchitectureRefr
615
684
  return issues;
616
685
  }
617
686
 
687
+ const SHA256_DIGEST = /^sha256:[a-f0-9]{64}$/;
688
+ const ISO_INSTANT = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?Z$/;
689
+
618
690
  function sortedUniqueIssues(label: string, values: readonly string[]): string[] {
619
691
  const expected = [...new Set(values)].sort();
620
692
  return expected.length === values.length && expected.every((value, index) => value === values[index])