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
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ProjectionTargetFormat, ProjectionTargetOwnership, ProjectionTargetScopeKind, ProjectionTargetType } from "./ledger";
|
|
2
2
|
import {
|
|
3
3
|
ARCHITECTURE_DOCS_RENDERER_VERSION,
|
|
4
|
+
PROJECTION_REQUEST_SCHEMA_VERSION,
|
|
4
5
|
ARCHITECTURE_SEMANTIC_FACETS,
|
|
5
6
|
isArchitectureCapabilityProofStatus,
|
|
6
7
|
type ArchitectureCapabilityProofStatusV1,
|
|
@@ -8,20 +9,37 @@ import {
|
|
|
8
9
|
type ArchitectureSemanticFacet,
|
|
9
10
|
type Sha256Digest
|
|
10
11
|
} from "./projection";
|
|
11
|
-
import { isRepoRelativePosixPath } from "./schema";
|
|
12
|
+
import { digestJson, isRepoRelativePosixPath, type Json } from "./schema";
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* The committed projection manifest, `docs/architecture/.projection-manifest.json` (#264). The
|
|
15
|
-
* projection engine writes it and reads its semantic baseline and
|
|
16
|
-
* the only definition of its shape.
|
|
16
|
+
* projection engine writes it and reads its semantic baseline and per-target rows back; this module
|
|
17
|
+
* is the only definition of its shape, of its byte layout and of every value derived from it.
|
|
17
18
|
*
|
|
18
|
-
* `schemaVersion` is `
|
|
19
|
-
*
|
|
20
|
-
*
|
|
19
|
+
* `schemaVersion` is `v3` (#327). The file holds no global digest of all owned sources: header
|
|
20
|
+
* lines change only on a protocol upgrade, the one Git-visible anchor is
|
|
21
|
+
* `semanticBaseline.modelDigest`, and every other line belongs to one capability or one target.
|
|
22
|
+
* Two pull requests that touch different capabilities therefore rewrite disjoint lines. The global
|
|
23
|
+
* digests a run still needs (source tree, flow proof, projection) are computed from the rows when
|
|
24
|
+
* the manifest is read (`architectureDocsProjectionManifestDigests`).
|
|
25
|
+
*
|
|
26
|
+
* A `v1` or `v2` manifest is read only for its `schemaVersion` and refused with
|
|
27
|
+
* `projection-manifest-upgrade-required`; nothing else in it is read.
|
|
21
28
|
*/
|
|
22
|
-
export const
|
|
23
|
-
|
|
29
|
+
export const ARCHITECTURE_DOCS_PROJECTION_MANIFEST_SCHEMA_VERSION = "archcontext.architecture-docs-projection-manifest/v3" as const;
|
|
30
|
+
/** Manifest versions earlier releases wrote. Only their `schemaVersion` is read, to refuse them. */
|
|
31
|
+
export const SUPERSEDED_ARCHITECTURE_DOCS_PROJECTION_MANIFEST_SCHEMA_VERSIONS = [
|
|
32
|
+
"archcontext.architecture-docs-projection-manifest/v1",
|
|
33
|
+
"archcontext.architecture-docs-projection-manifest/v2"
|
|
34
|
+
] as const;
|
|
35
|
+
/** The reason code of a refused superseded manifest; the operator re-baselines once with `archctx docs apply --approved`. */
|
|
36
|
+
export const PROJECTION_MANIFEST_UPGRADE_REQUIRED = "projection-manifest-upgrade-required" as const;
|
|
37
|
+
/** The reason code of a current-format manifest that fails its contract; restore it from Git. */
|
|
38
|
+
export const PROJECTION_MANIFEST_INVALID = "projection-manifest-invalid" as const;
|
|
39
|
+
/** The reason code of a declared source footprint that no entity-summary target carries a stamp for. */
|
|
40
|
+
export const PROJECTION_FOOTPRINT_UNCOVERED = "projection-footprint-uncovered" as const;
|
|
24
41
|
export const ARCHITECTURE_DOCS_PROJECTION_PROVENANCE_SCHEMA_VERSION = "archcontext.architecture-docs-projection-provenance/v3" as const;
|
|
42
|
+
export const ARCHITECTURE_DOCS_LAYOUT_VERSION_V1 = "archcontext.docs-layout/v1" as const;
|
|
25
43
|
export const ARCHITECTURE_SEMANTIC_STATE_SCHEMA_VERSION = "archcontext.architecture-semantic-state/v1" as const;
|
|
26
44
|
export const ARCHITECTURE_DOCS_PROJECTION_PROFILES = ["default", "repo-harness/v1"] as const;
|
|
27
45
|
|
|
@@ -43,33 +61,26 @@ export interface ArchitectureSemanticStateV1 {
|
|
|
43
61
|
flowProofFingerprint: string;
|
|
44
62
|
}
|
|
45
63
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
64
|
+
/**
|
|
65
|
+
* The inputs besides the model that the flow proofs of one projection were compiled from, per
|
|
66
|
+
* capability. Committed acceptance compares the manifest's value with a fresh measurement: a
|
|
67
|
+
* capability whose proof moved while its semantic fingerprint held is explained by the journaled
|
|
68
|
+
* model only when its member stamps, its selector-evidence digest and the renderer are unchanged.
|
|
69
|
+
*/
|
|
70
|
+
export interface ArchitectureProjectionProofEvidenceV1 {
|
|
52
71
|
rendererVersion: string;
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
digests: ArchitectureDigestSetV1;
|
|
58
|
-
/**
|
|
59
|
-
* The non-model proof inputs this baseline was rendered from, recorded fresh on every render.
|
|
60
|
-
* Absent in manifests written before it existed.
|
|
61
|
-
*/
|
|
62
|
-
evidence?: ArchitectureProofEvidenceDigestsV1;
|
|
72
|
+
/** Per capability: digest of the proof-relevant selector evidence its flows cite. */
|
|
73
|
+
selectorEvidenceDigests: Record<string, string>;
|
|
74
|
+
/** Per node that declares `source.include`: the content digest of its footprint. */
|
|
75
|
+
sourceStamps: Record<string, string>;
|
|
63
76
|
}
|
|
64
77
|
|
|
65
78
|
/**
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
* CodeGraph evidence digest, the indexed-worktree digest and
|
|
70
|
-
*
|
|
71
|
-
* facts (the projection runtime snapshot) and never reach the committed manifest. A provenance of
|
|
72
|
-
* any other schemaVersion is not read as this one; the next projection rewrites it.
|
|
79
|
+
* Projection provenance of one run. Runtime only: receipts, recovery bindings and envelopes carry
|
|
80
|
+
* it, the committed manifest does not (#327). It records machine-independent values only: content
|
|
81
|
+
* digests of the model and the declared sources, plus renderer, layout and CodeGraph versions.
|
|
82
|
+
* HEAD, the full-worktree digest, the CodeGraph evidence digest, the indexed-worktree digest and
|
|
83
|
+
* the CodeGraph status are runtime snapshot facts (#277).
|
|
73
84
|
*/
|
|
74
85
|
export interface ArchitectureDocumentationProjectionProvenanceV3 {
|
|
75
86
|
schemaVersion: typeof ARCHITECTURE_DOCS_PROJECTION_PROVENANCE_SCHEMA_VERSION;
|
|
@@ -77,14 +88,16 @@ export interface ArchitectureDocumentationProjectionProvenanceV3 {
|
|
|
77
88
|
modelDigest: string;
|
|
78
89
|
projectionInputDigest: string;
|
|
79
90
|
rendererVersion: typeof ARCHITECTURE_DOCS_RENDERER_VERSION;
|
|
80
|
-
layoutVersion:
|
|
91
|
+
layoutVersion: typeof ARCHITECTURE_DOCS_LAYOUT_VERSION_V1;
|
|
81
92
|
/** Reproducible CodeGraph identity only: package name and version (#266, #277). */
|
|
82
|
-
generatedFrom:
|
|
83
|
-
codeGraphPackage: string;
|
|
84
|
-
codeGraphVersion: string;
|
|
85
|
-
};
|
|
93
|
+
generatedFrom: ArchitectureDocsProjectionGeneratedFromV1;
|
|
86
94
|
}
|
|
87
95
|
|
|
96
|
+
export type ArchitectureDocsProjectionGeneratedFromV1 = {
|
|
97
|
+
codeGraphPackage: string;
|
|
98
|
+
codeGraphVersion: string;
|
|
99
|
+
};
|
|
100
|
+
|
|
88
101
|
/**
|
|
89
102
|
* One 1–2–5 magnitude bucket: the half-open range `[lower, upper)` that contains a measured count.
|
|
90
103
|
* `{ lower: 0, upper: 1 }` holds only zero; otherwise `lower` is `1`, `2` or `5` times a power of ten
|
|
@@ -104,37 +117,54 @@ export interface ArchitectureCapabilityScaleV1 {
|
|
|
104
117
|
lineCountBucket: ArchitectureScaleBucketV1;
|
|
105
118
|
}
|
|
106
119
|
|
|
107
|
-
|
|
108
|
-
|
|
120
|
+
/**
|
|
121
|
+
* One target row of the committed manifest, keyed by `targetId`. The targetId and the renderer
|
|
122
|
+
* version are not repeated in the row: the key and the manifest header carry them.
|
|
123
|
+
*/
|
|
124
|
+
export interface ArchitectureDocsProjectionManifestTargetV3 {
|
|
109
125
|
type: ProjectionTargetType;
|
|
110
126
|
scope: { kind: ProjectionTargetScopeKind; id?: string; entityKind?: string };
|
|
111
127
|
path: string;
|
|
112
128
|
ownership: ProjectionTargetOwnership;
|
|
113
|
-
rendererVersion: typeof ARCHITECTURE_DOCS_RENDERER_VERSION;
|
|
114
129
|
format: ProjectionTargetFormat;
|
|
115
|
-
|
|
130
|
+
/** `digestJson({ targetId, body })` of the generated body: the region of a `mixed` target, the whole file of a `generated` one. */
|
|
116
131
|
outputDigest: string;
|
|
117
132
|
/**
|
|
118
|
-
* `entity-summary` targets
|
|
119
|
-
*
|
|
133
|
+
* `entity-summary` targets only: the content digest of every declared source footprint this
|
|
134
|
+
* target documents, by node id. A footprint belongs to its node's nearest ancestor-or-self that
|
|
135
|
+
* has an entity-summary target, so every declared footprint is stamped on exactly one row.
|
|
120
136
|
*/
|
|
121
|
-
|
|
122
|
-
/** `
|
|
137
|
+
sourceStamps?: Record<string, string>;
|
|
138
|
+
/** Present exactly when `sourceStamps` stamps the target's own node: the scale buckets its document prints (#264). */
|
|
123
139
|
scale?: ArchitectureCapabilityScaleV1;
|
|
124
140
|
}
|
|
125
141
|
|
|
126
|
-
|
|
142
|
+
/** One capability of the committed semantic baseline. */
|
|
143
|
+
export interface ArchitectureDocsProjectionManifestCapabilityV3 extends ArchitectureCapabilitySemanticStateV1 {
|
|
144
|
+
/** Digest of the proof-relevant selector evidence for the selectors this capability's flows cite. */
|
|
145
|
+
selectorEvidenceDigest: string;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export interface ArchitectureDocsProjectionManifestV3 {
|
|
127
149
|
schemaVersion: typeof ARCHITECTURE_DOCS_PROJECTION_MANIFEST_SCHEMA_VERSION;
|
|
128
|
-
|
|
150
|
+
/**
|
|
151
|
+
* The renderer that wrote this manifest. Any renderer is readable: a renderer bump re-renders
|
|
152
|
+
* the owned outputs (drift reports them stale) and the per-capability proof check compares it.
|
|
153
|
+
*/
|
|
154
|
+
rendererVersion: string;
|
|
155
|
+
/** Optional repository-declared engine floors; preserved by every render. */
|
|
156
|
+
minimumRendererVersion?: string;
|
|
157
|
+
minimumProtocolVersion?: string;
|
|
129
158
|
profile: ArchitectureDocsProjectionProfile;
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
159
|
+
layoutVersion: typeof ARCHITECTURE_DOCS_LAYOUT_VERSION_V1;
|
|
160
|
+
generatedFrom: ArchitectureDocsProjectionGeneratedFromV1;
|
|
161
|
+
semanticBaseline: {
|
|
162
|
+
/** The one Git-visible anchor: the digest of the model this baseline was compiled from. */
|
|
163
|
+
modelDigest: string;
|
|
164
|
+
/** Sorted and unique by capabilityId. */
|
|
165
|
+
capabilities: ArchitectureDocsProjectionManifestCapabilityV3[];
|
|
166
|
+
};
|
|
167
|
+
targets: Record<string, ArchitectureDocsProjectionManifestTargetV3>;
|
|
138
168
|
}
|
|
139
169
|
|
|
140
170
|
const SHA256 = /^sha256:[a-f0-9]{64}$/;
|
|
@@ -158,28 +188,191 @@ export function architectureDocsProjectionManifestIssues(value: unknown): string
|
|
|
158
188
|
const issues: string[] = [];
|
|
159
189
|
const manifest = record(value);
|
|
160
190
|
if (!manifest) return ["manifest must be an object"];
|
|
161
|
-
issues.push(...exactKeys(manifest, ["schemaVersion", "rendererVersion", "profile", "
|
|
191
|
+
issues.push(...exactKeys(manifest, ["schemaVersion", "rendererVersion", "profile", "layoutVersion", "generatedFrom", "semanticBaseline", "targets"], ["minimumRendererVersion", "minimumProtocolVersion"], "manifest"));
|
|
162
192
|
if (manifest.schemaVersion !== ARCHITECTURE_DOCS_PROJECTION_MANIFEST_SCHEMA_VERSION) issues.push("manifest.schemaVersion is unsupported");
|
|
163
|
-
if (manifest.rendererVersion
|
|
193
|
+
if (!isNonEmptyString(manifest.rendererVersion)) issues.push("manifest.rendererVersion must be a non-empty string");
|
|
194
|
+
for (const [key, family] of [["minimumRendererVersion", "archcontext.docs-renderer/v"], ["minimumProtocolVersion", "archcontext.projection-request/v"]] as const) {
|
|
195
|
+
if (Object.hasOwn(manifest, key) && versionNumber(manifest[key], family) === undefined) issues.push(`manifest.${key} must name a supported version family`);
|
|
196
|
+
}
|
|
164
197
|
if (!(ARCHITECTURE_DOCS_PROJECTION_PROFILES as readonly unknown[]).includes(manifest.profile)) issues.push("manifest.profile is unsupported");
|
|
165
|
-
|
|
166
|
-
|
|
198
|
+
if (manifest.layoutVersion !== ARCHITECTURE_DOCS_LAYOUT_VERSION_V1) issues.push("manifest.layoutVersion is unsupported");
|
|
199
|
+
issues.push(...generatedFromIssues(manifest.generatedFrom, "manifest.generatedFrom"));
|
|
200
|
+
const baseline = record(manifest.semanticBaseline);
|
|
201
|
+
if (!baseline) issues.push("manifest.semanticBaseline must be an object");
|
|
202
|
+
else {
|
|
203
|
+
issues.push(...exactKeys(baseline, ["modelDigest", "capabilities"], [], "manifest.semanticBaseline"));
|
|
204
|
+
if (!isDigest(baseline.modelDigest)) issues.push("manifest.semanticBaseline.modelDigest must be a SHA-256 digest");
|
|
205
|
+
issues.push(...capabilityEntriesIssues(baseline.capabilities, "manifest.semanticBaseline.capabilities", true));
|
|
167
206
|
}
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
if (!Array.isArray(targets)) {
|
|
172
|
-
issues.push("manifest.targets must be an array");
|
|
207
|
+
const targets = record(manifest.targets);
|
|
208
|
+
if (!targets) {
|
|
209
|
+
issues.push("manifest.targets must be an object keyed by targetId");
|
|
173
210
|
return issues;
|
|
174
211
|
}
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
const
|
|
178
|
-
|
|
179
|
-
|
|
212
|
+
const paths = new Set<string>();
|
|
213
|
+
const stampOwners = new Map<string, string>();
|
|
214
|
+
for (const [targetId, row] of Object.entries(targets)) {
|
|
215
|
+
const prefix = `manifest.targets[${JSON.stringify(targetId)}]`;
|
|
216
|
+
if (!isNonEmptyString(targetId)) issues.push("manifest.targets keys must be non-empty targetIds");
|
|
217
|
+
issues.push(...manifestTargetIssues(row, prefix));
|
|
218
|
+
const path = record(row)?.path;
|
|
219
|
+
if (typeof path === "string") {
|
|
220
|
+
if (paths.has(path)) issues.push(`${prefix}.path is recorded by another target`);
|
|
221
|
+
paths.add(path);
|
|
222
|
+
}
|
|
223
|
+
for (const nodeId of Object.keys(record(record(row)?.sourceStamps) ?? {})) {
|
|
224
|
+
const owner = stampOwners.get(nodeId);
|
|
225
|
+
if (owner !== undefined) issues.push(`${prefix}.sourceStamps.${nodeId} is already stamped on ${owner}`);
|
|
226
|
+
else stampOwners.set(nodeId, targetId);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
180
229
|
return issues;
|
|
181
230
|
}
|
|
182
231
|
|
|
232
|
+
/**
|
|
233
|
+
* Reads only the `schemaVersion` of a manifest body: the one field this release reads from a
|
|
234
|
+
* manifest it does not write. Undefined when the body is not a JSON object or carries no string
|
|
235
|
+
* `schemaVersion`.
|
|
236
|
+
*/
|
|
237
|
+
export function architectureDocsProjectionManifestSchemaVersion(body: string): string | undefined {
|
|
238
|
+
let parsed: unknown;
|
|
239
|
+
try {
|
|
240
|
+
parsed = JSON.parse(body);
|
|
241
|
+
} catch {
|
|
242
|
+
return undefined;
|
|
243
|
+
}
|
|
244
|
+
const schemaVersion = record(parsed)?.schemaVersion;
|
|
245
|
+
return typeof schemaVersion === "string" ? schemaVersion : undefined;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/** True for a manifest version an earlier release wrote; such a manifest is refused, never read. */
|
|
249
|
+
export function isSupersededArchitectureDocsProjectionManifestSchemaVersion(schemaVersion: string | undefined): boolean {
|
|
250
|
+
return (SUPERSEDED_ARCHITECTURE_DOCS_PROJECTION_MANIFEST_SCHEMA_VERSIONS as readonly (string | undefined)[]).includes(schemaVersion);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* The committed byte layout. JSON, with one rule beyond `JSON.stringify`: every value that one
|
|
255
|
+
* capability or one target owns sits on lines no other capability or target shares, and an
|
|
256
|
+
* unchanged line separates the lines of any two owners. Git merges two branches that touch
|
|
257
|
+
* different owners without a conflict, because a merge conflicts on adjacent changed lines too.
|
|
258
|
+
*
|
|
259
|
+
* - Header lines (`schemaVersion`, `rendererVersion`, `profile`, `layoutVersion`, `generatedFrom`)
|
|
260
|
+
* change only on a protocol upgrade.
|
|
261
|
+
* - `semanticBaseline.modelDigest` is the one shared line a model change rewrites; two concurrent
|
|
262
|
+
* model changes conflict there, as they should.
|
|
263
|
+
* - Each capability entry is indented JSON and starts with its own `capabilityId` line.
|
|
264
|
+
* - Each target row is one line, after a line holding its `targetId` key, in canonical key order.
|
|
265
|
+
*/
|
|
266
|
+
export function serializeArchitectureDocsProjectionManifest(manifest: ArchitectureDocsProjectionManifestV3): string {
|
|
267
|
+
const capabilities = [...manifest.semanticBaseline.capabilities]
|
|
268
|
+
.sort((left, right) => compareStrings(left.capabilityId, right.capabilityId))
|
|
269
|
+
.map(canonicalCapability);
|
|
270
|
+
const capabilityBlock = JSON.stringify(capabilities, null, 2).split("\n").map((line, index) => index === 0 ? line : ` ${line}`).join("\n");
|
|
271
|
+
const targetIds = Object.keys(manifest.targets).sort(compareStrings);
|
|
272
|
+
const targetLines = targetIds.length === 0
|
|
273
|
+
? [" \"targets\": {}"]
|
|
274
|
+
: [
|
|
275
|
+
" \"targets\": {",
|
|
276
|
+
...targetIds.flatMap((targetId, index) => [
|
|
277
|
+
` ${JSON.stringify(targetId)}:`,
|
|
278
|
+
` ${JSON.stringify(canonicalTargetRow(manifest.targets[targetId]!))}${index === targetIds.length - 1 ? "" : ","}`
|
|
279
|
+
]),
|
|
280
|
+
" }"
|
|
281
|
+
];
|
|
282
|
+
return [
|
|
283
|
+
"{",
|
|
284
|
+
` "schemaVersion": ${JSON.stringify(manifest.schemaVersion)},`,
|
|
285
|
+
` "rendererVersion": ${JSON.stringify(manifest.rendererVersion)},`,
|
|
286
|
+
...(manifest.minimumRendererVersion === undefined ? [] : [` "minimumRendererVersion": ${JSON.stringify(manifest.minimumRendererVersion)},`]),
|
|
287
|
+
...(manifest.minimumProtocolVersion === undefined ? [] : [` "minimumProtocolVersion": ${JSON.stringify(manifest.minimumProtocolVersion)},`]),
|
|
288
|
+
` "profile": ${JSON.stringify(manifest.profile)},`,
|
|
289
|
+
` "layoutVersion": ${JSON.stringify(manifest.layoutVersion)},`,
|
|
290
|
+
` "generatedFrom": ${JSON.stringify({ codeGraphPackage: manifest.generatedFrom.codeGraphPackage, codeGraphVersion: manifest.generatedFrom.codeGraphVersion })},`,
|
|
291
|
+
" \"semanticBaseline\": {",
|
|
292
|
+
` "modelDigest": ${JSON.stringify(manifest.semanticBaseline.modelDigest)},`,
|
|
293
|
+
` "capabilities": ${capabilityBlock}`,
|
|
294
|
+
" },",
|
|
295
|
+
...targetLines,
|
|
296
|
+
"}",
|
|
297
|
+
""
|
|
298
|
+
].join("\n");
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/** The runtime semantic state the baseline records: capability entries without their evidence, plus the two aggregates. */
|
|
302
|
+
export function architectureDocsProjectionManifestSemanticState(manifest: ArchitectureDocsProjectionManifestV3): ArchitectureSemanticStateV1 {
|
|
303
|
+
return architectureSemanticStateFromCapabilities(manifest.semanticBaseline.capabilities.map(({ selectorEvidenceDigest: _selectorEvidenceDigest, ...capability }) => capability));
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Assembles a semantic state from its capability entries. The two aggregates are a function of
|
|
308
|
+
* the entries, so the committed manifest records only the entries and every reader recomputes them.
|
|
309
|
+
*/
|
|
310
|
+
export function architectureSemanticStateFromCapabilities(capabilities: readonly ArchitectureCapabilitySemanticStateV1[]): ArchitectureSemanticStateV1 {
|
|
311
|
+
const sorted = [...capabilities].sort((left, right) => compareStrings(left.capabilityId, right.capabilityId));
|
|
312
|
+
return {
|
|
313
|
+
schemaVersion: ARCHITECTURE_SEMANTIC_STATE_SCHEMA_VERSION,
|
|
314
|
+
capabilities: sorted,
|
|
315
|
+
semanticFingerprint: digestJson(sorted.map(({ capabilityId, semanticFingerprint }) => ({ capabilityId, semanticFingerprint })) as unknown as Json),
|
|
316
|
+
flowProofFingerprint: digestJson(sorted.map(({ capabilityId, flowProofFingerprint }) => ({ capabilityId, flowProofFingerprint })) as unknown as Json)
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/** Every stamp the rows record, by node id. A valid manifest stamps each node on one row only. */
|
|
321
|
+
export function architectureDocsProjectionManifestSourceStamps(manifest: ArchitectureDocsProjectionManifestV3): Record<string, string> {
|
|
322
|
+
const stamps: Record<string, string> = {};
|
|
323
|
+
for (const row of Object.values(manifest.targets)) Object.assign(stamps, row.sourceStamps ?? {});
|
|
324
|
+
return stamps;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/** The proof evidence the baseline was compiled from, as `ArchitectureProjectionProofEvidenceV1`. */
|
|
328
|
+
export function architectureDocsProjectionManifestProofEvidence(manifest: ArchitectureDocsProjectionManifestV3): ArchitectureProjectionProofEvidenceV1 {
|
|
329
|
+
return {
|
|
330
|
+
rendererVersion: manifest.rendererVersion,
|
|
331
|
+
selectorEvidenceDigests: Object.fromEntries(manifest.semanticBaseline.capabilities.map((capability) => [capability.capabilityId, capability.selectorEvidenceDigest])),
|
|
332
|
+
sourceStamps: architectureDocsProjectionManifestSourceStamps(manifest)
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* The declared source tree digest: the digest of every footprint stamp, by node id. Runtime
|
|
338
|
+
* provenance and the read-time value of a committed manifest use this one formula, so they agree
|
|
339
|
+
* exactly when every stamp agrees.
|
|
340
|
+
*/
|
|
341
|
+
export function architectureSourceStampsDigest(stamps: Readonly<Record<string, string>>): Sha256Digest {
|
|
342
|
+
return digestJson({
|
|
343
|
+
schemaVersion: "archcontext.architecture-source-stamps/v1",
|
|
344
|
+
stamps: Object.keys(stamps).sort(compareStrings).map((nodeId) => ({ nodeId, digest: stamps[nodeId]! }))
|
|
345
|
+
} as unknown as Json) as Sha256Digest;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/** The projection digest: renderer, profile and every target's output digest. */
|
|
349
|
+
export function architectureProjectionOutputsDigest(input: {
|
|
350
|
+
rendererVersion: string;
|
|
351
|
+
profile: string;
|
|
352
|
+
outputDigests: Readonly<Record<string, string>>;
|
|
353
|
+
}): Sha256Digest {
|
|
354
|
+
return digestJson({
|
|
355
|
+
schemaVersion: "archcontext.architecture-projection-outputs/v1",
|
|
356
|
+
rendererVersion: input.rendererVersion,
|
|
357
|
+
profile: input.profile,
|
|
358
|
+
outputs: Object.keys(input.outputDigests).sort(compareStrings).map((targetId) => ({ targetId, outputDigest: input.outputDigests[targetId]! }))
|
|
359
|
+
} as unknown as Json) as Sha256Digest;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/** The global digests of a committed manifest, computed from its rows at read time. */
|
|
363
|
+
export function architectureDocsProjectionManifestDigests(manifest: ArchitectureDocsProjectionManifestV3): ArchitectureDigestSetV1 {
|
|
364
|
+
return {
|
|
365
|
+
modelDigest: manifest.semanticBaseline.modelDigest as Sha256Digest,
|
|
366
|
+
sourceTreeDigest: architectureSourceStampsDigest(architectureDocsProjectionManifestSourceStamps(manifest)),
|
|
367
|
+
flowProofDigest: architectureDocsProjectionManifestSemanticState(manifest).flowProofFingerprint as Sha256Digest,
|
|
368
|
+
projectionDigest: architectureProjectionOutputsDigest({
|
|
369
|
+
rendererVersion: manifest.rendererVersion,
|
|
370
|
+
profile: manifest.profile,
|
|
371
|
+
outputDigests: Object.fromEntries(Object.entries(manifest.targets).map(([targetId, row]) => [targetId, row.outputDigest]))
|
|
372
|
+
})
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
|
|
183
376
|
export function architectureDocsProjectionProvenanceIssues(value: unknown, prefix = "provenance"): string[] {
|
|
184
377
|
const provenance = record(value);
|
|
185
378
|
if (!provenance) return [`${prefix} must be an object`];
|
|
@@ -189,72 +382,30 @@ export function architectureDocsProjectionProvenanceIssues(value: unknown, prefi
|
|
|
189
382
|
if (!isDigest(provenance[field])) issues.push(`${prefix}.${field} must be a SHA-256 digest`);
|
|
190
383
|
}
|
|
191
384
|
if (provenance.rendererVersion !== ARCHITECTURE_DOCS_RENDERER_VERSION) issues.push(`${prefix}.rendererVersion must be ${ARCHITECTURE_DOCS_RENDERER_VERSION}`);
|
|
192
|
-
if (provenance.layoutVersion !==
|
|
193
|
-
|
|
194
|
-
if (!generatedFrom) {
|
|
195
|
-
issues.push(`${prefix}.generatedFrom must be an object`);
|
|
196
|
-
return issues;
|
|
197
|
-
}
|
|
198
|
-
issues.push(...exactKeys(generatedFrom, ["codeGraphPackage", "codeGraphVersion"], [], `${prefix}.generatedFrom`));
|
|
199
|
-
for (const field of ["codeGraphPackage", "codeGraphVersion"] as const) {
|
|
200
|
-
if (!isNonEmptyString(generatedFrom[field])) issues.push(`${prefix}.generatedFrom.${field} must be a non-empty string`);
|
|
201
|
-
}
|
|
202
|
-
return issues;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
export function architectureProjectionSemanticBaselineIssues(value: unknown, prefix = "semanticBaseline"): string[] {
|
|
206
|
-
const baseline = record(value);
|
|
207
|
-
if (!baseline) return [`${prefix} must be an object`];
|
|
208
|
-
const issues = exactKeys(baseline, ["semanticState", "digests"], ["evidence"], prefix);
|
|
209
|
-
issues.push(...architectureSemanticStateIssues(baseline.semanticState, `${prefix}.semanticState`));
|
|
210
|
-
issues.push(...architectureDigestSetIssues(baseline.digests, `${prefix}.digests`));
|
|
211
|
-
if (baseline.evidence !== undefined) {
|
|
212
|
-
const evidence = record(baseline.evidence);
|
|
213
|
-
if (!evidence) issues.push(`${prefix}.evidence must be an object`);
|
|
214
|
-
else {
|
|
215
|
-
issues.push(...exactKeys(evidence, ["sourceTreeDigest", "selectorEvidenceDigest", "rendererVersion"], [], `${prefix}.evidence`));
|
|
216
|
-
if (!isDigest(evidence.sourceTreeDigest)) issues.push(`${prefix}.evidence.sourceTreeDigest must be a SHA-256 digest`);
|
|
217
|
-
if (!isDigest(evidence.selectorEvidenceDigest)) issues.push(`${prefix}.evidence.selectorEvidenceDigest must be a SHA-256 digest`);
|
|
218
|
-
if (!isNonEmptyString(evidence.rendererVersion)) issues.push(`${prefix}.evidence.rendererVersion must be a non-empty string`);
|
|
219
|
-
}
|
|
220
|
-
}
|
|
385
|
+
if (provenance.layoutVersion !== ARCHITECTURE_DOCS_LAYOUT_VERSION_V1) issues.push(`${prefix}.layoutVersion is unsupported`);
|
|
386
|
+
issues.push(...generatedFromIssues(provenance.generatedFrom, `${prefix}.generatedFrom`));
|
|
221
387
|
return issues;
|
|
222
388
|
}
|
|
223
389
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
const
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
export function architectureSemanticStateIssues(value: unknown, prefix = "semanticState"): string[] {
|
|
234
|
-
const state = record(value);
|
|
235
|
-
if (!state) return [`${prefix} must be an object`];
|
|
236
|
-
const issues = exactKeys(state, ["schemaVersion", "capabilities", "semanticFingerprint", "flowProofFingerprint"], [], prefix);
|
|
237
|
-
if (state.schemaVersion !== ARCHITECTURE_SEMANTIC_STATE_SCHEMA_VERSION) issues.push(`${prefix}.schemaVersion is unsupported`);
|
|
238
|
-
if (!isDigest(state.semanticFingerprint)) issues.push(`${prefix}.semanticFingerprint must be a SHA-256 digest`);
|
|
239
|
-
if (!isDigest(state.flowProofFingerprint)) issues.push(`${prefix}.flowProofFingerprint must be a SHA-256 digest`);
|
|
240
|
-
if (!Array.isArray(state.capabilities)) {
|
|
241
|
-
issues.push(`${prefix}.capabilities must be an array`);
|
|
242
|
-
return issues;
|
|
243
|
-
}
|
|
244
|
-
const ids = state.capabilities.map((entry) => record(entry)?.capabilityId);
|
|
245
|
-
if (!isSortedUniqueStrings(ids)) issues.push(`${prefix}.capabilities must be sorted and unique by capabilityId`);
|
|
246
|
-
state.capabilities.forEach((entry, index) => {
|
|
247
|
-
const entryPrefix = `${prefix}.capabilities[${index}]`;
|
|
390
|
+
function capabilityEntriesIssues(value: unknown, prefix: string, withSelectorEvidence: boolean): string[] {
|
|
391
|
+
if (!Array.isArray(value)) return [`${prefix} must be an array`];
|
|
392
|
+
const issues: string[] = [];
|
|
393
|
+
const ids = value.map((entry) => record(entry)?.capabilityId);
|
|
394
|
+
if (!isSortedUniqueStrings(ids)) issues.push(`${prefix} must be sorted and unique by capabilityId`);
|
|
395
|
+
value.forEach((entry, index) => {
|
|
396
|
+
const entryPrefix = `${prefix}[${index}]`;
|
|
248
397
|
const capability = record(entry);
|
|
249
398
|
if (!capability) {
|
|
250
399
|
issues.push(`${entryPrefix} must be an object`);
|
|
251
400
|
return;
|
|
252
401
|
}
|
|
253
|
-
|
|
402
|
+
const required = ["capabilityId", "memberNodeIds", "semanticFingerprint", "flowProofFingerprint", "proofStatus", "facets", ...(withSelectorEvidence ? ["selectorEvidenceDigest"] : [])];
|
|
403
|
+
issues.push(...exactKeys(capability, required, [], entryPrefix));
|
|
254
404
|
if (!isNonEmptyString(capability.capabilityId)) issues.push(`${entryPrefix}.capabilityId must be a non-empty string`);
|
|
255
405
|
if (!Array.isArray(capability.memberNodeIds) || !isSortedUniqueStrings(capability.memberNodeIds)) issues.push(`${entryPrefix}.memberNodeIds must be sorted and unique`);
|
|
256
406
|
if (!isDigest(capability.semanticFingerprint)) issues.push(`${entryPrefix}.semanticFingerprint must be a SHA-256 digest`);
|
|
257
407
|
if (!isDigest(capability.flowProofFingerprint)) issues.push(`${entryPrefix}.flowProofFingerprint must be a SHA-256 digest`);
|
|
408
|
+
if (withSelectorEvidence && !isDigest(capability.selectorEvidenceDigest)) issues.push(`${entryPrefix}.selectorEvidenceDigest must be a SHA-256 digest`);
|
|
258
409
|
if (!isArchitectureCapabilityProofStatus(capability.proofStatus)) issues.push(`${entryPrefix}.proofStatus must be a P1/P2 proof status`);
|
|
259
410
|
const facets = record(capability.facets);
|
|
260
411
|
if (!facets) issues.push(`${entryPrefix}.facets must be an object`);
|
|
@@ -268,6 +419,54 @@ export function architectureSemanticStateIssues(value: unknown, prefix = "semant
|
|
|
268
419
|
return issues;
|
|
269
420
|
}
|
|
270
421
|
|
|
422
|
+
function generatedFromIssues(value: unknown, prefix: string): string[] {
|
|
423
|
+
const generatedFrom = record(value);
|
|
424
|
+
if (!generatedFrom) return [`${prefix} must be an object`];
|
|
425
|
+
const issues = exactKeys(generatedFrom, ["codeGraphPackage", "codeGraphVersion"], [], prefix);
|
|
426
|
+
for (const field of ["codeGraphPackage", "codeGraphVersion"] as const) {
|
|
427
|
+
if (!isNonEmptyString(generatedFrom[field])) issues.push(`${prefix}.${field} must be a non-empty string`);
|
|
428
|
+
}
|
|
429
|
+
return issues;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function canonicalCapability(capability: ArchitectureDocsProjectionManifestCapabilityV3): ArchitectureDocsProjectionManifestCapabilityV3 {
|
|
433
|
+
return {
|
|
434
|
+
capabilityId: capability.capabilityId,
|
|
435
|
+
memberNodeIds: [...capability.memberNodeIds],
|
|
436
|
+
semanticFingerprint: capability.semanticFingerprint,
|
|
437
|
+
flowProofFingerprint: capability.flowProofFingerprint,
|
|
438
|
+
proofStatus: { p1: capability.proofStatus.p1, p2: capability.proofStatus.p2 },
|
|
439
|
+
facets: Object.fromEntries(Object.keys(capability.facets).sort(compareStrings).map((facet) => [facet, capability.facets[facet as ArchitectureSemanticFacet]])) as Record<ArchitectureSemanticFacet, string>,
|
|
440
|
+
selectorEvidenceDigest: capability.selectorEvidenceDigest
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function canonicalTargetRow(row: ArchitectureDocsProjectionManifestTargetV3): ArchitectureDocsProjectionManifestTargetV3 {
|
|
445
|
+
return {
|
|
446
|
+
type: row.type,
|
|
447
|
+
scope: {
|
|
448
|
+
kind: row.scope.kind,
|
|
449
|
+
...(row.scope.id !== undefined ? { id: row.scope.id } : {}),
|
|
450
|
+
...(row.scope.entityKind !== undefined ? { entityKind: row.scope.entityKind } : {})
|
|
451
|
+
},
|
|
452
|
+
path: row.path,
|
|
453
|
+
ownership: row.ownership,
|
|
454
|
+
format: row.format,
|
|
455
|
+
outputDigest: row.outputDigest,
|
|
456
|
+
...(row.sourceStamps !== undefined
|
|
457
|
+
? { sourceStamps: Object.fromEntries(Object.keys(row.sourceStamps).sort(compareStrings).map((nodeId) => [nodeId, row.sourceStamps![nodeId]!])) }
|
|
458
|
+
: {}),
|
|
459
|
+
...(row.scale !== undefined
|
|
460
|
+
? {
|
|
461
|
+
scale: {
|
|
462
|
+
fileCountBucket: { lower: row.scale.fileCountBucket.lower, upper: row.scale.fileCountBucket.upper },
|
|
463
|
+
lineCountBucket: { lower: row.scale.lineCountBucket.lower, upper: row.scale.lineCountBucket.upper }
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
: {})
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
|
|
271
470
|
/** True when the value is a valid 1–2–5 magnitude bucket (see ArchitectureScaleBucketV1). */
|
|
272
471
|
export function isArchitectureScaleBucket(value: unknown): value is ArchitectureScaleBucketV1 {
|
|
273
472
|
const bucket = record(value);
|
|
@@ -287,8 +486,7 @@ export function isArchitectureScaleBucket(value: unknown): value is Architecture
|
|
|
287
486
|
function manifestTargetIssues(value: unknown, prefix: string): string[] {
|
|
288
487
|
const target = record(value);
|
|
289
488
|
if (!target) return [`${prefix} must be an object`];
|
|
290
|
-
const issues = exactKeys(target, ["
|
|
291
|
-
if (!isNonEmptyString(target.targetId)) issues.push(`${prefix}.targetId must be a non-empty string`);
|
|
489
|
+
const issues = exactKeys(target, ["type", "scope", "path", "ownership", "format", "outputDigest"], ["sourceStamps", "scale"], prefix);
|
|
292
490
|
if (typeof target.type !== "string" || !Object.hasOwn(TARGET_TYPES, target.type)) issues.push(`${prefix}.type is unsupported`);
|
|
293
491
|
const scope = record(target.scope);
|
|
294
492
|
if (!scope || typeof scope.kind !== "string" || !Object.hasOwn(SCOPE_KINDS, scope.kind)) issues.push(`${prefix}.scope.kind is unsupported`);
|
|
@@ -300,15 +498,24 @@ function manifestTargetIssues(value: unknown, prefix: string): string[] {
|
|
|
300
498
|
}
|
|
301
499
|
if (typeof target.path !== "string" || !isRepoRelativePosixPath(target.path)) issues.push(`${prefix}.path must be a repository-relative POSIX path`);
|
|
302
500
|
if (typeof target.ownership !== "string" || !Object.hasOwn(OWNERSHIPS, target.ownership)) issues.push(`${prefix}.ownership is unsupported`);
|
|
303
|
-
if (target.rendererVersion !== ARCHITECTURE_DOCS_RENDERER_VERSION) issues.push(`${prefix}.rendererVersion must be ${ARCHITECTURE_DOCS_RENDERER_VERSION}`);
|
|
304
501
|
if (typeof target.format !== "string" || !Object.hasOwn(FORMATS, target.format)) issues.push(`${prefix}.format is unsupported`);
|
|
305
|
-
if (!isDigest(target.sourceDigest)) issues.push(`${prefix}.sourceDigest must be a SHA-256 digest`);
|
|
306
502
|
if (!isDigest(target.outputDigest)) issues.push(`${prefix}.outputDigest must be a SHA-256 digest`);
|
|
307
|
-
if (target.
|
|
308
|
-
|
|
309
|
-
issues.push(`${prefix}.sourceFootprintDigest and scale are only allowed on entity-summary targets`);
|
|
503
|
+
if ((target.sourceStamps !== undefined || target.scale !== undefined) && target.type !== "entity-summary") {
|
|
504
|
+
issues.push(`${prefix}.sourceStamps and scale are only allowed on entity-summary targets`);
|
|
310
505
|
}
|
|
311
|
-
if (
|
|
506
|
+
if (target.sourceStamps !== undefined) {
|
|
507
|
+
const stamps = record(target.sourceStamps);
|
|
508
|
+
if (!stamps || Object.keys(stamps).length === 0) issues.push(`${prefix}.sourceStamps must be a non-empty object keyed by node id`);
|
|
509
|
+
else {
|
|
510
|
+
for (const [nodeId, digest] of Object.entries(stamps)) {
|
|
511
|
+
if (!isNonEmptyString(nodeId)) issues.push(`${prefix}.sourceStamps keys must be non-empty node ids`);
|
|
512
|
+
if (!isDigest(digest)) issues.push(`${prefix}.sourceStamps.${nodeId} must be a SHA-256 digest`);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
const ownNodeId = scope?.id;
|
|
517
|
+
const stampsOwnNode = typeof ownNodeId === "string" && record(target.sourceStamps) !== undefined && Object.hasOwn(record(target.sourceStamps)!, ownNodeId);
|
|
518
|
+
if (stampsOwnNode !== (target.scale !== undefined)) issues.push(`${prefix}.scale must be present exactly when sourceStamps stamps the target's own node`);
|
|
312
519
|
if (target.scale !== undefined) {
|
|
313
520
|
const scale = record(target.scale);
|
|
314
521
|
if (!scale) issues.push(`${prefix}.scale must be an object`);
|
|
@@ -344,3 +551,38 @@ function isNonEmptyString(value: unknown): value is string {
|
|
|
344
551
|
function isSortedUniqueStrings(values: readonly unknown[]): boolean {
|
|
345
552
|
return values.every((value, index) => typeof value === "string" && (index === 0 || (values[index - 1] as string) < value));
|
|
346
553
|
}
|
|
554
|
+
|
|
555
|
+
function compareStrings(left: string, right: string): number {
|
|
556
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
function versionNumber(value: unknown, family: string): number | undefined {
|
|
560
|
+
if (typeof value !== "string" || !value.startsWith(family)) return undefined;
|
|
561
|
+
const suffix = value.slice(family.length);
|
|
562
|
+
if (!/^[1-9][0-9]*$/.test(suffix)) return undefined;
|
|
563
|
+
const version = Number(suffix);
|
|
564
|
+
return Number.isSafeInteger(version) ? version : undefined;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
/** Pure engine compatibility; absent floors retain the v3 behavior. Future formats never rebaseline. */
|
|
568
|
+
export function architectureDocsProjectionCompatibilityIssues(value: unknown, engine: { rendererVersion: string; protocolVersion: string } = {
|
|
569
|
+
rendererVersion: ARCHITECTURE_DOCS_RENDERER_VERSION, protocolVersion: PROJECTION_REQUEST_SCHEMA_VERSION
|
|
570
|
+
}): string[] {
|
|
571
|
+
const manifest = record(value);
|
|
572
|
+
if (!manifest) return [];
|
|
573
|
+
const issues: string[] = [];
|
|
574
|
+
if (["minimumRendererVersion", "minimumProtocolVersion"].some(key => Object.hasOwn(manifest, key))
|
|
575
|
+
&& manifest.schemaVersion !== ARCHITECTURE_DOCS_PROJECTION_MANIFEST_SCHEMA_VERSION) issues.push("projection manifest floors require the current schema");
|
|
576
|
+
if (typeof manifest.schemaVersion === "string" && manifest.schemaVersion !== ARCHITECTURE_DOCS_PROJECTION_MANIFEST_SCHEMA_VERSION
|
|
577
|
+
&& !isSupersededArchitectureDocsProjectionManifestSchemaVersion(manifest.schemaVersion)) issues.push("projection manifest schema is unsupported by this engine");
|
|
578
|
+
for (const [key, family, current] of [
|
|
579
|
+
["minimumRendererVersion", "archcontext.docs-renderer/v", engine.rendererVersion],
|
|
580
|
+
["minimumProtocolVersion", "archcontext.projection-request/v", engine.protocolVersion]
|
|
581
|
+
] as const) {
|
|
582
|
+
if (!Object.hasOwn(manifest, key)) continue;
|
|
583
|
+
const required = versionNumber(manifest[key], family);
|
|
584
|
+
const supported = versionNumber(current, family);
|
|
585
|
+
if (required === undefined || supported === undefined || supported < required) issues.push(`projection manifest ${key} is unsupported by this engine`);
|
|
586
|
+
}
|
|
587
|
+
return issues;
|
|
588
|
+
}
|