archctx-contracts 0.7.1 → 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.
- 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/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 +776 -0
- package/src/index.ts +6 -0
- package/src/ledger.ts +14 -3
- package/src/native-context.ts +277 -0
- package/src/ownership-dependencies.ts +39 -1
- 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 +6 -0
- package/src/validator.ts +8 -3
package/src/projection.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { EvidenceCoverageLevelV2 } from "./ledger";
|
|
1
2
|
import { digestJson, isRepoRelativePosixPath, type Json } from "./schema";
|
|
2
3
|
|
|
3
4
|
export const PROJECTION_REQUEST_SCHEMA_VERSION = "archcontext.projection-request/v1" as const;
|
|
@@ -11,7 +12,12 @@ export const PROJECTION_APPLY_RECOVERY_RESULT_SCHEMA_VERSION = "archcontext.proj
|
|
|
11
12
|
export const PROJECTION_APPLY_READBACK_RESULT_SCHEMA_VERSION = "archcontext.projection-apply-readback-result/v1" as const;
|
|
12
13
|
export const ARCHITECTURE_REFRESH_SIGNAL_SCHEMA_VERSION = "archcontext.architecture-refresh-signal/v1" as const;
|
|
13
14
|
export const ARCHCTX_CAPABILITIES_SCHEMA_VERSION = "archcontext.capabilities/v1" as const;
|
|
14
|
-
export const ARCHITECTURE_DOCS_RENDERER_VERSION = "archcontext.docs-renderer/
|
|
15
|
+
export const ARCHITECTURE_DOCS_RENDERER_VERSION = "archcontext.docs-renderer/v6" as const;
|
|
16
|
+
/**
|
|
17
|
+
* The committed projection manifest's path. `projection-manifest.ts` owns its shape and byte layout;
|
|
18
|
+
* the path lives here because a projection result's `files[]` names the manifest write.
|
|
19
|
+
*/
|
|
20
|
+
export const ARCHITECTURE_DOCS_PROJECTION_MANIFEST_PATH = "docs/architecture/.projection-manifest.json" as const;
|
|
15
21
|
export const AGENT_CONTEXT_RENDERER_VERSION = "archcontext.agent-context-renderer/v1" as const;
|
|
16
22
|
|
|
17
23
|
export const PROJECTION_MODES = ["check", "plan", "apply", "adopt"] as const;
|
|
@@ -20,14 +26,28 @@ export const PROJECTION_TARGETS = ["agent-context", "architecture-docs"] as cons
|
|
|
20
26
|
export const PROJECTION_RESULT_STATUSES = [
|
|
21
27
|
"adoption-required",
|
|
22
28
|
"applied",
|
|
29
|
+
"applied-partial",
|
|
23
30
|
"applied-reconcile-required",
|
|
24
31
|
"blocked",
|
|
25
32
|
"human-action-required",
|
|
26
33
|
"noop",
|
|
27
34
|
"permanent-failure",
|
|
28
35
|
"planned",
|
|
36
|
+
"planned-partial",
|
|
29
37
|
"retryable-failure"
|
|
30
38
|
] as const;
|
|
39
|
+
/**
|
|
40
|
+
* The two statuses of a projection that covers only part of its targets (#327, Phase 1 capability
|
|
41
|
+
* isolation): `planned-partial` from `check` and `plan`, `applied-partial` from `apply`. Exactly
|
|
42
|
+
* these results carry `coverage`, always with `complete: false`; a complete result omits it.
|
|
43
|
+
*/
|
|
44
|
+
export const PROJECTION_PARTIAL_RESULT_STATUSES = ["applied-partial", "planned-partial"] as const;
|
|
45
|
+
/**
|
|
46
|
+
* Why a target is carried forward instead of applied: a capability it depends on has an
|
|
47
|
+
* unprovable P1 or P2 proof (`unprovable-required-flow`), or its flow proof moved since the
|
|
48
|
+
* committed baseline while its semantic fingerprint held (`verified-flow-proof-changed`).
|
|
49
|
+
*/
|
|
50
|
+
export const PROJECTION_COVERAGE_REASON_CODES = ["unprovable-required-flow", "verified-flow-proof-changed"] as const;
|
|
31
51
|
export const PROJECTION_HUMAN_ACTION_REASON_CODES = [
|
|
32
52
|
"adoption-required",
|
|
33
53
|
"manual-region-conflict",
|
|
@@ -60,8 +80,10 @@ export const ARCHITECTURE_REFRESH_TARGETS = [
|
|
|
60
80
|
] as const;
|
|
61
81
|
export const ARCHCTX_FEATURES = [
|
|
62
82
|
"architecture-change-capabilities-v1",
|
|
83
|
+
"architecture-debt-v1",
|
|
63
84
|
"architecture-docs-renderer-v2",
|
|
64
85
|
"architecture-refresh-signal-v1",
|
|
86
|
+
"capability-dependencies-declared-v1",
|
|
65
87
|
"capability-dependencies-v1",
|
|
66
88
|
"changeset-entity-operations-v1",
|
|
67
89
|
"changeset-ownership-delta-v1",
|
|
@@ -69,17 +91,24 @@ export const ARCHCTX_FEATURES = [
|
|
|
69
91
|
"docs-anchors-v1",
|
|
70
92
|
"flow-selector-check-v1",
|
|
71
93
|
"module-statistics-v1",
|
|
94
|
+
"native-context-export-v1",
|
|
72
95
|
"projection-apply-readback-v1",
|
|
73
96
|
"projection-apply-receipt-v1",
|
|
74
97
|
"projection-apply-recovery-v1",
|
|
98
|
+
"projection-apply-replay-restore-v1",
|
|
75
99
|
"projection-apply-replay-v1",
|
|
100
|
+
"projection-architecture-debt-v1",
|
|
101
|
+
"projection-capability-isolation-v1",
|
|
102
|
+
"projection-change-classification-v1",
|
|
76
103
|
"projection-check-freshness-v1",
|
|
77
|
-
"projection-
|
|
104
|
+
"projection-freshness-v1",
|
|
105
|
+
"projection-manifest-contract-v3",
|
|
78
106
|
"projection-observed-major-change-acceptance-v1",
|
|
79
107
|
"projection-orphan-review-v1",
|
|
80
108
|
"projection-preview-v1",
|
|
81
109
|
"projection-prior-committed-applies-v1",
|
|
82
110
|
"projection-protocol-v2",
|
|
111
|
+
"projection-snapshot-v1",
|
|
83
112
|
"recommendation-scan-decision-v1",
|
|
84
113
|
"recommendation-v3",
|
|
85
114
|
"refactor-assessment-v1",
|
|
@@ -185,22 +214,99 @@ export const PROJECTION_PREVIEW_TOTAL_MAX_BYTES = 1_048_576;
|
|
|
185
214
|
export const PROJECTION_FRESHNESS_REASON_CODES = [
|
|
186
215
|
"projection-manifest-missing",
|
|
187
216
|
"projection-manifest-unreadable",
|
|
188
|
-
"projection-snapshot-provenance-missing",
|
|
189
217
|
"projection-source-changed-since-stamp",
|
|
190
|
-
"projection-source-stamp-invalid",
|
|
191
218
|
"projection-source-stamp-missing",
|
|
192
219
|
"projection-source-tree-digest-mismatch"
|
|
193
220
|
] as const;
|
|
194
221
|
|
|
222
|
+
/** The architecture debt a `check` or `plan` result reports. Debt is a finding, never a blocker. */
|
|
223
|
+
export const PROJECTION_ARCHITECTURE_DEBT_REASON_CODES = ["undeclared-dependency"] as const;
|
|
224
|
+
/**
|
|
225
|
+
* Why the debt read is not a complete observation. The `code-facts-*` codes mean what they mean on
|
|
226
|
+
* `archctx deps`; `workspace-resolution-failed` means the workspace package map was unreadable, so
|
|
227
|
+
* no bare workspace import specifier could be resolved.
|
|
228
|
+
*/
|
|
229
|
+
export const PROJECTION_ARCHITECTURE_DEBT_COVERAGE_REASON_CODES = [
|
|
230
|
+
"code-facts-import-limit",
|
|
231
|
+
"code-facts-truncated",
|
|
232
|
+
"code-facts-unavailable",
|
|
233
|
+
"workspace-resolution-failed"
|
|
234
|
+
] as const;
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* How one target of a `check` or `plan` result changes (#327 finding 2), from the strongest reason
|
|
238
|
+
* down: `blocked` (a human must act before the target can be written: Phase 1 isolation, an orphan
|
|
239
|
+
* that needs review, an adoption or ownership rejection), `proof-change` (a known proof change,
|
|
240
|
+
* which a missing baseline never hides), `baseline-missing` (a comparison is unknown: no committed
|
|
241
|
+
* v3 manifest, or no committed row for the target), `semantic-change`, `output-change`,
|
|
242
|
+
* `stamp-only` (only the source stamps moved) and `unchanged`.
|
|
243
|
+
* `projectionTargetChangeClassification` is the one precedence rule.
|
|
244
|
+
*/
|
|
245
|
+
export const PROJECTION_TARGET_CHANGE_CLASSIFICATIONS = [
|
|
246
|
+
"baseline-missing",
|
|
247
|
+
"blocked",
|
|
248
|
+
"output-change",
|
|
249
|
+
"proof-change",
|
|
250
|
+
"semantic-change",
|
|
251
|
+
"stamp-only",
|
|
252
|
+
"unchanged"
|
|
253
|
+
] as const;
|
|
254
|
+
/** What the projection write does to the target's file: the result's own `files[]` decision for that path. */
|
|
255
|
+
export const PROJECTION_TARGET_CHANGE_PLANNED_ACTIONS = ["create", "delete", "none", "update"] as const;
|
|
256
|
+
/** The reason codes that block a target: no apply writes it until a human acts. */
|
|
257
|
+
export const PROJECTION_TARGET_CHANGE_BLOCKING_REASON_CODES = [
|
|
258
|
+
"adoption-required",
|
|
259
|
+
"ambiguous-ownership",
|
|
260
|
+
"orphan-review-required",
|
|
261
|
+
"unprovable-required-flow",
|
|
262
|
+
"verified-flow-proof-changed"
|
|
263
|
+
] as const;
|
|
264
|
+
/**
|
|
265
|
+
* Why a target is classified as it is. Each comparison flag has its codes: `stampChanged` is
|
|
266
|
+
* `source-stamp-changed`; `semanticChanged` is `semantic-fingerprint-changed` or, for a target that
|
|
267
|
+
* describes the whole model, `model-digest-changed` (the committed `semanticBaseline.modelDigest`
|
|
268
|
+
* differs from the current model's); `proofChanged` is
|
|
269
|
+
* `flow-proof-changed` or `selector-evidence-changed`; `outputChanged` is `rendered-output-changed`
|
|
270
|
+
* (the rendered output differs from the committed row's `outputDigest`), `target-added`,
|
|
271
|
+
* `target-removed` or `file-bytes-changed` (the write changes the file although its rendered output
|
|
272
|
+
* is the committed one: a marker, a missing file, an edited region); an unknown comparison is
|
|
273
|
+
* `baseline-missing` or `target-row-missing`.
|
|
274
|
+
*/
|
|
275
|
+
export const PROJECTION_TARGET_CHANGE_REASON_CODES = [
|
|
276
|
+
"adoption-required",
|
|
277
|
+
"ambiguous-ownership",
|
|
278
|
+
"baseline-missing",
|
|
279
|
+
"file-bytes-changed",
|
|
280
|
+
"flow-proof-changed",
|
|
281
|
+
"model-digest-changed",
|
|
282
|
+
"orphan-review-required",
|
|
283
|
+
"rendered-output-changed",
|
|
284
|
+
"selector-evidence-changed",
|
|
285
|
+
"semantic-fingerprint-changed",
|
|
286
|
+
"source-stamp-changed",
|
|
287
|
+
"target-added",
|
|
288
|
+
"target-removed",
|
|
289
|
+
"target-row-missing",
|
|
290
|
+
"unprovable-required-flow",
|
|
291
|
+
"verified-flow-proof-changed"
|
|
292
|
+
] as const;
|
|
293
|
+
|
|
195
294
|
export type ProjectionMode = (typeof PROJECTION_MODES)[number];
|
|
295
|
+
export type ProjectionTargetChangeClassification = (typeof PROJECTION_TARGET_CHANGE_CLASSIFICATIONS)[number];
|
|
296
|
+
export type ProjectionTargetChangePlannedAction = (typeof PROJECTION_TARGET_CHANGE_PLANNED_ACTIONS)[number];
|
|
297
|
+
export type ProjectionTargetChangeReasonCode = (typeof PROJECTION_TARGET_CHANGE_REASON_CODES)[number];
|
|
196
298
|
export type ProjectionFreshnessReasonCode = (typeof PROJECTION_FRESHNESS_REASON_CODES)[number];
|
|
197
299
|
export type ProjectionTarget = (typeof PROJECTION_TARGETS)[number];
|
|
198
300
|
export type ProjectionResultStatus = (typeof PROJECTION_RESULT_STATUSES)[number];
|
|
301
|
+
export type ProjectionPartialResultStatus = (typeof PROJECTION_PARTIAL_RESULT_STATUSES)[number];
|
|
302
|
+
export type ProjectionCoverageReasonCode = (typeof PROJECTION_COVERAGE_REASON_CODES)[number];
|
|
199
303
|
export type ProjectionHumanActionReasonCode = (typeof PROJECTION_HUMAN_ACTION_REASON_CODES)[number];
|
|
200
304
|
export type ProjectionPriorCommittedApplyOperation = (typeof PROJECTION_PRIOR_COMMITTED_APPLY_OPERATIONS)[number];
|
|
201
305
|
export type ArchitectureMajorChangeReasonCode = (typeof ARCHITECTURE_MAJOR_CHANGE_REASON_CODES)[number];
|
|
202
306
|
export type ArchitectureRefreshTarget = (typeof ARCHITECTURE_REFRESH_TARGETS)[number];
|
|
203
307
|
export type ArchctxFeature = (typeof ARCHCTX_FEATURES)[number];
|
|
308
|
+
export type ProjectionArchitectureDebtReasonCode = (typeof PROJECTION_ARCHITECTURE_DEBT_REASON_CODES)[number];
|
|
309
|
+
export type ProjectionArchitectureDebtCoverageReasonCode = (typeof PROJECTION_ARCHITECTURE_DEBT_COVERAGE_REASON_CODES)[number];
|
|
204
310
|
export type ArchitectureSemanticFacet = (typeof ARCHITECTURE_SEMANTIC_FACETS)[number];
|
|
205
311
|
export type ProjectionFilePreviewFormat = (typeof PROJECTION_FILE_PREVIEW_FORMATS)[number];
|
|
206
312
|
export type Sha256Digest = `sha256:${string}`;
|
|
@@ -280,7 +386,7 @@ export interface ProjectionSnapshotV1 extends ProjectionExpectedSnapshotV1 {
|
|
|
280
386
|
layoutVersion: "archcontext.docs-layout/v1";
|
|
281
387
|
generatedFrom: {
|
|
282
388
|
codeGraphPackage: "@colbymchenry/codegraph";
|
|
283
|
-
codeGraphVersion: "1.6.
|
|
389
|
+
codeGraphVersion: "1.6.2";
|
|
284
390
|
codeGraphStatus: "ready" | "unavailable";
|
|
285
391
|
};
|
|
286
392
|
}
|
|
@@ -405,6 +511,108 @@ export interface ProjectionFreshnessV1 {
|
|
|
405
511
|
staleNodes: ProjectionFreshnessStaleNodeV1[];
|
|
406
512
|
}
|
|
407
513
|
|
|
514
|
+
/**
|
|
515
|
+
* One capability pair whose imports no model relation declares, under the rule of
|
|
516
|
+
* CAPABILITY_DEPENDENCY_DECLARING_RELATION_KINDS. It is the same pair, with the same edge count,
|
|
517
|
+
* that `archctx deps` reports as a cross-capability cell with `declared: false`, measured over
|
|
518
|
+
* the files this run measured instead of the `HEAD` tree.
|
|
519
|
+
*/
|
|
520
|
+
export interface ProjectionUndeclaredDependencyV1 {
|
|
521
|
+
reasonCode: "undeclared-dependency";
|
|
522
|
+
fromCapabilityId: string;
|
|
523
|
+
toCapabilityId: string;
|
|
524
|
+
/** Distinct file-to-file import edges from `fromCapabilityId` into `toCapabilityId`; at least 1. */
|
|
525
|
+
importEdgeCount: number;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/**
|
|
529
|
+
* Architecture debt that this `check` or `plan` run found. It reuses the import facts the run
|
|
530
|
+
* already read, never blocks a mode and changes no rendered output. It covers every capability
|
|
531
|
+
* pair in the repository, not only pairs that touch the request's `changedPaths`.
|
|
532
|
+
*/
|
|
533
|
+
export interface ProjectionArchitectureDebtV1 {
|
|
534
|
+
/**
|
|
535
|
+
* `unknown` when the run had no code facts (`code-facts-unavailable`): `items` is then empty and
|
|
536
|
+
* says nothing about the repository. `partial` when the import read is a prefix of the index or
|
|
537
|
+
* a workspace specifier could not be resolved: `items` then holds what was observed.
|
|
538
|
+
*/
|
|
539
|
+
coverage: EvidenceCoverageLevelV2;
|
|
540
|
+
/** Sorted and unique; empty exactly when `coverage` is `complete`. */
|
|
541
|
+
coverageReasonCodes: ProjectionArchitectureDebtCoverageReasonCode[];
|
|
542
|
+
/** Sorted by `fromCapabilityId`, then `toCapabilityId`. */
|
|
543
|
+
items: ProjectionUndeclaredDependencyV1[];
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/** One target a partial result carries forward instead of applying. */
|
|
547
|
+
export interface ProjectionCoverageBlockedTargetV1 {
|
|
548
|
+
targetId: string;
|
|
549
|
+
path: string;
|
|
550
|
+
/** The blocked capabilities this target depends on, sorted. Never empty. */
|
|
551
|
+
capabilityIds: string[];
|
|
552
|
+
/** Why those capabilities block it, sorted. Never empty. */
|
|
553
|
+
reasonCodes: ProjectionCoverageReasonCode[];
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
/**
|
|
557
|
+
* Which part of the projection a `planned-partial` or `applied-partial` result covers (#327,
|
|
558
|
+
* Phase 1 capability isolation). The blocked capabilities are those whose proof is unprovable or
|
|
559
|
+
* whose flow proof moved without an accepted change, while their semantic fingerprint held. Every
|
|
560
|
+
* target that depends on one of them is carried forward unchanged: its document, its manifest row
|
|
561
|
+
* and the capability's baseline entry keep their committed bytes. `appliedTargetIds` are all the
|
|
562
|
+
* other targets, which the result renders from the current model and sources.
|
|
563
|
+
*
|
|
564
|
+
* `complete` is always `false`: a result that covers every target omits `coverage` instead. A
|
|
565
|
+
* partial result is never an acceptance. It carries no apply receipt and no refresh signal, and a
|
|
566
|
+
* complete check or the `complete` gate still reports the blocked capabilities until they are
|
|
567
|
+
* fixed or accepted.
|
|
568
|
+
*/
|
|
569
|
+
export interface ProjectionCoverageV1 {
|
|
570
|
+
complete: false;
|
|
571
|
+
/** Sorted and unique. Never empty. */
|
|
572
|
+
blockedCapabilityIds: string[];
|
|
573
|
+
/** Sorted and unique by targetId. Never empty. */
|
|
574
|
+
blockedTargets: ProjectionCoverageBlockedTargetV1[];
|
|
575
|
+
/** Sorted and unique; disjoint from the blocked targets. */
|
|
576
|
+
appliedTargetIds: string[];
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
/**
|
|
580
|
+
* One target of a `check` or `plan` result (#327 finding 2): how it changes since the committed
|
|
581
|
+
* projection, and what this run's write does to its file. The entries cover the union of the
|
|
582
|
+
* current layout's targets and the committed manifest's rows (plus orphans the manifest does not
|
|
583
|
+
* record), one per path. The projection manifest is not a target; its write is the `files[]` entry
|
|
584
|
+
* no change names.
|
|
585
|
+
*
|
|
586
|
+
* Each comparison is against the committed v3 manifest: `stampChanged` the target row's
|
|
587
|
+
* `sourceStamps`; `outputChanged` the row's `outputDigest`, or any byte the write changes;
|
|
588
|
+
* `semanticChanged` the `semanticFingerprint` of a capability the target documents (every
|
|
589
|
+
* capability for a target that is not an entity summary, which also compares the manifest's
|
|
590
|
+
* `semanticBaseline.modelDigest`, because it renders the whole model); `proofChanged` the `flowProofFingerprint`
|
|
591
|
+
* or `selectorEvidenceDigest` of a capability an entity summary documents. `null` is an unknown
|
|
592
|
+
* comparison, never `false`.
|
|
593
|
+
*
|
|
594
|
+
* `plannedAction` is the result's own write decision (`files[]`) for that path. When `status` is
|
|
595
|
+
* `planned` or `planned-partial`, the targets whose action is not `none` are exactly the documents
|
|
596
|
+
* an apply of this run writes or deletes; under `adoption-required` or `human-action-required`
|
|
597
|
+
* apply writes nothing, and `plannedAction` reports only what the render would write. A
|
|
598
|
+
* path rejected for adoption or ownership repair is `blocked` and `none`, because only an
|
|
599
|
+
* approved adoption writes it. `classification` never changes `status`, the write set or a gate.
|
|
600
|
+
*/
|
|
601
|
+
export interface ProjectionTargetChangeV1 {
|
|
602
|
+
targetId: string;
|
|
603
|
+
path: string;
|
|
604
|
+
/** Sorted and unique: the documented capabilities whose change affects this target. Empty when unchanged. */
|
|
605
|
+
affectedCapabilityIds: string[];
|
|
606
|
+
stampChanged: boolean | null;
|
|
607
|
+
outputChanged: boolean;
|
|
608
|
+
semanticChanged: boolean | null;
|
|
609
|
+
proofChanged: boolean | null;
|
|
610
|
+
plannedAction: ProjectionTargetChangePlannedAction;
|
|
611
|
+
classification: ProjectionTargetChangeClassification;
|
|
612
|
+
/** Sorted and unique. Empty exactly when `classification` is `unchanged`. */
|
|
613
|
+
reasonCodes: ProjectionTargetChangeReasonCode[];
|
|
614
|
+
}
|
|
615
|
+
|
|
408
616
|
export interface ProjectionResultV2 {
|
|
409
617
|
schemaVersion: typeof PROJECTION_RESULT_SCHEMA_VERSION;
|
|
410
618
|
requestId: string;
|
|
@@ -427,12 +635,31 @@ export interface ProjectionResultV2 {
|
|
|
427
635
|
* Consumers gate on the `projection-check-freshness-v1` capability, not on field presence.
|
|
428
636
|
*/
|
|
429
637
|
freshness?: ProjectionFreshnessV1;
|
|
638
|
+
/**
|
|
639
|
+
* Present on every `check` and `plan` result, and on no other: the architecture debt this run
|
|
640
|
+
* found. It never changes `status`. Consumers gate on the `projection-architecture-debt-v1`
|
|
641
|
+
* capability, not on field presence.
|
|
642
|
+
*/
|
|
643
|
+
architectureDebt?: ProjectionArchitectureDebtV1;
|
|
644
|
+
/**
|
|
645
|
+
* Present on every `check` and `plan` result, and on no other: per-target change classification,
|
|
646
|
+
* sorted by path (#327). It never changes `status` or `files`.
|
|
647
|
+
*/
|
|
648
|
+
changes?: ProjectionTargetChangeV1[];
|
|
430
649
|
/**
|
|
431
650
|
* Present, as `true`, only when this response returns an apply this requestId already committed
|
|
432
651
|
* for the same request digest, without applying again (#265). It is excluded from
|
|
433
652
|
* `receiptDigest`, so a replay carries the committed receipt digest unchanged.
|
|
434
653
|
*/
|
|
435
654
|
replayed?: true;
|
|
655
|
+
/**
|
|
656
|
+
* Present exactly when `replayed` is: whether this response wrote projection-owned files (#325).
|
|
657
|
+
* `false` means it wrote nothing; the committed result is returned as recorded. `true` means
|
|
658
|
+
* the projection-owned outputs differed from the approved result at the approved snapshot, so
|
|
659
|
+
* the provider wrote the committed fixed point again under the original receipt. Excluded from
|
|
660
|
+
* `receiptDigest`.
|
|
661
|
+
*/
|
|
662
|
+
written?: boolean;
|
|
436
663
|
/**
|
|
437
664
|
* Present only on a `human-action-required` result whose request sent `acceptObservedMajorChange`
|
|
438
665
|
* and the provider declined it: `declined-unprovable-proof` means a capability's P1 or P2 proof is
|
|
@@ -441,6 +668,11 @@ export interface ProjectionResultV2 {
|
|
|
441
668
|
* so a caller can tell a declined flag from an absent one (#275).
|
|
442
669
|
*/
|
|
443
670
|
majorChangeAcceptance?: "declined-unprovable-proof";
|
|
671
|
+
/**
|
|
672
|
+
* Present exactly on `planned-partial` and `applied-partial` results, with `complete: false`; a
|
|
673
|
+
* result that covers every target omits it. Consumers gate on `projection-capability-isolation-v1`.
|
|
674
|
+
*/
|
|
675
|
+
coverage?: ProjectionCoverageV1;
|
|
444
676
|
receiptDigest: Sha256Digest;
|
|
445
677
|
}
|
|
446
678
|
|
|
@@ -623,9 +855,27 @@ export function projectionRequestInvariantIssues(input: ProjectionRequestV1): st
|
|
|
623
855
|
* `adopt` results never carry `files[].preview`, whatever their status.
|
|
624
856
|
*/
|
|
625
857
|
export function projectionResultRequestModeIssues(input: ProjectionResultV2, mode: ProjectionMode): string[] {
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
858
|
+
const issues: string[] = [];
|
|
859
|
+
if (mode !== "plan" && input.files.some((file) => file.preview !== undefined)) {
|
|
860
|
+
issues.push(`files[].preview is only allowed on a plan result, never when mode=${mode}`);
|
|
861
|
+
}
|
|
862
|
+
if (mode !== "check" && mode !== "plan" && input.architectureDebt !== undefined) {
|
|
863
|
+
issues.push(`architectureDebt is only allowed on a check or plan result, never when mode=${mode}`);
|
|
864
|
+
}
|
|
865
|
+
if (mode !== "check" && mode !== "plan" && input.changes !== undefined) {
|
|
866
|
+
issues.push(`changes is only allowed on a check or plan result, never when mode=${mode}`);
|
|
867
|
+
}
|
|
868
|
+
// The schema cannot condition on the request mode, so the presence promise is checked here.
|
|
869
|
+
if ((mode === "check" || mode === "plan") && input.changes === undefined) {
|
|
870
|
+
issues.push(`changes is required on every check or plan result, also when mode=${mode}`);
|
|
871
|
+
}
|
|
872
|
+
if (input.status === "planned-partial" && mode !== "check" && mode !== "plan") {
|
|
873
|
+
issues.push(`planned-partial is only allowed on a check or plan result, never when mode=${mode}`);
|
|
874
|
+
}
|
|
875
|
+
if (input.status === "applied-partial" && mode !== "apply") {
|
|
876
|
+
issues.push(`applied-partial is only allowed on an apply result, never when mode=${mode}`);
|
|
877
|
+
}
|
|
878
|
+
return issues;
|
|
629
879
|
}
|
|
630
880
|
|
|
631
881
|
export function projectionResultInvariantIssues(input: ProjectionResultV2): string[] {
|
|
@@ -647,6 +897,15 @@ export function projectionResultInvariantIssues(input: ProjectionResultV2): stri
|
|
|
647
897
|
if (!input.applyReceipt) issues.push("applied-reconcile-required status requires applyReceipt");
|
|
648
898
|
if (input.refreshSignals.length > 0) issues.push("applied-reconcile-required status cannot deliver refreshSignals");
|
|
649
899
|
}
|
|
900
|
+
const partial = (PROJECTION_PARTIAL_RESULT_STATUSES as readonly string[]).includes(input.status);
|
|
901
|
+
if (partial !== (input.coverage !== undefined)) issues.push("coverage must be present exactly when status is planned-partial or applied-partial");
|
|
902
|
+
if (input.coverage !== undefined) issues.push(...projectionCoverageIssues(input.coverage, input.files));
|
|
903
|
+
if (partial) {
|
|
904
|
+
// An incomplete projection is never an acceptance: it records no apply and signals no refresh.
|
|
905
|
+
if (input.applyReceipt !== undefined) issues.push(`${input.status} status never carries applyReceipt`);
|
|
906
|
+
if (input.refreshSignals.length > 0) issues.push(`${input.status} status carries no refreshSignals`);
|
|
907
|
+
}
|
|
908
|
+
const committedApply = input.applyReceipt !== undefined || input.status === "applied" || input.status === "applied-partial" || input.status === "applied-reconcile-required";
|
|
650
909
|
for (const [index, action] of input.humanActions.entries()) {
|
|
651
910
|
const prefix = `humanActions[${index}]`;
|
|
652
911
|
if (!(PROJECTION_HUMAN_ACTION_REASON_CODES as readonly string[]).includes(action.reasonCode)) issues.push(`${prefix}.reasonCode is unsupported`);
|
|
@@ -682,14 +941,30 @@ export function projectionResultInvariantIssues(input: ProjectionResultV2): stri
|
|
|
682
941
|
}
|
|
683
942
|
// The result does not carry its request mode; projectionResultRequestModeIssues binds a preview
|
|
684
943
|
// 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")) {
|
|
944
|
+
if (input.files.some((file) => file.preview !== undefined) && committedApply) {
|
|
687
945
|
issues.push("files[].preview is only allowed on a plan result, never on a committed apply");
|
|
688
946
|
}
|
|
689
947
|
if (input.priorCommittedApplies) {
|
|
690
948
|
issues.push(...projectionPriorCommittedAppliesIssues(input.priorCommittedApplies, input.requestId));
|
|
691
949
|
}
|
|
692
950
|
if (input.freshness) issues.push(...projectionFreshnessIssues(input.freshness));
|
|
951
|
+
if (input.architectureDebt) {
|
|
952
|
+
issues.push(...projectionArchitectureDebtIssues(input.architectureDebt));
|
|
953
|
+
if (committedApply) {
|
|
954
|
+
issues.push("architectureDebt is only allowed on a check or plan result, never on a committed apply");
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
if (input.changes) {
|
|
958
|
+
issues.push(...projectionTargetChangesIssues(input.changes, input.files));
|
|
959
|
+
if (committedApply) issues.push("changes is only allowed on a check or plan result, never on a committed apply");
|
|
960
|
+
// A carried-forward target is blocked, by the same reasons its coverage entry names.
|
|
961
|
+
for (const target of input.coverage?.blockedTargets ?? []) {
|
|
962
|
+
const entry = input.changes.find((candidate) => candidate.path === target.path);
|
|
963
|
+
if (!entry || entry.classification !== "blocked" || target.reasonCodes.some((code) => !entry.reasonCodes.includes(code))) {
|
|
964
|
+
issues.push(`changes must classify the carried-forward target ${target.path} as blocked with its coverage reason codes`);
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
}
|
|
693
968
|
if (input.majorChangeAcceptance !== undefined) {
|
|
694
969
|
if (input.majorChangeAcceptance !== "declined-unprovable-proof") issues.push("majorChangeAcceptance is unsupported");
|
|
695
970
|
if (input.status !== "human-action-required"
|
|
@@ -704,6 +979,8 @@ export function projectionResultInvariantIssues(input: ProjectionResultV2): stri
|
|
|
704
979
|
issues.push("replayed is only allowed on a committed apply result with applyReceipt");
|
|
705
980
|
}
|
|
706
981
|
}
|
|
982
|
+
if ((input.replayed !== undefined) !== (input.written !== undefined)) issues.push("written must be present exactly when replayed is present");
|
|
983
|
+
if (input.written !== undefined && typeof input.written !== "boolean") issues.push("written must be a boolean when present");
|
|
707
984
|
const { receiptDigest, ...receiptPayload } = input;
|
|
708
985
|
if (projectionResultReceiptDigest(receiptPayload) !== receiptDigest) issues.push("receiptDigest must match the canonical projection result payload");
|
|
709
986
|
for (const [index, signal] of input.refreshSignals.entries()) {
|
|
@@ -744,6 +1021,134 @@ function utf8ByteLength(value: string): number {
|
|
|
744
1021
|
return UTF8.encode(value).length;
|
|
745
1022
|
}
|
|
746
1023
|
|
|
1024
|
+
/**
|
|
1025
|
+
* Shape and canonical order of a partial result's `coverage`, and its binding to the result's
|
|
1026
|
+
* files: a carried-forward target is never also a written file.
|
|
1027
|
+
*/
|
|
1028
|
+
export function projectionCoverageIssues(input: ProjectionCoverageV1, files: readonly ProjectionFileResultV1[] = []): string[] {
|
|
1029
|
+
const issues = [
|
|
1030
|
+
...sortedUniqueIssues("coverage.blockedCapabilityIds", input.blockedCapabilityIds),
|
|
1031
|
+
...sortedUniqueIssues("coverage.blockedTargets.targetId", input.blockedTargets.map((target) => target.targetId)),
|
|
1032
|
+
...sortedUniqueIssues("coverage.appliedTargetIds", input.appliedTargetIds)
|
|
1033
|
+
];
|
|
1034
|
+
if (input.complete !== false) issues.push("coverage.complete must be false; a result that covers every target omits coverage");
|
|
1035
|
+
if (input.blockedCapabilityIds.length === 0) issues.push("coverage.blockedCapabilityIds must name at least one capability");
|
|
1036
|
+
if (input.blockedTargets.length === 0) issues.push("coverage.blockedTargets must name at least one target");
|
|
1037
|
+
if (input.blockedCapabilityIds.some((id) => typeof id !== "string" || id.trim() === "")) issues.push("coverage.blockedCapabilityIds must not contain empty ids");
|
|
1038
|
+
if (input.appliedTargetIds.some((id) => typeof id !== "string" || id.trim() === "")) issues.push("coverage.appliedTargetIds must not contain empty ids");
|
|
1039
|
+
const blockedCapabilities = new Set(input.blockedCapabilityIds);
|
|
1040
|
+
const blockingCapabilities = new Set<string>();
|
|
1041
|
+
const blockedPaths = new Set<string>();
|
|
1042
|
+
const reasons = new Set<string>(PROJECTION_COVERAGE_REASON_CODES);
|
|
1043
|
+
for (const [index, target] of input.blockedTargets.entries()) {
|
|
1044
|
+
const prefix = `coverage.blockedTargets[${index}]`;
|
|
1045
|
+
if (typeof target.targetId !== "string" || target.targetId.trim() === "") issues.push(`${prefix}.targetId must not be empty`);
|
|
1046
|
+
if (!isRepoRelativePosixPath(target.path)) issues.push(`${prefix}.path must be a repository-relative POSIX path`);
|
|
1047
|
+
else if (blockedPaths.has(target.path)) issues.push(`${prefix}.path is named by another blocked target`);
|
|
1048
|
+
blockedPaths.add(target.path);
|
|
1049
|
+
issues.push(...sortedUniqueIssues(`${prefix}.capabilityIds`, target.capabilityIds), ...sortedUniqueIssues(`${prefix}.reasonCodes`, target.reasonCodes));
|
|
1050
|
+
if (target.capabilityIds.length === 0) issues.push(`${prefix}.capabilityIds must name at least one capability`);
|
|
1051
|
+
if (target.reasonCodes.length === 0) issues.push(`${prefix}.reasonCodes must name at least one reason`);
|
|
1052
|
+
for (const capabilityId of target.capabilityIds) {
|
|
1053
|
+
if (!blockedCapabilities.has(capabilityId)) issues.push(`${prefix}.capabilityIds names ${capabilityId}, which is not a blocked capability`);
|
|
1054
|
+
blockingCapabilities.add(capabilityId);
|
|
1055
|
+
}
|
|
1056
|
+
const reason = target.reasonCodes.find((code) => !reasons.has(code));
|
|
1057
|
+
if (reason !== undefined) issues.push(`${prefix}.reasonCodes contains unsupported reason: ${reason}`);
|
|
1058
|
+
if (input.appliedTargetIds.includes(target.targetId)) issues.push(`${prefix}.targetId must not also be an applied target`);
|
|
1059
|
+
}
|
|
1060
|
+
for (const capabilityId of input.blockedCapabilityIds) {
|
|
1061
|
+
if (!blockingCapabilities.has(capabilityId)) issues.push(`coverage.blockedCapabilityIds names ${capabilityId}, which blocks no target`);
|
|
1062
|
+
}
|
|
1063
|
+
for (const file of files) {
|
|
1064
|
+
if (blockedPaths.has(file.path)) issues.push(`files[] must not write the carried-forward target path ${file.path}`);
|
|
1065
|
+
}
|
|
1066
|
+
return issues;
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
const TARGET_CHANGE_BLOCKING_REASONS = new Set<string>(PROJECTION_TARGET_CHANGE_BLOCKING_REASON_CODES);
|
|
1070
|
+
const TARGET_CHANGE_UNKNOWN_REASONS = ["baseline-missing", "target-row-missing"] as const;
|
|
1071
|
+
const TARGET_CHANGE_OUTPUT_REASONS = ["file-bytes-changed", "rendered-output-changed", "target-added", "target-removed"] as const;
|
|
1072
|
+
const TARGET_CHANGE_PROOF_REASONS = ["flow-proof-changed", "selector-evidence-changed"] as const;
|
|
1073
|
+
const TARGET_CHANGE_SEMANTIC_REASONS = ["model-digest-changed", "semantic-fingerprint-changed"] as const;
|
|
1074
|
+
|
|
1075
|
+
/**
|
|
1076
|
+
* The one precedence rule of a target change: blocked > proof-change (`proofChanged === true`) >
|
|
1077
|
+
* baseline-missing > semantic-change > output-change > stamp-only > unchanged. A known proof change
|
|
1078
|
+
* is never hidden by an unknown comparison (a target without a committed row can still carry a
|
|
1079
|
+
* moved flow proof); an unknown comparison outranks everything below the proof change and is never
|
|
1080
|
+
* `unchanged`. `stamp-only` therefore means the stamps moved while the output bytes, and the
|
|
1081
|
+
* semantic and proof comparisons, are known and unchanged on a target nothing blocks.
|
|
1082
|
+
*/
|
|
1083
|
+
export function projectionTargetChangeClassification(
|
|
1084
|
+
input: Pick<ProjectionTargetChangeV1, "reasonCodes" | "stampChanged" | "outputChanged" | "semanticChanged" | "proofChanged">
|
|
1085
|
+
): ProjectionTargetChangeClassification {
|
|
1086
|
+
if (input.reasonCodes.some((code) => TARGET_CHANGE_BLOCKING_REASONS.has(code))) return "blocked";
|
|
1087
|
+
if (input.proofChanged === true) return "proof-change";
|
|
1088
|
+
if (input.stampChanged === null || input.semanticChanged === null || input.proofChanged === null) return "baseline-missing";
|
|
1089
|
+
if (input.semanticChanged) return "semantic-change";
|
|
1090
|
+
if (input.outputChanged) return "output-change";
|
|
1091
|
+
if (input.stampChanged) return "stamp-only";
|
|
1092
|
+
return "unchanged";
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
/**
|
|
1096
|
+
* Shape, canonical order and internal consistency of a result's `changes`, and their binding to its
|
|
1097
|
+
* `files`: a change with a planned action names the `files[]` entry of its path with that action; a
|
|
1098
|
+
* `files[]` entry a change names but does not plan is a path rejected for adoption or ownership
|
|
1099
|
+
* repair, so that change is blocked by that rejection, and still reports the bytes that entry
|
|
1100
|
+
* changes (`outputChanged`). Every `files[]` entry that changes a file has a change of its path,
|
|
1101
|
+
* except the projection manifest, which is never a target.
|
|
1102
|
+
*/
|
|
1103
|
+
export function projectionTargetChangesIssues(input: readonly ProjectionTargetChangeV1[], files: readonly ProjectionFileResultV1[] = []): string[] {
|
|
1104
|
+
const issues = sortedUniqueIssues("changes.path", input.map((entry) => entry.path));
|
|
1105
|
+
const fileByPath = new Map(files.map((file) => [file.path, file]));
|
|
1106
|
+
const reasons = new Set<string>(PROJECTION_TARGET_CHANGE_REASON_CODES);
|
|
1107
|
+
for (const [index, entry] of input.entries()) {
|
|
1108
|
+
const prefix = `changes[${index}]`;
|
|
1109
|
+
const has = (codes: readonly string[]) => codes.some((code) => entry.reasonCodes.includes(code as ProjectionTargetChangeReasonCode));
|
|
1110
|
+
if (typeof entry.targetId !== "string" || entry.targetId.trim() === "") issues.push(`${prefix}.targetId must not be empty`);
|
|
1111
|
+
if (!isRepoRelativePosixPath(entry.path)) issues.push(`${prefix}.path must be a repository-relative POSIX path`);
|
|
1112
|
+
issues.push(...sortedUniqueIssues(`${prefix}.affectedCapabilityIds`, entry.affectedCapabilityIds), ...sortedUniqueIssues(`${prefix}.reasonCodes`, entry.reasonCodes));
|
|
1113
|
+
if (entry.affectedCapabilityIds.some((id) => typeof id !== "string" || id.trim() === "")) issues.push(`${prefix}.affectedCapabilityIds must not contain empty ids`);
|
|
1114
|
+
const unsupported = entry.reasonCodes.find((code) => !reasons.has(code));
|
|
1115
|
+
if (unsupported !== undefined) issues.push(`${prefix}.reasonCodes contains an unsupported code: ${unsupported}`);
|
|
1116
|
+
for (const field of ["stampChanged", "semanticChanged", "proofChanged"] as const) {
|
|
1117
|
+
if (entry[field] !== null && typeof entry[field] !== "boolean") issues.push(`${prefix}.${field} must be a boolean or null`);
|
|
1118
|
+
}
|
|
1119
|
+
if (typeof entry.outputChanged !== "boolean") issues.push(`${prefix}.outputChanged must be a boolean`);
|
|
1120
|
+
if (!(PROJECTION_TARGET_CHANGE_PLANNED_ACTIONS as readonly string[]).includes(entry.plannedAction)) issues.push(`${prefix}.plannedAction is unsupported`);
|
|
1121
|
+
if ((entry.stampChanged === true) !== has(["source-stamp-changed"])) issues.push(`${prefix}.stampChanged must be true exactly when source-stamp-changed is reported`);
|
|
1122
|
+
if ((entry.semanticChanged === true) !== has(TARGET_CHANGE_SEMANTIC_REASONS)) issues.push(`${prefix}.semanticChanged must be true exactly when semantic-fingerprint-changed or model-digest-changed is reported`);
|
|
1123
|
+
if ((entry.proofChanged === true) !== has(TARGET_CHANGE_PROOF_REASONS)) issues.push(`${prefix}.proofChanged must be true exactly when flow-proof-changed or selector-evidence-changed is reported`);
|
|
1124
|
+
if (entry.outputChanged !== has(TARGET_CHANGE_OUTPUT_REASONS)) issues.push(`${prefix}.outputChanged must be true exactly when an output reason code is reported`);
|
|
1125
|
+
if ((entry.stampChanged === null || entry.semanticChanged === null || entry.proofChanged === null) !== has(TARGET_CHANGE_UNKNOWN_REASONS)) {
|
|
1126
|
+
issues.push(`${prefix} must report baseline-missing or target-row-missing exactly when a comparison is unknown`);
|
|
1127
|
+
}
|
|
1128
|
+
const derived = projectionTargetChangeClassification(entry);
|
|
1129
|
+
if (entry.classification !== derived) issues.push(`${prefix}.classification must be ${derived} for its comparisons and reason codes`);
|
|
1130
|
+
if ((entry.classification === "unchanged") !== (entry.reasonCodes.length === 0)) issues.push(`${prefix}.reasonCodes must be empty exactly when classification is unchanged`);
|
|
1131
|
+
if (entry.classification === "unchanged" && entry.affectedCapabilityIds.length > 0) issues.push(`${prefix} is unchanged, so affectedCapabilityIds must be empty`);
|
|
1132
|
+
if (entry.classification === "blocked" && entry.plannedAction !== "none") issues.push(`${prefix} is blocked, so plannedAction must be none`);
|
|
1133
|
+
if (entry.plannedAction !== "none" && entry.outputChanged !== true) issues.push(`${prefix} writes its file, so outputChanged must be true`);
|
|
1134
|
+
if (entry.path === ARCHITECTURE_DOCS_PROJECTION_MANIFEST_PATH) issues.push(`${prefix} names the projection manifest, which is not a target`);
|
|
1135
|
+
const file = fileByPath.get(entry.path);
|
|
1136
|
+
const fileAction = file === undefined || file.action === "unchanged" ? "none" : file.action;
|
|
1137
|
+
const rejected = has(["adoption-required", "ambiguous-ownership"]);
|
|
1138
|
+
if (entry.plannedAction !== fileAction && !(rejected && entry.plannedAction === "none" && fileAction === "update")) {
|
|
1139
|
+
issues.push(`${prefix}.plannedAction must be ${fileAction}, the files[] action for ${entry.path}`);
|
|
1140
|
+
}
|
|
1141
|
+
// The file result proves the bytes change, whether or not this run may write them.
|
|
1142
|
+
if (fileAction !== "none" && entry.outputChanged !== true) issues.push(`${prefix}.outputChanged must be true, the files[] entry for ${entry.path} changes its bytes`);
|
|
1143
|
+
}
|
|
1144
|
+
const changedPaths = new Set(input.map((entry) => entry.path));
|
|
1145
|
+
for (const file of files) {
|
|
1146
|
+
if (file.action === "unchanged" || file.path === ARCHITECTURE_DOCS_PROJECTION_MANIFEST_PATH || changedPaths.has(file.path)) continue;
|
|
1147
|
+
issues.push(`changes must name the files[] ${file.action} of ${file.path}; only the projection manifest has none`);
|
|
1148
|
+
}
|
|
1149
|
+
return issues;
|
|
1150
|
+
}
|
|
1151
|
+
|
|
747
1152
|
export function projectionFreshnessIssues(input: ProjectionFreshnessV1): string[] {
|
|
748
1153
|
const issues = [
|
|
749
1154
|
...sortedUniqueIssues("freshness.reasonCodes", input.reasonCodes),
|
|
@@ -764,6 +1169,31 @@ export function projectionFreshnessIssues(input: ProjectionFreshnessV1): string[
|
|
|
764
1169
|
return issues;
|
|
765
1170
|
}
|
|
766
1171
|
|
|
1172
|
+
export function projectionArchitectureDebtIssues(input: ProjectionArchitectureDebtV1): string[] {
|
|
1173
|
+
const issues = [
|
|
1174
|
+
...sortedUniqueIssues("architectureDebt.coverageReasonCodes", input.coverageReasonCodes),
|
|
1175
|
+
...sortedUniqueIssues("architectureDebt.items", input.items.map((item) => JSON.stringify([item.fromCapabilityId, item.toCapabilityId])))
|
|
1176
|
+
];
|
|
1177
|
+
if (!(["complete", "partial", "unknown"] as const).includes(input.coverage)) issues.push("architectureDebt.coverage is unsupported");
|
|
1178
|
+
const unsupported = input.coverageReasonCodes.find((code) => !(PROJECTION_ARCHITECTURE_DEBT_COVERAGE_REASON_CODES as readonly string[]).includes(code));
|
|
1179
|
+
if (unsupported) issues.push(`architectureDebt.coverageReasonCodes contains an unsupported code: ${unsupported}`);
|
|
1180
|
+
if ((input.coverage === "complete") !== (input.coverageReasonCodes.length === 0)) {
|
|
1181
|
+
issues.push("architectureDebt.coverageReasonCodes must be empty exactly when coverage is complete");
|
|
1182
|
+
}
|
|
1183
|
+
if ((input.coverage === "unknown") !== input.coverageReasonCodes.includes("code-facts-unavailable")) {
|
|
1184
|
+
issues.push("architectureDebt.coverage must be unknown exactly when code-facts-unavailable is reported");
|
|
1185
|
+
}
|
|
1186
|
+
if (input.coverage === "unknown" && input.items.length > 0) issues.push("architectureDebt.items must be empty when coverage is unknown");
|
|
1187
|
+
for (const [index, item] of input.items.entries()) {
|
|
1188
|
+
const prefix = `architectureDebt.items[${index}]`;
|
|
1189
|
+
if (!(PROJECTION_ARCHITECTURE_DEBT_REASON_CODES as readonly string[]).includes(item.reasonCode)) issues.push(`${prefix}.reasonCode is unsupported`);
|
|
1190
|
+
if (item.fromCapabilityId.trim() === "" || item.toCapabilityId.trim() === "") issues.push(`${prefix} must name two capabilities`);
|
|
1191
|
+
else if (item.fromCapabilityId === item.toCapabilityId) issues.push(`${prefix} must name two different capabilities`);
|
|
1192
|
+
if (!Number.isSafeInteger(item.importEdgeCount) || item.importEdgeCount < 1) issues.push(`${prefix}.importEdgeCount must be a positive integer`);
|
|
1193
|
+
}
|
|
1194
|
+
return issues;
|
|
1195
|
+
}
|
|
1196
|
+
|
|
767
1197
|
export function projectionPriorCommittedAppliesIssues(
|
|
768
1198
|
input: readonly ProjectionPriorCommittedApplyV1[],
|
|
769
1199
|
requestId: string
|
|
@@ -804,7 +1234,7 @@ export function projectionPriorCommittedAppliesIssues(
|
|
|
804
1234
|
* projectionReceiptDigest, which projectionResultInvariantIssues verifies.
|
|
805
1235
|
*/
|
|
806
1236
|
export function projectionResultReceiptDigest(input: Omit<ProjectionResultV2, "receiptDigest">): Sha256Digest {
|
|
807
|
-
const { replayed: _replayed, ...payload } = input;
|
|
1237
|
+
const { replayed: _replayed, written: _written, ...payload } = input;
|
|
808
1238
|
const refreshSignals = payload.refreshSignals.map(({ projectionReceiptDigest: _projectionReceiptDigest, ...signal }) => signal);
|
|
809
1239
|
return digestJson({ ...payload, refreshSignals } as unknown as Json) as Sha256Digest;
|
|
810
1240
|
}
|
|
@@ -866,6 +1296,8 @@ export function projectionApplyReceiptInvariantIssues(input: ProjectionApplyRece
|
|
|
866
1296
|
issues.push("projection apply identity must bind the committed files and original refresh signals");
|
|
867
1297
|
}
|
|
868
1298
|
if (input.result.status !== "applied") issues.push("projection apply receipt result status must be applied");
|
|
1299
|
+
// A partial projection is never accepted, so no receipt ever records one.
|
|
1300
|
+
if (input.result.coverage !== undefined) issues.push("projection apply receipt result must cover every target");
|
|
869
1301
|
if (digestJson(input.result.applyReceipt as unknown as Json) !== digestJson(input.identity as unknown as Json)) {
|
|
870
1302
|
issues.push("projection apply receipt result must carry the same apply identity");
|
|
871
1303
|
}
|