archctx-contracts 0.6.3 → 0.7.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/recommendation-v3-structural-before-evidence.json +59 -0
- package/fixtures/valid/archctx-capabilities.json +3 -3
- package/fixtures/valid/capability-dependencies.json +35 -0
- package/fixtures/valid/docs-anchors.json +105 -0
- package/fixtures/valid/flow-selector-check.json +50 -0
- package/fixtures/valid/product-version-manifest.json +6 -6
- package/fixtures/valid/projection-manifest.json +175 -0
- package/fixtures/valid/projection-result.json +5 -5
- package/fixtures/valid/recommendation-v3.json +69 -0
- package/fixtures/valid/runtime-recommendation-show.json +312 -0
- package/fixtures/valid/runtime-refactor-scan.json +543 -0
- package/package.json +1 -1
- package/schemas/runtime/archctx-capabilities.schema.json +7 -2
- package/schemas/runtime/architecture-refresh-signal.schema.json +27 -0
- package/schemas/runtime/capability-dependencies.schema.json +116 -0
- package/schemas/runtime/changeset.schema.json +42 -16
- package/schemas/runtime/docs-anchors.schema.json +143 -0
- package/schemas/runtime/flow-selector-check.schema.json +109 -0
- package/schemas/runtime/projection-apply-readback.schema.json +351 -10
- package/schemas/runtime/projection-apply-recovery.schema.json +24 -3
- package/schemas/runtime/projection-manifest.schema.json +491 -0
- package/schemas/runtime/projection-request.schema.json +11 -2
- package/schemas/runtime/projection-result.schema.json +108 -8
- package/schemas/runtime/recommendation-v3.schema.json +1167 -0
- package/schemas/runtime/runtime-recommendation-show.schema.json +2463 -0
- package/schemas/runtime/runtime-refactor-scan.schema.json +2508 -0
- package/schemas/runtime/structural-observation-payload.schema.json +579 -0
- package/src/architecture.ts +6 -3
- package/src/docs-anchors.ts +126 -0
- package/src/index.ts +3 -0
- package/src/ledger.ts +26 -10
- package/src/ownership-dependencies.ts +129 -0
- package/src/product-version.ts +2 -2
- package/src/projection-manifest.ts +346 -0
- package/src/projection.ts +406 -6
- package/src/refactor.ts +305 -1
- package/src/schema.ts +20 -4
- package/src/validator.ts +87 -15
package/src/projection.ts
CHANGED
|
@@ -11,7 +11,7 @@ export const PROJECTION_APPLY_RECOVERY_RESULT_SCHEMA_VERSION = "archcontext.proj
|
|
|
11
11
|
export const PROJECTION_APPLY_READBACK_RESULT_SCHEMA_VERSION = "archcontext.projection-apply-readback-result/v1" as const;
|
|
12
12
|
export const ARCHITECTURE_REFRESH_SIGNAL_SCHEMA_VERSION = "archcontext.architecture-refresh-signal/v1" as const;
|
|
13
13
|
export const ARCHCTX_CAPABILITIES_SCHEMA_VERSION = "archcontext.capabilities/v1" as const;
|
|
14
|
-
export const ARCHITECTURE_DOCS_RENDERER_VERSION = "archcontext.docs-renderer/
|
|
14
|
+
export const ARCHITECTURE_DOCS_RENDERER_VERSION = "archcontext.docs-renderer/v5" as const;
|
|
15
15
|
export const AGENT_CONTEXT_RENDERER_VERSION = "archcontext.agent-context-renderer/v1" as const;
|
|
16
16
|
|
|
17
17
|
export const PROJECTION_MODES = ["check", "plan", "apply", "adopt"] as const;
|
|
@@ -31,6 +31,7 @@ export const PROJECTION_RESULT_STATUSES = [
|
|
|
31
31
|
export const PROJECTION_HUMAN_ACTION_REASON_CODES = [
|
|
32
32
|
"adoption-required",
|
|
33
33
|
"manual-region-conflict",
|
|
34
|
+
"orphaned-document-review",
|
|
34
35
|
"target-collision",
|
|
35
36
|
"unprovable-required-flow",
|
|
36
37
|
"unresolved-major-change"
|
|
@@ -58,20 +59,141 @@ export const ARCHITECTURE_REFRESH_TARGETS = [
|
|
|
58
59
|
"capability-index"
|
|
59
60
|
] as const;
|
|
60
61
|
export const ARCHCTX_FEATURES = [
|
|
62
|
+
"architecture-change-capabilities-v1",
|
|
61
63
|
"architecture-docs-renderer-v2",
|
|
62
64
|
"architecture-refresh-signal-v1",
|
|
65
|
+
"capability-dependencies-v1",
|
|
66
|
+
"changeset-entity-operations-v1",
|
|
67
|
+
"changeset-ownership-delta-v1",
|
|
68
|
+
"code-facts-unavailable-error-v1",
|
|
69
|
+
"docs-anchors-v1",
|
|
70
|
+
"flow-selector-check-v1",
|
|
63
71
|
"module-statistics-v1",
|
|
64
72
|
"projection-apply-readback-v1",
|
|
65
73
|
"projection-apply-receipt-v1",
|
|
66
74
|
"projection-apply-recovery-v1",
|
|
75
|
+
"projection-apply-replay-v1",
|
|
76
|
+
"projection-check-freshness-v1",
|
|
77
|
+
"projection-manifest-contract-v2",
|
|
78
|
+
"projection-observed-major-change-acceptance-v1",
|
|
79
|
+
"projection-orphan-review-v1",
|
|
80
|
+
"projection-preview-v1",
|
|
67
81
|
"projection-prior-committed-applies-v1",
|
|
68
82
|
"projection-protocol-v2",
|
|
83
|
+
"recommendation-scan-decision-v1",
|
|
69
84
|
"recommendation-v3",
|
|
70
85
|
"refactor-assessment-v1",
|
|
71
|
-
"refactor-
|
|
86
|
+
"refactor-observation-evidence-v1",
|
|
87
|
+
"refactor-resolution-v1",
|
|
88
|
+
"refactor-scan-limits-v1",
|
|
89
|
+
"refactor-scan-untracked-count-v1"
|
|
90
|
+
] as const;
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* The closed set of semantic facets a capability's semantic state digests. Each facet maps to the
|
|
94
|
+
* major-change reason code it raises when its digest moves.
|
|
95
|
+
*/
|
|
96
|
+
export const ARCHITECTURE_SEMANTIC_FACETS = [
|
|
97
|
+
"constraints",
|
|
98
|
+
"entrypoints",
|
|
99
|
+
"interfaces",
|
|
100
|
+
"lifecycle",
|
|
101
|
+
"names",
|
|
102
|
+
"ownership",
|
|
103
|
+
"placement",
|
|
104
|
+
"relations",
|
|
105
|
+
"responsibilities",
|
|
106
|
+
"riskBoundaries"
|
|
107
|
+
] as const;
|
|
108
|
+
export const ARCHITECTURE_PROOF_P1_STATUSES = ["proven", "unprovable"] as const;
|
|
109
|
+
export const ARCHITECTURE_PROOF_P2_STATUSES = ["not-applicable", "proven", "unprovable"] as const;
|
|
110
|
+
|
|
111
|
+
export const ARCHITECTURE_FLOW_SELECTOR_CHECK_SCHEMA_VERSION = "archcontext.flow-selector-check/v1" as const;
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* The proof status of one declared entrypoint→sink selector, as the P2 flow proof reads its
|
|
115
|
+
* evidence. Only `proven` can bind a flow step. `no-direct-call`: no `calls` edge from the source
|
|
116
|
+
* to the sink. `only-references`: no proving call, but the source references the sink by value.
|
|
117
|
+
* `false-builtin-call`: the only calls carry a runtime built-in name and the source file does not
|
|
118
|
+
* import the sink file. `ambiguous-symbol`: the source or the sink matches more than one indexed
|
|
119
|
+
* symbol. `evidence-missing`: no evidence was measured (for example, no CodeGraph index).
|
|
120
|
+
* `evidence-truncated`: the evidence producer could not enumerate every edge.
|
|
121
|
+
*/
|
|
122
|
+
export const ARCHITECTURE_FLOW_SELECTOR_PROOF_STATUSES = [
|
|
123
|
+
"ambiguous-symbol",
|
|
124
|
+
"evidence-missing",
|
|
125
|
+
"evidence-truncated",
|
|
126
|
+
"false-builtin-call",
|
|
127
|
+
"no-direct-call",
|
|
128
|
+
"only-references",
|
|
129
|
+
"proven"
|
|
130
|
+
] as const;
|
|
131
|
+
export type ArchitectureFlowSelectorProofStatus = typeof ARCHITECTURE_FLOW_SELECTOR_PROOF_STATUSES[number];
|
|
132
|
+
|
|
133
|
+
export interface ArchitectureFlowSelectorSiteV1 {
|
|
134
|
+
path: string;
|
|
135
|
+
line?: number;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export interface ArchitectureFlowSelectorCheckEntryV1 {
|
|
139
|
+
nodeId: string;
|
|
140
|
+
entrypointId: string;
|
|
141
|
+
sourcePath: string;
|
|
142
|
+
sourceSymbol: string;
|
|
143
|
+
sinkId: string;
|
|
144
|
+
sinkPath: string;
|
|
145
|
+
sinkSymbol: string;
|
|
146
|
+
status: ArchitectureFlowSelectorProofStatus;
|
|
147
|
+
callSites: ArchitectureFlowSelectorSiteV1[];
|
|
148
|
+
referenceSites: ArchitectureFlowSelectorSiteV1[];
|
|
149
|
+
rejectedBuiltinCallSites: ArchitectureFlowSelectorSiteV1[];
|
|
150
|
+
/** The required-flow steps that name this selector as evidence. Empty for an unused selector. */
|
|
151
|
+
flowSteps: Array<{ flowId: string; stepId: string }>;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export interface ArchitectureFlowSelectorCheckCapabilityV1 {
|
|
155
|
+
capabilityId: string;
|
|
156
|
+
/** The same P2 status a projection records for this capability. */
|
|
157
|
+
p2Status: typeof ARCHITECTURE_PROOF_P2_STATUSES[number];
|
|
158
|
+
diagnostics: Array<{ code: string; flowId?: string; stepId?: string; detail: string }>;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* `archctx flows check`: the P2 flow proof without rendering. Read-only. `ok` is true when no
|
|
163
|
+
* capability that declares a flow has an unprovable P2 proof.
|
|
164
|
+
*/
|
|
165
|
+
export interface ArchitectureFlowSelectorCheckV1 {
|
|
166
|
+
schemaVersion: typeof ARCHITECTURE_FLOW_SELECTOR_CHECK_SCHEMA_VERSION;
|
|
167
|
+
ok: boolean;
|
|
168
|
+
codeGraph: { availability: "ready" | "unavailable"; version: string };
|
|
169
|
+
summary: { selectors: number; proven: number; unproven: number; usedUnproven: number };
|
|
170
|
+
selectors: ArchitectureFlowSelectorCheckEntryV1[];
|
|
171
|
+
capabilities: ArchitectureFlowSelectorCheckCapabilityV1[];
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** `files[].preview.format`: a `create` carries the rendered body, an `update` or `delete` a unified diff. */
|
|
175
|
+
export const PROJECTION_FILE_PREVIEW_FORMATS = ["body", "unified-diff"] as const;
|
|
176
|
+
/** Upper bound, in UTF-8 bytes, on one `files[].preview.content`. */
|
|
177
|
+
export const PROJECTION_FILE_PREVIEW_MAX_BYTES = 65_536;
|
|
178
|
+
/** Upper bound, in UTF-8 bytes, on the sum of every `files[].preview.content` of one result. */
|
|
179
|
+
export const PROJECTION_PREVIEW_TOTAL_MAX_BYTES = 1_048_576;
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Why a projection is not fresh. Shared by the core freshness evaluator and the `check` result's
|
|
183
|
+
* `freshness` field so the two can never disagree.
|
|
184
|
+
*/
|
|
185
|
+
export const PROJECTION_FRESHNESS_REASON_CODES = [
|
|
186
|
+
"projection-manifest-missing",
|
|
187
|
+
"projection-manifest-unreadable",
|
|
188
|
+
"projection-snapshot-provenance-missing",
|
|
189
|
+
"projection-source-changed-since-stamp",
|
|
190
|
+
"projection-source-stamp-invalid",
|
|
191
|
+
"projection-source-stamp-missing",
|
|
192
|
+
"projection-source-tree-digest-mismatch"
|
|
72
193
|
] as const;
|
|
73
194
|
|
|
74
195
|
export type ProjectionMode = (typeof PROJECTION_MODES)[number];
|
|
196
|
+
export type ProjectionFreshnessReasonCode = (typeof PROJECTION_FRESHNESS_REASON_CODES)[number];
|
|
75
197
|
export type ProjectionTarget = (typeof PROJECTION_TARGETS)[number];
|
|
76
198
|
export type ProjectionResultStatus = (typeof PROJECTION_RESULT_STATUSES)[number];
|
|
77
199
|
export type ProjectionHumanActionReasonCode = (typeof PROJECTION_HUMAN_ACTION_REASON_CODES)[number];
|
|
@@ -79,8 +201,47 @@ export type ProjectionPriorCommittedApplyOperation = (typeof PROJECTION_PRIOR_CO
|
|
|
79
201
|
export type ArchitectureMajorChangeReasonCode = (typeof ARCHITECTURE_MAJOR_CHANGE_REASON_CODES)[number];
|
|
80
202
|
export type ArchitectureRefreshTarget = (typeof ARCHITECTURE_REFRESH_TARGETS)[number];
|
|
81
203
|
export type ArchctxFeature = (typeof ARCHCTX_FEATURES)[number];
|
|
204
|
+
export type ArchitectureSemanticFacet = (typeof ARCHITECTURE_SEMANTIC_FACETS)[number];
|
|
205
|
+
export type ProjectionFilePreviewFormat = (typeof PROJECTION_FILE_PREVIEW_FORMATS)[number];
|
|
82
206
|
export type Sha256Digest = `sha256:${string}`;
|
|
83
207
|
|
|
208
|
+
/** One capability's P1/P2 proof status, as its semantic state records it. */
|
|
209
|
+
export interface ArchitectureCapabilityProofStatusV1 {
|
|
210
|
+
p1: (typeof ARCHITECTURE_PROOF_P1_STATUSES)[number];
|
|
211
|
+
p2: (typeof ARCHITECTURE_PROOF_P2_STATUSES)[number];
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Why one capability is part of a major change: the reason codes and semantic facets that moved for
|
|
216
|
+
* it between the committed semantic baseline and this run. `proofStatusBefore` is null when the
|
|
217
|
+
* baseline has no such capability (or no baseline exists); `proofStatusAfter` is null when the
|
|
218
|
+
* capability was removed. A capability present on one side only lists no changed facets. A
|
|
219
|
+
* capability listed only because its current proof is unprovable, while another capability carries
|
|
220
|
+
* the observed change, lists no reason codes.
|
|
221
|
+
*/
|
|
222
|
+
export interface ArchitectureCapabilityChangeV1 {
|
|
223
|
+
capabilityId: string;
|
|
224
|
+
reasonCodes: ArchitectureMajorChangeReasonCode[];
|
|
225
|
+
changedFacets: ArchitectureSemanticFacet[];
|
|
226
|
+
proofStatusBefore: ArchitectureCapabilityProofStatusV1 | null;
|
|
227
|
+
proofStatusAfter: ArchitectureCapabilityProofStatusV1 | null;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* What a `plan` would write to one file, returned in the response only and never persisted: the
|
|
232
|
+
* rendered body of a `create`, or a unified diff (current bytes to rendered bytes) of an `update` or
|
|
233
|
+
* `delete`. `content` is cut at a line boundary to at most PROJECTION_FILE_PREVIEW_MAX_BYTES UTF-8
|
|
234
|
+
* bytes, and to whatever remains of PROJECTION_PREVIEW_TOTAL_MAX_BYTES across the result, in path
|
|
235
|
+
* order; `byteLength` is the UTF-8 length of the complete content and `truncated` says whether
|
|
236
|
+
* `content` is shorter.
|
|
237
|
+
*/
|
|
238
|
+
export interface ProjectionFilePreviewV1 {
|
|
239
|
+
format: ProjectionFilePreviewFormat;
|
|
240
|
+
content: string;
|
|
241
|
+
byteLength: number;
|
|
242
|
+
truncated: boolean;
|
|
243
|
+
}
|
|
244
|
+
|
|
84
245
|
export interface ProjectionExpectedSnapshotV1 {
|
|
85
246
|
repositoryId: string;
|
|
86
247
|
workspaceId: string;
|
|
@@ -98,6 +259,14 @@ export interface ProjectionRequestV1 {
|
|
|
98
259
|
expected: ProjectionExpectedSnapshotV1;
|
|
99
260
|
adoptionPlanId?: string;
|
|
100
261
|
acceptedChange?: AcceptedArchitectureChangeReferenceV1;
|
|
262
|
+
/**
|
|
263
|
+
* `apply` and `adopt` only, never together with `acceptedChange`: accept the major change this
|
|
264
|
+
* run observes at `expected`. The provider classifies, derives the accepted change and applies
|
|
265
|
+
* it against the same expected snapshot, which the daemon re-checks under its writer lock before
|
|
266
|
+
* writing. The apply receipt records the observed change with provider-generated `changeSetId`
|
|
267
|
+
* and `eventId`. Absent, a major change still stops at `human-action-required` (#261).
|
|
268
|
+
*/
|
|
269
|
+
acceptObservedMajorChange?: true;
|
|
101
270
|
}
|
|
102
271
|
|
|
103
272
|
export interface ProjectionSnapshotV1 extends ProjectionExpectedSnapshotV1 {
|
|
@@ -112,7 +281,6 @@ export interface ProjectionSnapshotV1 extends ProjectionExpectedSnapshotV1 {
|
|
|
112
281
|
generatedFrom: {
|
|
113
282
|
codeGraphPackage: "@colbymchenry/codegraph";
|
|
114
283
|
codeGraphVersion: "1.6.1";
|
|
115
|
-
codeGraphBinaryDigest: Sha256Digest;
|
|
116
284
|
codeGraphStatus: "ready" | "unavailable";
|
|
117
285
|
};
|
|
118
286
|
}
|
|
@@ -122,11 +290,23 @@ export interface ProjectionFileResultV1 {
|
|
|
122
290
|
action: "create" | "delete" | "unchanged" | "update";
|
|
123
291
|
preimageDigest: Sha256Digest | null;
|
|
124
292
|
outputDigest: Sha256Digest | null;
|
|
293
|
+
/**
|
|
294
|
+
* `plan` results only (#264): what this entry would write. Absent on every other mode, on a
|
|
295
|
+
* committed receipt, and on an entry whose path the projection rejected for adoption or ownership
|
|
296
|
+
* repair, because no apply writes that rendered body. Consumers gate on `projection-preview-v1`.
|
|
297
|
+
*/
|
|
298
|
+
preview?: ProjectionFilePreviewV1;
|
|
125
299
|
}
|
|
126
300
|
|
|
127
301
|
export interface ProjectionHumanActionV1 {
|
|
128
302
|
reasonCode: ProjectionHumanActionReasonCode;
|
|
129
303
|
affectedNodeIds: string[];
|
|
304
|
+
/**
|
|
305
|
+
* Present exactly on `orphaned-document-review`: the managed document whose target the model no
|
|
306
|
+
* longer renders and which may hold human text. `apply` never deletes it; a human must review,
|
|
307
|
+
* move or remove it. A generated-only orphan is a `files[]` delete instead.
|
|
308
|
+
*/
|
|
309
|
+
path?: string;
|
|
130
310
|
requestPayloadDigest: Sha256Digest;
|
|
131
311
|
}
|
|
132
312
|
|
|
@@ -170,6 +350,13 @@ export interface ArchitectureRefreshSignalV1 {
|
|
|
170
350
|
acceptedChange?: AcceptedArchitectureChangeReferenceV1;
|
|
171
351
|
reasonCodes: ArchitectureMajorChangeReasonCode[];
|
|
172
352
|
affectedNodeIds: string[];
|
|
353
|
+
/**
|
|
354
|
+
* The observed per-capability breakdown of the semantic delta this signal classifies, sorted by
|
|
355
|
+
* capabilityId (#264). Every signal this provider produces carries it; it is part of the signal
|
|
356
|
+
* identity; signals committed before it existed lack it. Consumers gate on
|
|
357
|
+
* `architecture-change-capabilities-v1`, not on field presence.
|
|
358
|
+
*/
|
|
359
|
+
capabilities?: ArchitectureCapabilityChangeV1[];
|
|
173
360
|
refreshTargets: ArchitectureRefreshTarget[];
|
|
174
361
|
baseDigests: ArchitectureDigestSetV1;
|
|
175
362
|
resultingDigests: ArchitectureDigestSetV1;
|
|
@@ -203,6 +390,21 @@ export interface ProjectionPriorCommittedApplyV1 {
|
|
|
203
390
|
files: ProjectionPriorCommittedApplyFileV1[];
|
|
204
391
|
}
|
|
205
392
|
|
|
393
|
+
export interface ProjectionFreshnessStaleNodeV1 {
|
|
394
|
+
nodeId: string;
|
|
395
|
+
/** Source-footprint digest the node's projected document is stamped with. */
|
|
396
|
+
stampedDigest: Sha256Digest;
|
|
397
|
+
/** Source-footprint digest measured by this run. */
|
|
398
|
+
currentDigest: Sha256Digest;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/** Read-only per-node freshness of the committed projection, as `check` measured it. */
|
|
402
|
+
export interface ProjectionFreshnessV1 {
|
|
403
|
+
ok: boolean;
|
|
404
|
+
reasonCodes: ProjectionFreshnessReasonCode[];
|
|
405
|
+
staleNodes: ProjectionFreshnessStaleNodeV1[];
|
|
406
|
+
}
|
|
407
|
+
|
|
206
408
|
export interface ProjectionResultV2 {
|
|
207
409
|
schemaVersion: typeof PROJECTION_RESULT_SCHEMA_VERSION;
|
|
208
410
|
requestId: string;
|
|
@@ -214,8 +416,31 @@ export interface ProjectionResultV2 {
|
|
|
214
416
|
humanActions: ProjectionHumanActionV1[];
|
|
215
417
|
refreshSignals: ArchitectureRefreshSignalV1[];
|
|
216
418
|
applyReceipt?: ProjectionApplyIdentityV1;
|
|
217
|
-
/**
|
|
419
|
+
/**
|
|
420
|
+
* Omitted, never `[]`, when no earlier attempt of this requestId committed. Never present on
|
|
421
|
+
* `check` results: a check runs in-process without the daemon journal, so read committed applies
|
|
422
|
+
* through `apply` or `readback`.
|
|
423
|
+
*/
|
|
218
424
|
priorCommittedApplies?: ProjectionPriorCommittedApplyV1[];
|
|
425
|
+
/**
|
|
426
|
+
* Present on `check` results only: which nodes changed since their documents were verified.
|
|
427
|
+
* Consumers gate on the `projection-check-freshness-v1` capability, not on field presence.
|
|
428
|
+
*/
|
|
429
|
+
freshness?: ProjectionFreshnessV1;
|
|
430
|
+
/**
|
|
431
|
+
* Present, as `true`, only when this response returns an apply this requestId already committed
|
|
432
|
+
* for the same request digest, without applying again (#265). It is excluded from
|
|
433
|
+
* `receiptDigest`, so a replay carries the committed receipt digest unchanged.
|
|
434
|
+
*/
|
|
435
|
+
replayed?: true;
|
|
436
|
+
/**
|
|
437
|
+
* Present only on a `human-action-required` result whose request sent `acceptObservedMajorChange`
|
|
438
|
+
* and the provider declined it: `declined-unprovable-proof` means a capability's P1 or P2 proof is
|
|
439
|
+
* unprovable, so no observed change can be accepted. The result then carries an
|
|
440
|
+
* `unprovable-required-flow` action naming those capabilities, never `unresolved-major-change`,
|
|
441
|
+
* so a caller can tell a declined flag from an absent one (#275).
|
|
442
|
+
*/
|
|
443
|
+
majorChangeAcceptance?: "declined-unprovable-proof";
|
|
219
444
|
receiptDigest: Sha256Digest;
|
|
220
445
|
}
|
|
221
446
|
|
|
@@ -243,6 +468,12 @@ export interface ProjectionApplyRecoveryBindingV1 {
|
|
|
243
468
|
generatedFrom: ProjectionSnapshotV1["generatedFrom"];
|
|
244
469
|
ownedOutputDigest: Sha256Digest;
|
|
245
470
|
receiptDigest: Sha256Digest;
|
|
471
|
+
/**
|
|
472
|
+
* `digestJson` of the caller's ProjectionRequestV1 exactly as received. With the result's
|
|
473
|
+
* requestId it is the replay key of #265; it never includes provider-generated ids. Receipts
|
|
474
|
+
* committed before it existed lack it and are never replayed.
|
|
475
|
+
*/
|
|
476
|
+
requestDigest?: Sha256Digest;
|
|
246
477
|
}
|
|
247
478
|
|
|
248
479
|
/**
|
|
@@ -365,6 +596,11 @@ export function projectionRequestInvariantIssues(input: ProjectionRequestV1): st
|
|
|
365
596
|
if (!/^[a-zA-Z0-9_.:-]+$/.test(input.requestId)) issues.push("requestId must use the stable identifier character set");
|
|
366
597
|
if (input.mode === "adopt" && !input.adoptionPlanId) issues.push("adoptionPlanId is required when mode=adopt");
|
|
367
598
|
if (input.mode !== "adopt" && input.adoptionPlanId !== undefined) issues.push("adoptionPlanId is only allowed when mode=adopt");
|
|
599
|
+
if (input.acceptObservedMajorChange !== undefined) {
|
|
600
|
+
if (input.acceptObservedMajorChange !== true) issues.push("acceptObservedMajorChange must be true when present");
|
|
601
|
+
if (input.mode !== "apply" && input.mode !== "adopt") issues.push("acceptObservedMajorChange is only allowed when mode=apply or mode=adopt");
|
|
602
|
+
if (input.acceptedChange !== undefined) issues.push("acceptObservedMajorChange and acceptedChange are mutually exclusive");
|
|
603
|
+
}
|
|
368
604
|
if (input.acceptedChange) {
|
|
369
605
|
if (input.acceptedChange.changeSetId.trim() === "") issues.push("acceptedChange.changeSetId must not be empty");
|
|
370
606
|
if (input.acceptedChange.eventId.trim() === "") issues.push("acceptedChange.eventId must not be empty");
|
|
@@ -381,6 +617,17 @@ export function projectionRequestInvariantIssues(input: ProjectionRequestV1): st
|
|
|
381
617
|
return issues;
|
|
382
618
|
}
|
|
383
619
|
|
|
620
|
+
/**
|
|
621
|
+
* Invariants that bind a result to the request mode that produced it, which the result itself does
|
|
622
|
+
* not carry: only a `plan` result previews what it would write (#264); `check`, `apply` and
|
|
623
|
+
* `adopt` results never carry `files[].preview`, whatever their status.
|
|
624
|
+
*/
|
|
625
|
+
export function projectionResultRequestModeIssues(input: ProjectionResultV2, mode: ProjectionMode): string[] {
|
|
626
|
+
return mode !== "plan" && input.files.some((file) => file.preview !== undefined)
|
|
627
|
+
? [`files[].preview is only allowed on a plan result, never when mode=${mode}`]
|
|
628
|
+
: [];
|
|
629
|
+
}
|
|
630
|
+
|
|
384
631
|
export function projectionResultInvariantIssues(input: ProjectionResultV2): string[] {
|
|
385
632
|
const issues = [
|
|
386
633
|
...sortedUniqueIssues("affectedNodeIds", input.affectedNodeIds),
|
|
@@ -400,6 +647,19 @@ export function projectionResultInvariantIssues(input: ProjectionResultV2): stri
|
|
|
400
647
|
if (!input.applyReceipt) issues.push("applied-reconcile-required status requires applyReceipt");
|
|
401
648
|
if (input.refreshSignals.length > 0) issues.push("applied-reconcile-required status cannot deliver refreshSignals");
|
|
402
649
|
}
|
|
650
|
+
for (const [index, action] of input.humanActions.entries()) {
|
|
651
|
+
const prefix = `humanActions[${index}]`;
|
|
652
|
+
if (!(PROJECTION_HUMAN_ACTION_REASON_CODES as readonly string[]).includes(action.reasonCode)) issues.push(`${prefix}.reasonCode is unsupported`);
|
|
653
|
+
if ((action.reasonCode === "orphaned-document-review") !== (action.path !== undefined)) {
|
|
654
|
+
issues.push(`${prefix}.path must be present exactly when reasonCode=orphaned-document-review`);
|
|
655
|
+
}
|
|
656
|
+
if (action.path !== undefined && !isRepoRelativePosixPath(action.path)) issues.push(`${prefix}.path must be a repository-relative POSIX path`);
|
|
657
|
+
}
|
|
658
|
+
const orphanReviewPaths = input.humanActions.flatMap((action) => action.reasonCode === "orphaned-document-review" && action.path !== undefined ? [action.path] : []);
|
|
659
|
+
issues.push(...sortedUniqueIssues("humanActions[orphaned-document-review].path", orphanReviewPaths));
|
|
660
|
+
if (orphanReviewPaths.some((path) => input.files.some((file) => file.path === path))) {
|
|
661
|
+
issues.push("an orphaned-document-review path must not also be a files[] entry");
|
|
662
|
+
}
|
|
403
663
|
for (const [index, file] of input.files.entries()) {
|
|
404
664
|
const prefix = `files[${index}]`;
|
|
405
665
|
if (file.action === "create" && (file.preimageDigest !== null || file.outputDigest === null)) {
|
|
@@ -414,10 +674,36 @@ export function projectionResultInvariantIssues(input: ProjectionResultV2): stri
|
|
|
414
674
|
if (file.action === "unchanged" && (file.preimageDigest === null || file.outputDigest === null || file.preimageDigest !== file.outputDigest)) {
|
|
415
675
|
issues.push(`${prefix} unchanged requires equal non-null digests`);
|
|
416
676
|
}
|
|
677
|
+
if (file.preview !== undefined) issues.push(...projectionFilePreviewIssues(file, prefix));
|
|
678
|
+
}
|
|
679
|
+
const previewBytes = input.files.reduce((total, file) => total + (file.preview ? utf8ByteLength(file.preview.content) : 0), 0);
|
|
680
|
+
if (previewBytes > PROJECTION_PREVIEW_TOTAL_MAX_BYTES) {
|
|
681
|
+
issues.push(`files[].preview.content must not exceed ${PROJECTION_PREVIEW_TOTAL_MAX_BYTES} UTF-8 bytes in total`);
|
|
682
|
+
}
|
|
683
|
+
// The result does not carry its request mode; projectionResultRequestModeIssues binds a preview
|
|
684
|
+
// to mode=plan. On the result alone, a committed apply (and so a persisted receipt) never carries one.
|
|
685
|
+
if (input.files.some((file) => file.preview !== undefined)
|
|
686
|
+
&& (input.applyReceipt !== undefined || input.status === "applied" || input.status === "applied-reconcile-required")) {
|
|
687
|
+
issues.push("files[].preview is only allowed on a plan result, never on a committed apply");
|
|
417
688
|
}
|
|
418
689
|
if (input.priorCommittedApplies) {
|
|
419
690
|
issues.push(...projectionPriorCommittedAppliesIssues(input.priorCommittedApplies, input.requestId));
|
|
420
691
|
}
|
|
692
|
+
if (input.freshness) issues.push(...projectionFreshnessIssues(input.freshness));
|
|
693
|
+
if (input.majorChangeAcceptance !== undefined) {
|
|
694
|
+
if (input.majorChangeAcceptance !== "declined-unprovable-proof") issues.push("majorChangeAcceptance is unsupported");
|
|
695
|
+
if (input.status !== "human-action-required"
|
|
696
|
+
|| !input.humanActions.some((action) => action.reasonCode === "unprovable-required-flow")
|
|
697
|
+
|| input.humanActions.some((action) => action.reasonCode === "unresolved-major-change")) {
|
|
698
|
+
issues.push("majorChangeAcceptance requires human-action-required with unprovable-required-flow in place of unresolved-major-change");
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
if (input.replayed !== undefined) {
|
|
702
|
+
if (input.replayed !== true) issues.push("replayed must be true when present");
|
|
703
|
+
if (!input.applyReceipt || (input.status !== "applied" && input.status !== "applied-reconcile-required")) {
|
|
704
|
+
issues.push("replayed is only allowed on a committed apply result with applyReceipt");
|
|
705
|
+
}
|
|
706
|
+
}
|
|
421
707
|
const { receiptDigest, ...receiptPayload } = input;
|
|
422
708
|
if (projectionResultReceiptDigest(receiptPayload) !== receiptDigest) issues.push("receiptDigest must match the canonical projection result payload");
|
|
423
709
|
for (const [index, signal] of input.refreshSignals.entries()) {
|
|
@@ -431,6 +717,53 @@ export function projectionResultInvariantIssues(input: ProjectionResultV2): stri
|
|
|
431
717
|
return issues;
|
|
432
718
|
}
|
|
433
719
|
|
|
720
|
+
function projectionFilePreviewIssues(file: ProjectionFileResultV1, prefix: string): string[] {
|
|
721
|
+
const preview = file.preview!;
|
|
722
|
+
const issues: string[] = [];
|
|
723
|
+
const expectedFormat: ProjectionFilePreviewFormat | undefined = file.action === "create"
|
|
724
|
+
? "body"
|
|
725
|
+
: file.action === "update" || file.action === "delete" ? "unified-diff" : undefined;
|
|
726
|
+
if (expectedFormat === undefined) issues.push(`${prefix}.preview is not allowed when action=${file.action}`);
|
|
727
|
+
else if (preview.format !== expectedFormat) issues.push(`${prefix}.preview.format must be ${expectedFormat} when action=${file.action}`);
|
|
728
|
+
if (typeof preview.content !== "string" || typeof preview.truncated !== "boolean"
|
|
729
|
+
|| !Number.isSafeInteger(preview.byteLength) || preview.byteLength < 0) {
|
|
730
|
+
issues.push(`${prefix}.preview must carry string content, a boolean truncated flag and a non-negative integer byteLength`);
|
|
731
|
+
return issues;
|
|
732
|
+
}
|
|
733
|
+
const contentBytes = utf8ByteLength(preview.content);
|
|
734
|
+
if (contentBytes > PROJECTION_FILE_PREVIEW_MAX_BYTES) issues.push(`${prefix}.preview.content must not exceed ${PROJECTION_FILE_PREVIEW_MAX_BYTES} UTF-8 bytes`);
|
|
735
|
+
if (preview.truncated ? contentBytes >= preview.byteLength : contentBytes !== preview.byteLength) {
|
|
736
|
+
issues.push(`${prefix}.preview.truncated must be true exactly when content is shorter than byteLength`);
|
|
737
|
+
}
|
|
738
|
+
return issues;
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
const UTF8 = new TextEncoder();
|
|
742
|
+
|
|
743
|
+
function utf8ByteLength(value: string): number {
|
|
744
|
+
return UTF8.encode(value).length;
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
export function projectionFreshnessIssues(input: ProjectionFreshnessV1): string[] {
|
|
748
|
+
const issues = [
|
|
749
|
+
...sortedUniqueIssues("freshness.reasonCodes", input.reasonCodes),
|
|
750
|
+
...sortedUniqueIssues("freshness.staleNodes.nodeId", input.staleNodes.map((node) => node.nodeId))
|
|
751
|
+
];
|
|
752
|
+
const unsupported = input.reasonCodes.find((code) => !(PROJECTION_FRESHNESS_REASON_CODES as readonly string[]).includes(code));
|
|
753
|
+
if (unsupported) issues.push(`freshness.reasonCodes contains an unsupported code: ${unsupported}`);
|
|
754
|
+
if (input.ok !== (input.reasonCodes.length === 0)) issues.push("freshness.ok must be true exactly when reasonCodes is empty");
|
|
755
|
+
if ((input.staleNodes.length > 0) !== input.reasonCodes.includes("projection-source-changed-since-stamp")) {
|
|
756
|
+
issues.push("freshness.staleNodes must be non-empty exactly when projection-source-changed-since-stamp is reported");
|
|
757
|
+
}
|
|
758
|
+
for (const [index, node] of input.staleNodes.entries()) {
|
|
759
|
+
const prefix = `freshness.staleNodes[${index}]`;
|
|
760
|
+
if (node.nodeId.trim() === "") issues.push(`${prefix}.nodeId must not be empty`);
|
|
761
|
+
if (!SHA256_DIGEST.test(node.stampedDigest) || !SHA256_DIGEST.test(node.currentDigest)) issues.push(`${prefix} digests must be SHA-256 digests`);
|
|
762
|
+
else if (node.stampedDigest === node.currentDigest) issues.push(`${prefix} must name two different digests`);
|
|
763
|
+
}
|
|
764
|
+
return issues;
|
|
765
|
+
}
|
|
766
|
+
|
|
434
767
|
export function projectionPriorCommittedAppliesIssues(
|
|
435
768
|
input: readonly ProjectionPriorCommittedApplyV1[],
|
|
436
769
|
requestId: string
|
|
@@ -471,8 +804,9 @@ export function projectionPriorCommittedAppliesIssues(
|
|
|
471
804
|
* projectionReceiptDigest, which projectionResultInvariantIssues verifies.
|
|
472
805
|
*/
|
|
473
806
|
export function projectionResultReceiptDigest(input: Omit<ProjectionResultV2, "receiptDigest">): Sha256Digest {
|
|
474
|
-
const
|
|
475
|
-
|
|
807
|
+
const { replayed: _replayed, ...payload } = input;
|
|
808
|
+
const refreshSignals = payload.refreshSignals.map(({ projectionReceiptDigest: _projectionReceiptDigest, ...signal }) => signal);
|
|
809
|
+
return digestJson({ ...payload, refreshSignals } as unknown as Json) as Sha256Digest;
|
|
476
810
|
}
|
|
477
811
|
|
|
478
812
|
export function projectionApplyLookupKey(input: {
|
|
@@ -551,6 +885,7 @@ export function projectionApplyRecoveryBindingInvariantIssues(
|
|
|
551
885
|
issues.push("recovery schemaVersion is invalid");
|
|
552
886
|
}
|
|
553
887
|
if (binding.targets.length === 0) issues.push("recovery targets must contain at least one projection target");
|
|
888
|
+
if (binding.requestDigest !== undefined && !SHA256_DIGEST.test(binding.requestDigest)) issues.push("recovery requestDigest must be a SHA-256 digest");
|
|
554
889
|
if (receipt) {
|
|
555
890
|
if (binding.receiptDigest !== receipt.result.receiptDigest) {
|
|
556
891
|
issues.push("recovery receiptDigest must match the committed result receiptDigest");
|
|
@@ -596,6 +931,33 @@ export function projectionApplyReadbackRequestInvariantIssues(request: Projectio
|
|
|
596
931
|
return issues;
|
|
597
932
|
}
|
|
598
933
|
|
|
934
|
+
/**
|
|
935
|
+
* The readback request for a committed apply receipt, built only from what the receipt recorded:
|
|
936
|
+
* its requestId, the recovery binding's targets, changedPaths and original expected snapshot, and
|
|
937
|
+
* the identity's accepted change. Readback always takes `mode: "apply"`, also for an adopt receipt,
|
|
938
|
+
* because the binding never records the mode. A caller that held only `acceptObservedMajorChange`
|
|
939
|
+
* never had the accepted change, so `AC_PROJECTION_APPLY_COMMITTED` hands it this request (#278).
|
|
940
|
+
* Undefined for a receipt without a recovery binding, which readback cannot prove.
|
|
941
|
+
*/
|
|
942
|
+
export function projectionApplyReadbackRequestFromReceipt(receipt: ProjectionApplyReceiptV1): ProjectionRequestV1 | undefined {
|
|
943
|
+
const binding = receipt.recovery;
|
|
944
|
+
if (!binding) return undefined;
|
|
945
|
+
return {
|
|
946
|
+
schemaVersion: PROJECTION_REQUEST_SCHEMA_VERSION,
|
|
947
|
+
requestId: receipt.result.requestId,
|
|
948
|
+
profile: "repo-harness/v1",
|
|
949
|
+
mode: "apply",
|
|
950
|
+
targets: [...binding.targets],
|
|
951
|
+
changedPaths: [...binding.changedPaths],
|
|
952
|
+
expected: { ...binding.originalExpectedSnapshot },
|
|
953
|
+
acceptedChange: {
|
|
954
|
+
...receipt.identity.acceptedChange,
|
|
955
|
+
reasonCodes: [...receipt.identity.acceptedChange.reasonCodes],
|
|
956
|
+
affectedNodeIds: [...receipt.identity.acceptedChange.affectedNodeIds]
|
|
957
|
+
}
|
|
958
|
+
};
|
|
959
|
+
}
|
|
960
|
+
|
|
599
961
|
export function projectionApplyAbsenceInvariantIssues(input: ProjectionApplyAbsenceV1, request: ProjectionRequestV1): string[] {
|
|
600
962
|
const issues = projectionApplyReadbackRequestInvariantIssues(request);
|
|
601
963
|
if (input.schemaVersion !== "archcontext.projection-apply-absence/v1") issues.push("absence schemaVersion is invalid");
|
|
@@ -773,6 +1135,7 @@ export function architectureRefreshSignalInvariantIssues(input: ArchitectureRefr
|
|
|
773
1135
|
issues.push(`${prefix}.acceptedChange.affectedNodeIds must match signal affectedNodeIds`);
|
|
774
1136
|
}
|
|
775
1137
|
}
|
|
1138
|
+
if (input.capabilities !== undefined) issues.push(...architectureCapabilityChangesIssues(input.capabilities, `${prefix}.capabilities`));
|
|
776
1139
|
if (input.reasonCodes.length === 0) issues.push(`${prefix}.reasonCodes must contain at least one reason`);
|
|
777
1140
|
if (input.affectedNodeIds.length === 0) issues.push(`${prefix}.affectedNodeIds must contain at least one node`);
|
|
778
1141
|
if (input.refreshTargets.length === 0) issues.push(`${prefix}.refreshTargets must contain at least one target`);
|
|
@@ -791,6 +1154,43 @@ export function architectureRefreshSignalInvariantIssues(input: ArchitectureRefr
|
|
|
791
1154
|
return issues;
|
|
792
1155
|
}
|
|
793
1156
|
|
|
1157
|
+
/** Shape and canonical order of a per-capability major-change breakdown (#264). */
|
|
1158
|
+
export function architectureCapabilityChangesIssues(input: readonly ArchitectureCapabilityChangeV1[], prefix = "capabilities"): string[] {
|
|
1159
|
+
const issues = sortedUniqueIssues(`${prefix}.capabilityId`, input.map((entry) => entry.capabilityId));
|
|
1160
|
+
if (input.length === 0) issues.push(`${prefix} must name at least one capability`);
|
|
1161
|
+
const reasons = new Set<string>(ARCHITECTURE_MAJOR_CHANGE_REASON_CODES);
|
|
1162
|
+
const facets = new Set<string>(ARCHITECTURE_SEMANTIC_FACETS);
|
|
1163
|
+
for (const [index, entry] of input.entries()) {
|
|
1164
|
+
const entryPrefix = `${prefix}[${index}]`;
|
|
1165
|
+
if (typeof entry.capabilityId !== "string" || entry.capabilityId.trim() === "") issues.push(`${entryPrefix}.capabilityId must not be empty`);
|
|
1166
|
+
issues.push(
|
|
1167
|
+
...sortedUniqueIssues(`${entryPrefix}.reasonCodes`, entry.reasonCodes),
|
|
1168
|
+
...sortedUniqueIssues(`${entryPrefix}.changedFacets`, entry.changedFacets)
|
|
1169
|
+
);
|
|
1170
|
+
const reason = entry.reasonCodes.find((code) => !reasons.has(code));
|
|
1171
|
+
if (reason !== undefined) issues.push(`${entryPrefix}.reasonCodes contains unsupported reason: ${reason}`);
|
|
1172
|
+
const facet = entry.changedFacets.find((name) => !facets.has(name));
|
|
1173
|
+
if (facet !== undefined) issues.push(`${entryPrefix}.changedFacets contains unsupported facet: ${facet}`);
|
|
1174
|
+
for (const field of ["proofStatusBefore", "proofStatusAfter"] as const) {
|
|
1175
|
+
const status = entry[field];
|
|
1176
|
+
if (status !== null && !isArchitectureCapabilityProofStatus(status)) issues.push(`${entryPrefix}.${field} must be null or a P1/P2 proof status`);
|
|
1177
|
+
}
|
|
1178
|
+
if (entry.proofStatusBefore === null && entry.proofStatusAfter === null) issues.push(`${entryPrefix} must exist before or after the change`);
|
|
1179
|
+
if ((entry.proofStatusBefore === null || entry.proofStatusAfter === null) && entry.changedFacets.length > 0) {
|
|
1180
|
+
issues.push(`${entryPrefix}.changedFacets must be empty for a capability present on one side only`);
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
return issues;
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
export function isArchitectureCapabilityProofStatus(value: unknown): value is ArchitectureCapabilityProofStatusV1 {
|
|
1187
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
1188
|
+
const record = value as Record<string, unknown>;
|
|
1189
|
+
return Object.keys(record).length === 2
|
|
1190
|
+
&& (ARCHITECTURE_PROOF_P1_STATUSES as readonly unknown[]).includes(record.p1)
|
|
1191
|
+
&& (ARCHITECTURE_PROOF_P2_STATUSES as readonly unknown[]).includes(record.p2);
|
|
1192
|
+
}
|
|
1193
|
+
|
|
794
1194
|
const SHA256_DIGEST = /^sha256:[a-f0-9]{64}$/;
|
|
795
1195
|
const ISO_INSTANT = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?Z$/;
|
|
796
1196
|
|