@xemahq/kernel-contracts 0.29.0 → 0.30.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/dist/document-patch/index.d.ts +4 -0
- package/dist/document-patch/index.d.ts.map +1 -0
- package/dist/document-patch/index.js +20 -0
- package/dist/document-patch/index.js.map +1 -0
- package/dist/document-patch/lib/patch-anchor.d.ts +7 -0
- package/dist/document-patch/lib/patch-anchor.d.ts.map +1 -0
- package/dist/document-patch/lib/patch-anchor.js +14 -0
- package/dist/document-patch/lib/patch-anchor.js.map +1 -0
- package/dist/document-patch/lib/patch-enums.d.ts +11 -0
- package/dist/document-patch/lib/patch-enums.d.ts.map +1 -0
- package/dist/document-patch/lib/patch-enums.js +16 -0
- package/dist/document-patch/lib/patch-enums.js.map +1 -0
- package/dist/document-patch/lib/pending-patch.d.ts +22 -0
- package/dist/document-patch/lib/pending-patch.d.ts.map +1 -0
- package/dist/document-patch/lib/pending-patch.js +3 -0
- package/dist/document-patch/lib/pending-patch.js.map +1 -0
- package/dist/output/index.d.ts +7 -0
- package/dist/output/index.d.ts.map +1 -0
- package/dist/output/index.js +23 -0
- package/dist/output/index.js.map +1 -0
- package/dist/output/lib/output-deliverable-expectation.d.ts +9 -0
- package/dist/output/lib/output-deliverable-expectation.d.ts.map +1 -0
- package/dist/output/lib/output-deliverable-expectation.js +3 -0
- package/dist/output/lib/output-deliverable-expectation.js.map +1 -0
- package/dist/output/lib/output-item-dto.d.ts +25 -0
- package/dist/output/lib/output-item-dto.d.ts.map +1 -0
- package/dist/output/lib/output-item-dto.js +3 -0
- package/dist/output/lib/output-item-dto.js.map +1 -0
- package/dist/output/lib/output-kind.d.ts +14 -0
- package/dist/output/lib/output-kind.d.ts.map +1 -0
- package/dist/output/lib/output-kind.js +18 -0
- package/dist/output/lib/output-kind.js.map +1 -0
- package/dist/output/lib/output-render-hint.d.ts +11 -0
- package/dist/output/lib/output-render-hint.d.ts.map +1 -0
- package/dist/output/lib/output-render-hint.js +10 -0
- package/dist/output/lib/output-render-hint.js.map +1 -0
- package/dist/output/lib/output-version-dto.d.ts +10 -0
- package/dist/output/lib/output-version-dto.d.ts.map +1 -0
- package/dist/output/lib/output-version-dto.js +3 -0
- package/dist/output/lib/output-version-dto.js.map +1 -0
- package/dist/output/lib/payload-schemas/code-diff.d.ts +7 -0
- package/dist/output/lib/payload-schemas/code-diff.d.ts.map +1 -0
- package/dist/output/lib/payload-schemas/code-diff.js +11 -0
- package/dist/output/lib/payload-schemas/code-diff.js.map +1 -0
- package/dist/output/lib/payload-schemas/decision-record.d.ts +19 -0
- package/dist/output/lib/payload-schemas/decision-record.d.ts.map +1 -0
- package/dist/output/lib/payload-schemas/decision-record.js +39 -0
- package/dist/output/lib/payload-schemas/decision-record.js.map +1 -0
- package/dist/output/lib/payload-schemas/diagram.d.ts +12 -0
- package/dist/output/lib/payload-schemas/diagram.d.ts.map +1 -0
- package/dist/output/lib/payload-schemas/diagram.js +34 -0
- package/dist/output/lib/payload-schemas/diagram.js.map +1 -0
- package/dist/output/lib/payload-schemas/external-blob.d.ts +9 -0
- package/dist/output/lib/payload-schemas/external-blob.d.ts.map +1 -0
- package/dist/output/lib/payload-schemas/external-blob.js +15 -0
- package/dist/output/lib/payload-schemas/external-blob.js.map +1 -0
- package/dist/output/lib/payload-schemas/index.d.ts +17 -0
- package/dist/output/lib/payload-schemas/index.d.ts.map +1 -0
- package/dist/output/lib/payload-schemas/index.js +94 -0
- package/dist/output/lib/payload-schemas/index.js.map +1 -0
- package/dist/output/lib/payload-schemas/json-payload.d.ts +4 -0
- package/dist/output/lib/payload-schemas/json-payload.d.ts.map +1 -0
- package/dist/output/lib/payload-schemas/json-payload.js +9 -0
- package/dist/output/lib/payload-schemas/json-payload.js.map +1 -0
- package/dist/output/lib/payload-schemas/manifest.d.ts +37 -0
- package/dist/output/lib/payload-schemas/manifest.d.ts.map +1 -0
- package/dist/output/lib/payload-schemas/manifest.js +43 -0
- package/dist/output/lib/payload-schemas/manifest.js.map +1 -0
- package/dist/output/lib/payload-schemas/markdown-doc.d.ts +8 -0
- package/dist/output/lib/payload-schemas/markdown-doc.d.ts.map +1 -0
- package/dist/output/lib/payload-schemas/markdown-doc.js +12 -0
- package/dist/output/lib/payload-schemas/markdown-doc.js.map +1 -0
- package/dist/output/lib/payload-schemas/review-findings.d.ts +30 -0
- package/dist/output/lib/payload-schemas/review-findings.d.ts.map +1 -0
- package/dist/output/lib/payload-schemas/review-findings.js +39 -0
- package/dist/output/lib/payload-schemas/review-findings.js.map +1 -0
- package/dist/output/lib/payload-schemas/schema.d.ts +4 -0
- package/dist/output/lib/payload-schemas/schema.d.ts.map +1 -0
- package/dist/output/lib/payload-schemas/schema.js +10 -0
- package/dist/output/lib/payload-schemas/schema.js.map +1 -0
- package/dist/output/lib/payload-schemas/test-report.d.ts +33 -0
- package/dist/output/lib/payload-schemas/test-report.d.ts.map +1 -0
- package/dist/output/lib/payload-schemas/test-report.js +46 -0
- package/dist/output/lib/payload-schemas/test-report.js.map +1 -0
- package/dist/workspace-image/index.d.ts +5 -0
- package/dist/workspace-image/index.d.ts.map +1 -0
- package/dist/workspace-image/index.js +21 -0
- package/dist/workspace-image/index.js.map +1 -0
- package/dist/workspace-image/lib/enums.d.ts +17 -0
- package/dist/workspace-image/lib/enums.d.ts.map +1 -0
- package/dist/workspace-image/lib/enums.js +22 -0
- package/dist/workspace-image/lib/enums.js.map +1 -0
- package/dist/workspace-image/lib/hash.d.ts +3 -0
- package/dist/workspace-image/lib/hash.d.ts.map +1 -0
- package/dist/workspace-image/lib/hash.js +50 -0
- package/dist/workspace-image/lib/hash.js.map +1 -0
- package/dist/workspace-image/lib/schemas.d.ts +103 -0
- package/dist/workspace-image/lib/schemas.d.ts.map +1 -0
- package/dist/workspace-image/lib/schemas.js +53 -0
- package/dist/workspace-image/lib/schemas.js.map +1 -0
- package/dist/workspace-image/lib/types.d.ts +50 -0
- package/dist/workspace-image/lib/types.d.ts.map +1 -0
- package/dist/workspace-image/lib/types.js +3 -0
- package/dist/workspace-image/lib/types.js.map +1 -0
- package/package.json +1 -1
- package/src/document-patch/index.ts +25 -0
- package/src/document-patch/lib/patch-anchor.ts +22 -0
- package/src/document-patch/lib/patch-enums.ts +36 -0
- package/src/document-patch/lib/pending-patch.ts +31 -0
- package/src/output/index.ts +20 -0
- package/src/output/lib/output-deliverable-expectation.ts +25 -0
- package/src/output/lib/output-item-dto.ts +54 -0
- package/src/output/lib/output-kind.ts +44 -0
- package/src/output/lib/output-render-hint.ts +28 -0
- package/src/output/lib/output-version-dto.ts +21 -0
- package/src/output/lib/payload-schemas/code-diff.ts +20 -0
- package/src/output/lib/payload-schemas/decision-record.ts +71 -0
- package/src/output/lib/payload-schemas/diagram.ts +54 -0
- package/src/output/lib/payload-schemas/external-blob.ts +36 -0
- package/src/output/lib/payload-schemas/index.ts +133 -0
- package/src/output/lib/payload-schemas/json-payload.ts +19 -0
- package/src/output/lib/payload-schemas/manifest.ts +81 -0
- package/src/output/lib/payload-schemas/markdown-doc.ts +33 -0
- package/src/output/lib/payload-schemas/review-findings.ts +75 -0
- package/src/output/lib/payload-schemas/schema.ts +22 -0
- package/src/output/lib/payload-schemas/test-report.ts +69 -0
- package/src/workspace-image/index.ts +13 -0
- package/src/workspace-image/lib/enums.ts +38 -0
- package/src/workspace-image/lib/hash.ts +95 -0
- package/src/workspace-image/lib/schemas.ts +63 -0
- package/src/workspace-image/lib/types.ts +79 -0
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Diagram-dialect enum. Closed today on `mermaid` only — the FE
|
|
5
|
+
* renderer pipeline can ingest mermaid via the existing
|
|
6
|
+
* `MarkdownView` mermaid block. Add new values here when a new
|
|
7
|
+
* dialect (drawio, plantuml, …) has a renderer wired.
|
|
8
|
+
*/
|
|
9
|
+
export enum DiagramDialect {
|
|
10
|
+
MERMAID = 'mermaid',
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const DiagramDialectSchema = z.nativeEnum(DiagramDialect);
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Payload shape for `OutputKind.DIAGRAM` artifacts — standalone
|
|
17
|
+
* diagrams emitted by architecture / requirements agents.
|
|
18
|
+
*
|
|
19
|
+
* Two valid shapes (mirroring how `xema/emit-artifact` producers
|
|
20
|
+
* actually emit today):
|
|
21
|
+
*
|
|
22
|
+
* 1. Inline source: `{ type: 'mermaid', source: '<mermaid src>' }`.
|
|
23
|
+
* 2. Markdown fallback: `{ body: '...mermaid block...' }` — when
|
|
24
|
+
* the producer wrapped a mermaid fence in a markdown narrative.
|
|
25
|
+
*
|
|
26
|
+
* The FE `DiagramRenderer` reads `source` when present, else falls
|
|
27
|
+
* through to markdown rendering which itself supports mermaid.
|
|
28
|
+
*/
|
|
29
|
+
export const DiagramPayloadSchema = z
|
|
30
|
+
.object({
|
|
31
|
+
type: DiagramDialectSchema.optional(),
|
|
32
|
+
source: z.string().optional(),
|
|
33
|
+
body: z.string().optional(),
|
|
34
|
+
})
|
|
35
|
+
.passthrough()
|
|
36
|
+
.superRefine((value, ctx) => {
|
|
37
|
+
const hasSource = typeof value.source === 'string' && value.source.trim().length > 0;
|
|
38
|
+
const hasBody = typeof value.body === 'string' && value.body.trim().length > 0;
|
|
39
|
+
if (!hasSource && !hasBody) {
|
|
40
|
+
ctx.addIssue({
|
|
41
|
+
code: 'custom',
|
|
42
|
+
message: 'diagram payload must include either `source` or `body`.',
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
if (hasSource && value.type === undefined) {
|
|
46
|
+
ctx.addIssue({
|
|
47
|
+
code: 'custom',
|
|
48
|
+
path: ['type'],
|
|
49
|
+
message: 'diagram payload with `source` must declare `type` (e.g. `mermaid`).',
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
export type DiagramPayload = z.infer<typeof DiagramPayloadSchema>;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Payload shape for `OutputKind.EXTERNAL_BLOB` artifacts — images,
|
|
5
|
+
* PDFs, archives, and other binary content stored out-of-band in
|
|
6
|
+
* MinIO / S3.
|
|
7
|
+
*
|
|
8
|
+
* The `payload` itself is metadata about the external bytes. The
|
|
9
|
+
* actual bytes live behind `ArtifactVersion.pointer` (durable
|
|
10
|
+
* storage URI) and are streamed via the artifact-store's
|
|
11
|
+
* version-content endpoint, NOT carried inline.
|
|
12
|
+
*
|
|
13
|
+
* `contentType` (IANA media type) is required so the FE renderer
|
|
14
|
+
* can branch on preview mode (image / pdf / video / download).
|
|
15
|
+
* `size` is informational. `originalFilename` flows into the
|
|
16
|
+
* download affordance when present.
|
|
17
|
+
*/
|
|
18
|
+
export const ExternalBlobPayloadSchema = z
|
|
19
|
+
.object({
|
|
20
|
+
contentType: z
|
|
21
|
+
.string()
|
|
22
|
+
.min(1, 'external_blob.contentType must be a non-empty IANA media type'),
|
|
23
|
+
/** Byte count, when known. Producers without it may omit. */
|
|
24
|
+
size: z.number().nonnegative().int().optional(),
|
|
25
|
+
/** Original filename, used as the download attachment name. */
|
|
26
|
+
originalFilename: z.string().optional(),
|
|
27
|
+
/**
|
|
28
|
+
* Producer-supplied alt text for image-kind blobs (a11y) and
|
|
29
|
+
* for any future LLM-briefing path that needs a non-visual
|
|
30
|
+
* description.
|
|
31
|
+
*/
|
|
32
|
+
altText: z.string().optional(),
|
|
33
|
+
})
|
|
34
|
+
.passthrough();
|
|
35
|
+
|
|
36
|
+
export type ExternalBlobPayload = z.infer<typeof ExternalBlobPayloadSchema>;
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import type { ZodTypeAny } from 'zod';
|
|
2
|
+
|
|
3
|
+
import { CodeDiffPayloadSchema } from './code-diff';
|
|
4
|
+
import { DecisionRecordPayloadSchema } from './decision-record';
|
|
5
|
+
import { DiagramPayloadSchema } from './diagram';
|
|
6
|
+
import { ExternalBlobPayloadSchema } from './external-blob';
|
|
7
|
+
import { JsonPayloadPayloadSchema } from './json-payload';
|
|
8
|
+
import { ManifestPayloadSchema } from './manifest';
|
|
9
|
+
import { MarkdownDocPayloadSchema } from './markdown-doc';
|
|
10
|
+
import { ReviewFindingsPayloadSchema } from './review-findings';
|
|
11
|
+
import { JsonSchemaPayloadSchema } from './schema';
|
|
12
|
+
import { TestReportPayloadSchema } from './test-report';
|
|
13
|
+
|
|
14
|
+
import { OutputKind } from '../output-kind';
|
|
15
|
+
|
|
16
|
+
export * from './code-diff';
|
|
17
|
+
export * from './decision-record';
|
|
18
|
+
export * from './diagram';
|
|
19
|
+
export * from './external-blob';
|
|
20
|
+
export * from './json-payload';
|
|
21
|
+
export * from './manifest';
|
|
22
|
+
export * from './markdown-doc';
|
|
23
|
+
export * from './review-findings';
|
|
24
|
+
export * from './schema';
|
|
25
|
+
export * from './test-report';
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Registry mapping closed `OutputKind` → Zod schema for the matching
|
|
29
|
+
* artifact payload. Every kind that has a renderer also has a schema;
|
|
30
|
+
* `OutputKind.UNKNOWN` is intentionally absent — biome-supplied
|
|
31
|
+
* payloads we can't classify are NOT validated (they pass through to
|
|
32
|
+
* the `unknown` renderer's "Show raw" surface).
|
|
33
|
+
*
|
|
34
|
+
* Keeping the registry keyed on `OutputKind` rather than free-form
|
|
35
|
+
* `type` strings means: the `(type → OutputKind)` mapping is the single
|
|
36
|
+
* place biomes add new aliases; once a producer's `type` resolves to
|
|
37
|
+
* a known kind, validation kicks in automatically.
|
|
38
|
+
*/
|
|
39
|
+
export const OUTPUT_KIND_PAYLOAD_SCHEMAS: Readonly<
|
|
40
|
+
Partial<Record<OutputKind, ZodTypeAny>>
|
|
41
|
+
> = {
|
|
42
|
+
[OutputKind.MARKDOWN_DOC]: MarkdownDocPayloadSchema,
|
|
43
|
+
[OutputKind.CODE_DIFF]: CodeDiffPayloadSchema,
|
|
44
|
+
[OutputKind.DECISION_RECORD]: DecisionRecordPayloadSchema,
|
|
45
|
+
[OutputKind.REVIEW_FINDINGS]: ReviewFindingsPayloadSchema,
|
|
46
|
+
[OutputKind.SCHEMA]: JsonSchemaPayloadSchema,
|
|
47
|
+
[OutputKind.MANIFEST]: ManifestPayloadSchema,
|
|
48
|
+
[OutputKind.TEST_REPORT]: TestReportPayloadSchema,
|
|
49
|
+
[OutputKind.DIAGRAM]: DiagramPayloadSchema,
|
|
50
|
+
[OutputKind.JSON_PAYLOAD]: JsonPayloadPayloadSchema,
|
|
51
|
+
[OutputKind.EXTERNAL_BLOB]: ExternalBlobPayloadSchema,
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Closed mapping from producer-supplied `type` string (the
|
|
56
|
+
* `EmitArtifactDto.type` value) → `OutputKind` enum. This is the
|
|
57
|
+
* platform's authoritative `type → kind` table; the FE and BE both
|
|
58
|
+
* read from here. Biomes that ship new aliases for an existing kind
|
|
59
|
+
* extend this map via a biome-host extension point (future) or
|
|
60
|
+
* register a new OutputKind (kernel-level).
|
|
61
|
+
*
|
|
62
|
+
* Order is irrelevant — duplicate keys would be a build-time error
|
|
63
|
+
* in TS, and the map is frozen at module load.
|
|
64
|
+
*/
|
|
65
|
+
export const ARTIFACT_TYPE_TO_OUTPUT_KIND: Readonly<Record<string, OutputKind>> = {
|
|
66
|
+
// markdown_doc
|
|
67
|
+
document_page: OutputKind.MARKDOWN_DOC,
|
|
68
|
+
'markdown.doc': OutputKind.MARKDOWN_DOC,
|
|
69
|
+
'doc.markdown': OutputKind.MARKDOWN_DOC,
|
|
70
|
+
'requirements.doc': OutputKind.MARKDOWN_DOC,
|
|
71
|
+
'design.doc': OutputKind.MARKDOWN_DOC,
|
|
72
|
+
'spec.doc': OutputKind.MARKDOWN_DOC,
|
|
73
|
+
// code_diff
|
|
74
|
+
'code.diff': OutputKind.CODE_DIFF,
|
|
75
|
+
'src.diff': OutputKind.CODE_DIFF,
|
|
76
|
+
'patch.diff': OutputKind.CODE_DIFF,
|
|
77
|
+
// decision_record
|
|
78
|
+
'decision.record': OutputKind.DECISION_RECORD,
|
|
79
|
+
adr: OutputKind.DECISION_RECORD,
|
|
80
|
+
// review_findings
|
|
81
|
+
'review.findings': OutputKind.REVIEW_FINDINGS,
|
|
82
|
+
'review.report': OutputKind.REVIEW_FINDINGS,
|
|
83
|
+
'review.verdict': OutputKind.REVIEW_FINDINGS,
|
|
84
|
+
// schema
|
|
85
|
+
schema: OutputKind.SCHEMA,
|
|
86
|
+
'json.schema': OutputKind.SCHEMA,
|
|
87
|
+
// manifest
|
|
88
|
+
manifest: OutputKind.MANIFEST,
|
|
89
|
+
'workflow.manifest': OutputKind.MANIFEST,
|
|
90
|
+
'biome.manifest': OutputKind.MANIFEST,
|
|
91
|
+
// test_report
|
|
92
|
+
'test.report': OutputKind.TEST_REPORT,
|
|
93
|
+
'qa.report': OutputKind.TEST_REPORT,
|
|
94
|
+
// diagram
|
|
95
|
+
diagram: OutputKind.DIAGRAM,
|
|
96
|
+
mermaid: OutputKind.DIAGRAM,
|
|
97
|
+
// json_payload
|
|
98
|
+
endpoint_fetch_response: OutputKind.JSON_PAYLOAD,
|
|
99
|
+
structured_json: OutputKind.JSON_PAYLOAD,
|
|
100
|
+
'json.payload': OutputKind.JSON_PAYLOAD,
|
|
101
|
+
// external_blob
|
|
102
|
+
'image.png': OutputKind.EXTERNAL_BLOB,
|
|
103
|
+
'image.jpeg': OutputKind.EXTERNAL_BLOB,
|
|
104
|
+
'binary.blob': OutputKind.EXTERNAL_BLOB,
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Resolve a producer-supplied `type` to the matching `OutputKind`.
|
|
109
|
+
* Returns `undefined` when no mapping exists — callers treat that as
|
|
110
|
+
* "biome-defined custom type" and skip schema validation, falling
|
|
111
|
+
* through to the `unknown` renderer downstream.
|
|
112
|
+
*/
|
|
113
|
+
export function outputKindForType(type: string): OutputKind | undefined {
|
|
114
|
+
return ARTIFACT_TYPE_TO_OUTPUT_KIND[type];
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Resolve the Zod payload schema for a given `type` string, going
|
|
119
|
+
* through the `type → OutputKind → schema` chain. Returns
|
|
120
|
+
* `undefined` when the `type` is unknown OR when the kind has no
|
|
121
|
+
* registered schema (today only `OutputKind.UNKNOWN`).
|
|
122
|
+
*
|
|
123
|
+
* Callers (artifact-store-api's `EmitArtifactValidator`,
|
|
124
|
+
* future FE preview helpers) treat `undefined` as "pass through —
|
|
125
|
+
* no schema-level validation for this type". Biome extensibility is
|
|
126
|
+
* preserved: a custom `my-org.special-output` type that's not in the
|
|
127
|
+
* table simply isn't validated, exactly as today.
|
|
128
|
+
*/
|
|
129
|
+
export function payloadSchemaForType(type: string): ZodTypeAny | undefined {
|
|
130
|
+
const kind = outputKindForType(type);
|
|
131
|
+
if (kind === undefined) {return undefined;}
|
|
132
|
+
return OUTPUT_KIND_PAYLOAD_SCHEMAS[kind];
|
|
133
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Payload shape for `OutputKind.JSON_PAYLOAD` artifacts — the
|
|
5
|
+
* deliberate "structured JSON without a more specific kind" bucket.
|
|
6
|
+
* Used today by `xema/http`, `xema/endpoint-fetch`, and any producer
|
|
7
|
+
* emitting an opaque JSON envelope.
|
|
8
|
+
*
|
|
9
|
+
* Permissive by design — the WHOLE payload IS the deliverable, no
|
|
10
|
+
* particular field is required. Validation just ensures it's an
|
|
11
|
+
* object (not a bare primitive); arrays masquerading as the root are
|
|
12
|
+
* also accepted via the union below.
|
|
13
|
+
*/
|
|
14
|
+
export const JsonPayloadPayloadSchema = z.union([
|
|
15
|
+
z.record(z.string(), z.unknown()),
|
|
16
|
+
z.array(z.unknown()),
|
|
17
|
+
]);
|
|
18
|
+
|
|
19
|
+
export type JsonPayloadPayload = z.infer<typeof JsonPayloadPayloadSchema>;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* One reference inside a manifest's `refs[]` — a pointer to a KB page,
|
|
5
|
+
* a backlog item, an emitted file, or any other tracked artifact the
|
|
6
|
+
* manifest summarises.
|
|
7
|
+
*/
|
|
8
|
+
export const ManifestRefSchema = z
|
|
9
|
+
.object({
|
|
10
|
+
/** Closed-ish set of ref kinds; producers may extend via .passthrough(). */
|
|
11
|
+
type: z.string().min(1),
|
|
12
|
+
id: z.string().min(1),
|
|
13
|
+
title: z.string().optional(),
|
|
14
|
+
/** Producer-defined verb describing what happened to the ref (created / modified / referenced / …). */
|
|
15
|
+
action: z.string().optional(),
|
|
16
|
+
/** File-system path for `file`-kind refs. */
|
|
17
|
+
path: z.string().optional(),
|
|
18
|
+
})
|
|
19
|
+
.passthrough();
|
|
20
|
+
|
|
21
|
+
export type ManifestRef = z.infer<typeof ManifestRefSchema>;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* An inline diagram embedded in the manifest. Today only `mermaid`
|
|
25
|
+
* is supported; the FE renders via the existing mermaid pipeline in
|
|
26
|
+
* `MarkdownView`.
|
|
27
|
+
*/
|
|
28
|
+
export const ManifestDiagramSchema = z
|
|
29
|
+
.object({
|
|
30
|
+
key: z.string().min(1),
|
|
31
|
+
title: z.string().min(1),
|
|
32
|
+
type: z.literal('mermaid'),
|
|
33
|
+
source: z.string().min(1),
|
|
34
|
+
})
|
|
35
|
+
.passthrough();
|
|
36
|
+
|
|
37
|
+
export type ManifestDiagram = z.infer<typeof ManifestDiagramSchema>;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Payload shape for `OutputKind.MANIFEST` artifacts — phase summaries,
|
|
41
|
+
* workflow manifests, biome manifests, and any structured "summary
|
|
42
|
+
* with cross-references" document.
|
|
43
|
+
*
|
|
44
|
+
* Two render shapes the FE recognises:
|
|
45
|
+
*
|
|
46
|
+
* 1. Rich manifest: `{ schemaVersion, summary, refs[]?, diagrams[]? }`.
|
|
47
|
+
* The FE renderer (`ManifestRenderer`, plus the legacy
|
|
48
|
+
* `ArtifactContentViewer`) renders the summary as markdown with
|
|
49
|
+
* phase + ref + diagram sidebars.
|
|
50
|
+
* 2. Plain YAML/JSON: `{ body: string }`. The renderer falls back
|
|
51
|
+
* to monospaced text preserving indentation.
|
|
52
|
+
*
|
|
53
|
+
* `passthrough()` so the `schemaVersion` field — which is a producer-
|
|
54
|
+
* declared manifest version, distinct from the artifact-store's
|
|
55
|
+
* `EmitArtifactDto.schemaVersion` — flows through.
|
|
56
|
+
*/
|
|
57
|
+
export const ManifestPayloadSchema = z
|
|
58
|
+
.object({
|
|
59
|
+
schemaVersion: z.string().optional(),
|
|
60
|
+
summary: z.string().optional(),
|
|
61
|
+
refs: z.array(ManifestRefSchema).optional(),
|
|
62
|
+
diagrams: z.array(ManifestDiagramSchema).optional(),
|
|
63
|
+
body: z.string().optional(),
|
|
64
|
+
stageKey: z.string().optional(),
|
|
65
|
+
})
|
|
66
|
+
.passthrough()
|
|
67
|
+
.superRefine((value, ctx) => {
|
|
68
|
+
const hasSummary =
|
|
69
|
+
typeof value.summary === 'string' && value.summary.trim().length > 0;
|
|
70
|
+
const hasBody = typeof value.body === 'string' && value.body.trim().length > 0;
|
|
71
|
+
const hasRefs = Array.isArray(value.refs) && value.refs.length > 0;
|
|
72
|
+
if (!hasSummary && !hasBody && !hasRefs) {
|
|
73
|
+
ctx.addIssue({
|
|
74
|
+
code: 'custom',
|
|
75
|
+
message:
|
|
76
|
+
'manifest payload must include at least one of `summary`, `body`, or non-empty `refs[]`.',
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
export type ManifestPayload = z.infer<typeof ManifestPayloadSchema>;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Payload shape for `OutputKind.MARKDOWN_DOC` artifacts.
|
|
5
|
+
*
|
|
6
|
+
* The canonical document-page shape across the platform. Used by the
|
|
7
|
+
* deliverable harvester (one artifact per page), `xema/emit-artifact`
|
|
8
|
+
* with markdown content, requirements/design/spec agents, and KB-bound
|
|
9
|
+
* publications.
|
|
10
|
+
*
|
|
11
|
+
* `body` is the rendered narrative — markdown source. The FE renderer
|
|
12
|
+
* (`MarkdownDocRenderer`) reads `body` directly via
|
|
13
|
+
* `useArtifactContent.textViewOfPayload` and renders it through
|
|
14
|
+
* `MarkdownView` (react-markdown + remark-gfm + mermaid).
|
|
15
|
+
*
|
|
16
|
+
* `slug` and `parentSlug` are KB-publish hints; the publish-kb activity
|
|
17
|
+
* reads them when present, otherwise it derives slugs from the
|
|
18
|
+
* artifact's title + phase. They are NOT required at emit-time — a
|
|
19
|
+
* deliverable that's reviewed but never published has no slug.
|
|
20
|
+
*
|
|
21
|
+
* `.passthrough()` is intentional: producers may attach arbitrary
|
|
22
|
+
* extras (e.g. front-matter metadata, agent commentary) without the
|
|
23
|
+
* schema rejecting the emit.
|
|
24
|
+
*/
|
|
25
|
+
export const MarkdownDocPayloadSchema = z
|
|
26
|
+
.object({
|
|
27
|
+
body: z.string().min(1, 'markdown_doc.body must be non-empty'),
|
|
28
|
+
slug: z.string().optional(),
|
|
29
|
+
parentSlug: z.string().optional(),
|
|
30
|
+
})
|
|
31
|
+
.passthrough();
|
|
32
|
+
|
|
33
|
+
export type MarkdownDocPayload = z.infer<typeof MarkdownDocPayloadSchema>;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Closed severity enum for `ReviewFinding.severity`. Drives FE
|
|
5
|
+
* grouping + colouring in `ReviewFindingsRenderer`. `info` is
|
|
6
|
+
* commentary that doesn't gate a verdict; `minor` / `major` /
|
|
7
|
+
* `blocking` escalate.
|
|
8
|
+
*/
|
|
9
|
+
export enum ReviewFindingSeverity {
|
|
10
|
+
INFO = 'info',
|
|
11
|
+
MINOR = 'minor',
|
|
12
|
+
MAJOR = 'major',
|
|
13
|
+
BLOCKING = 'blocking',
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const ReviewFindingSeveritySchema = z.nativeEnum(ReviewFindingSeverity);
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* A single finding inside a review-findings payload.
|
|
20
|
+
*
|
|
21
|
+
* `anchor` is a free-form string today (pre-W3C-anchor migration on
|
|
22
|
+
* REVIEW inquiries). Producers conventionally use shapes like
|
|
23
|
+
* `file:path:line` or `heading:slug`. A future tightening could
|
|
24
|
+
* promote this to a `WebAnnotationTarget` once the producer fleet
|
|
25
|
+
* is migrated, matching the inquiry-side anchor work.
|
|
26
|
+
*/
|
|
27
|
+
export const ReviewFindingSchema = z
|
|
28
|
+
.object({
|
|
29
|
+
severity: ReviewFindingSeveritySchema,
|
|
30
|
+
message: z.string().min(1, 'review_findings.findings[].message must be non-empty'),
|
|
31
|
+
anchor: z.string().optional(),
|
|
32
|
+
})
|
|
33
|
+
.passthrough();
|
|
34
|
+
|
|
35
|
+
export type ReviewFinding = z.infer<typeof ReviewFindingSchema>;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Payload shape for `OutputKind.REVIEW_FINDINGS` artifacts — emitted
|
|
39
|
+
* by reviewer agents inside `xema/review` iterations.
|
|
40
|
+
*
|
|
41
|
+
* Two valid shapes (the FE renderer detects both):
|
|
42
|
+
*
|
|
43
|
+
* 1. Structured: `findings: ReviewFinding[]` — preferred. Renders
|
|
44
|
+
* as a severity-grouped table with anchors.
|
|
45
|
+
* 2. Legacy: `body: string` — markdown narrative of the review.
|
|
46
|
+
* The renderer falls back to markdown rendering when `findings`
|
|
47
|
+
* is absent.
|
|
48
|
+
*
|
|
49
|
+
* `verdict` + `summary` are optional rollups — the reviewer's
|
|
50
|
+
* top-line answer and a short paragraph framing the detailed
|
|
51
|
+
* findings.
|
|
52
|
+
*/
|
|
53
|
+
export const ReviewFindingsPayloadSchema = z
|
|
54
|
+
.object({
|
|
55
|
+
findings: z.array(ReviewFindingSchema).optional(),
|
|
56
|
+
verdict: z.enum(['approve', 'reject', 'abstain']).optional(),
|
|
57
|
+
summary: z.string().optional(),
|
|
58
|
+
body: z.string().optional(),
|
|
59
|
+
})
|
|
60
|
+
.passthrough()
|
|
61
|
+
.superRefine((value, ctx) => {
|
|
62
|
+
const hasFindings = Array.isArray(value.findings) && value.findings.length > 0;
|
|
63
|
+
const hasBody = typeof value.body === 'string' && value.body.trim().length > 0;
|
|
64
|
+
const hasSummary =
|
|
65
|
+
typeof value.summary === 'string' && value.summary.trim().length > 0;
|
|
66
|
+
if (!hasFindings && !hasBody && !hasSummary) {
|
|
67
|
+
ctx.addIssue({
|
|
68
|
+
code: 'custom',
|
|
69
|
+
message:
|
|
70
|
+
'review_findings payload must include at least one of `findings[]`, `summary`, or `body`.',
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
export type ReviewFindingsPayload = z.infer<typeof ReviewFindingsPayloadSchema>;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Payload shape for `OutputKind.SCHEMA` artifacts — JSON Schema
|
|
5
|
+
* documents declared by agents.
|
|
6
|
+
*
|
|
7
|
+
* The payload IS the JSON Schema document. We don't dictate which
|
|
8
|
+
* dialect (Draft 07, 2020-12, OpenAPI) — the validator we use
|
|
9
|
+
* downstream picks based on the document's own `$schema` field.
|
|
10
|
+
* Minimum check: it must be a non-empty object.
|
|
11
|
+
*
|
|
12
|
+
* The FE `SchemaRenderer` reads the payload directly via
|
|
13
|
+
* `useArtifactContent.payload` (NOT `content`) and renders the
|
|
14
|
+
* schema as a typed tree.
|
|
15
|
+
*/
|
|
16
|
+
export const JsonSchemaPayloadSchema = z
|
|
17
|
+
.record(z.string(), z.unknown())
|
|
18
|
+
.refine((v) => Object.keys(v).length > 0, {
|
|
19
|
+
message: 'schema payload must be a non-empty JSON Schema document.',
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
export type JsonSchemaPayload = z.infer<typeof JsonSchemaPayloadSchema>;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Outcome of a single test in a test-report payload. Closed enum
|
|
5
|
+
* because the FE renders each state with distinctive iconography.
|
|
6
|
+
*/
|
|
7
|
+
export enum TestOutcome {
|
|
8
|
+
PASSED = 'passed',
|
|
9
|
+
FAILED = 'failed',
|
|
10
|
+
SKIPPED = 'skipped',
|
|
11
|
+
ERRORED = 'errored',
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const TestOutcomeSchema = z.nativeEnum(TestOutcome);
|
|
15
|
+
|
|
16
|
+
export const TestCaseSchema = z
|
|
17
|
+
.object({
|
|
18
|
+
name: z.string().min(1),
|
|
19
|
+
outcome: TestOutcomeSchema,
|
|
20
|
+
/** Duration in milliseconds; producers without timing data omit. */
|
|
21
|
+
durationMs: z.number().nonnegative().optional(),
|
|
22
|
+
/** Failure / error message for FAILED + ERRORED outcomes. */
|
|
23
|
+
message: z.string().optional(),
|
|
24
|
+
/** Optional path to the file under test (e.g. `src/auth.test.ts`). */
|
|
25
|
+
file: z.string().optional(),
|
|
26
|
+
})
|
|
27
|
+
.passthrough();
|
|
28
|
+
|
|
29
|
+
export type TestCase = z.infer<typeof TestCaseSchema>;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Payload shape for `OutputKind.TEST_REPORT` artifacts.
|
|
33
|
+
*
|
|
34
|
+
* `tests[]` is the per-case detail; the rollup counters
|
|
35
|
+
* (`passed`, `failed`, `skipped`, `errored`) are derived from
|
|
36
|
+
* `tests[]` when present but may be supplied directly when the
|
|
37
|
+
* producer only has aggregate numbers.
|
|
38
|
+
*
|
|
39
|
+
* `body` is the legacy markdown fallback (same pattern as other
|
|
40
|
+
* markdown-bearing kinds).
|
|
41
|
+
*/
|
|
42
|
+
export const TestReportPayloadSchema = z
|
|
43
|
+
.object({
|
|
44
|
+
tests: z.array(TestCaseSchema).optional(),
|
|
45
|
+
passed: z.number().nonnegative().int().optional(),
|
|
46
|
+
failed: z.number().nonnegative().int().optional(),
|
|
47
|
+
skipped: z.number().nonnegative().int().optional(),
|
|
48
|
+
errored: z.number().nonnegative().int().optional(),
|
|
49
|
+
summary: z.string().optional(),
|
|
50
|
+
body: z.string().optional(),
|
|
51
|
+
})
|
|
52
|
+
.passthrough()
|
|
53
|
+
.superRefine((value, ctx) => {
|
|
54
|
+
const hasTests = Array.isArray(value.tests) && value.tests.length > 0;
|
|
55
|
+
const hasCounters =
|
|
56
|
+
typeof value.passed === 'number' ||
|
|
57
|
+
typeof value.failed === 'number' ||
|
|
58
|
+
typeof value.skipped === 'number';
|
|
59
|
+
const hasBody = typeof value.body === 'string' && value.body.trim().length > 0;
|
|
60
|
+
if (!hasTests && !hasCounters && !hasBody) {
|
|
61
|
+
ctx.addIssue({
|
|
62
|
+
code: 'custom',
|
|
63
|
+
message:
|
|
64
|
+
'test_report payload must include at least one of `tests[]`, the rollup counters, or `body`.',
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
export type TestReportPayload = z.infer<typeof TestReportPayloadSchema>;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
2
|
+
// ── Workspace Image Contracts — Barrel Export ──
|
|
3
|
+
//
|
|
4
|
+
// Kernel (Layer 1) contracts for per-org workspace image variants. Pure
|
|
5
|
+
// types, closed enums, zod schemas, plus the deterministic content-hash
|
|
6
|
+
// helper used to identify a variant from its declarative manifest +
|
|
7
|
+
// base-image digest. Zero NestJS, zero Prisma.
|
|
8
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
9
|
+
|
|
10
|
+
export * from './lib/enums';
|
|
11
|
+
export * from './lib/types';
|
|
12
|
+
export * from './lib/hash';
|
|
13
|
+
export * from './lib/schemas';
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
2
|
+
// ── Workspace image — closed-domain enums ──
|
|
3
|
+
//
|
|
4
|
+
// Closed sets backing the per-org workspace image variants described in
|
|
5
|
+
// the ImageLayerKind enumerates every layer recipe the kernel
|
|
6
|
+
// understands; VariantStatus enumerates the bake lifecycle.
|
|
7
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Closed set of layer-recipe kinds. Each kind has a strict TypeScript
|
|
11
|
+
* payload shape (see `ImageLayer` in `types.ts`).
|
|
12
|
+
*
|
|
13
|
+
* IMPORTANT: there is no `entrypoint` / `cmd` kind — the entrypoint is
|
|
14
|
+
* owned by the worker-runtime, never by an org-supplied recipe.
|
|
15
|
+
*/
|
|
16
|
+
export enum ImageLayerKind {
|
|
17
|
+
Apt = 'apt',
|
|
18
|
+
Dnf = 'dnf',
|
|
19
|
+
Apk = 'apk',
|
|
20
|
+
Pip = 'pip',
|
|
21
|
+
NpmGlobal = 'npm-global',
|
|
22
|
+
Env = 'env',
|
|
23
|
+
File = 'file',
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Lifecycle of a single image variant. `draft` is editable in the UI;
|
|
28
|
+
* `building` is the active bake; `ready` is mountable by workers;
|
|
29
|
+
* `failed` retains the build log for debugging; `archived` keeps the row
|
|
30
|
+
* for history but disallows new use.
|
|
31
|
+
*/
|
|
32
|
+
export enum VariantStatus {
|
|
33
|
+
Draft = 'draft',
|
|
34
|
+
Building = 'building',
|
|
35
|
+
Ready = 'ready',
|
|
36
|
+
Failed = 'failed',
|
|
37
|
+
Archived = 'archived',
|
|
38
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
2
|
+
// ── Workspace image — deterministic content hash ──
|
|
3
|
+
//
|
|
4
|
+
// Given a `WorkspaceImageManifest` plus the resolved base-image digest,
|
|
5
|
+
// produce a stable sha256 used as the variant's content identity. Two
|
|
6
|
+
// inputs that differ only in JSON formatting MUST hash to the same value;
|
|
7
|
+
// two inputs that differ semantically MUST NOT collide.
|
|
8
|
+
//
|
|
9
|
+
// Strategy:
|
|
10
|
+
// 1. Canonicalize: recursively sort object keys; arrays keep their
|
|
11
|
+
// authored order (order is semantically meaningful — `apt install`
|
|
12
|
+
// ordering can change layer caching and image size).
|
|
13
|
+
// 2. JSON.stringify the canonical form (no whitespace, stable key order).
|
|
14
|
+
// 3. sha256 over the concatenation `canonicalManifest + '\n' + digest`.
|
|
15
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
16
|
+
|
|
17
|
+
import { createHash } from 'node:crypto';
|
|
18
|
+
|
|
19
|
+
import type { WorkspaceImageManifest } from './types';
|
|
20
|
+
|
|
21
|
+
type CanonicalJson =
|
|
22
|
+
| null
|
|
23
|
+
| boolean
|
|
24
|
+
| number
|
|
25
|
+
| string
|
|
26
|
+
| readonly CanonicalJson[]
|
|
27
|
+
| { readonly [key: string]: CanonicalJson };
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Recursively produce a canonical, order-stable JSON-compatible value:
|
|
31
|
+
*
|
|
32
|
+
* - Objects: keys sorted lexicographically. `undefined` values dropped
|
|
33
|
+
* (mirrors `JSON.stringify`'s default and avoids two inputs differing
|
|
34
|
+
* only by an explicit `undefined` hashing differently).
|
|
35
|
+
* - Arrays: order is preserved (semantically meaningful in image layers).
|
|
36
|
+
* - Primitives & null: pass through.
|
|
37
|
+
*
|
|
38
|
+
* Throws on values JSON cannot represent (functions, symbols, bigint,
|
|
39
|
+
* circular refs) — fail-fast per the project's constitution.
|
|
40
|
+
*/
|
|
41
|
+
function canonicalize(value: unknown, seen: WeakSet<object> = new WeakSet()): CanonicalJson {
|
|
42
|
+
if (value === null) return null;
|
|
43
|
+
const t = typeof value;
|
|
44
|
+
if (t === 'string' || t === 'boolean') {
|
|
45
|
+
return value as CanonicalJson;
|
|
46
|
+
}
|
|
47
|
+
if (t === 'number') {
|
|
48
|
+
if (!Number.isFinite(value as number)) {
|
|
49
|
+
throw new Error('canonicalize: non-finite number is not JSON-representable');
|
|
50
|
+
}
|
|
51
|
+
return value as number;
|
|
52
|
+
}
|
|
53
|
+
if (t === 'undefined') {
|
|
54
|
+
// Will be dropped by the object pass; encountering it at the root or
|
|
55
|
+
// inside an array indicates a programmer error.
|
|
56
|
+
throw new Error('canonicalize: undefined is not JSON-representable');
|
|
57
|
+
}
|
|
58
|
+
if (t === 'bigint' || t === 'function' || t === 'symbol') {
|
|
59
|
+
throw new Error(`canonicalize: ${t} is not JSON-representable`);
|
|
60
|
+
}
|
|
61
|
+
if (Array.isArray(value)) {
|
|
62
|
+
if (seen.has(value)) throw new Error('canonicalize: circular reference');
|
|
63
|
+
seen.add(value);
|
|
64
|
+
return value.map((entry) => canonicalize(entry, seen));
|
|
65
|
+
}
|
|
66
|
+
// Plain object.
|
|
67
|
+
if (seen.has(value as object)) throw new Error('canonicalize: circular reference');
|
|
68
|
+
seen.add(value as object);
|
|
69
|
+
const entries = Object.entries(value as Record<string, unknown>)
|
|
70
|
+
.filter(([, v]) => v !== undefined)
|
|
71
|
+
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
|
|
72
|
+
const out: Record<string, CanonicalJson> = {};
|
|
73
|
+
for (const [k, v] of entries) {
|
|
74
|
+
out[k] = canonicalize(v, seen);
|
|
75
|
+
}
|
|
76
|
+
return out;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Compute the canonical content hash of a workspace image variant. The
|
|
81
|
+
* `baseImageDigest` MUST be the resolved digest of the base image at
|
|
82
|
+
* bake-time (not a mutable tag) — otherwise a tag drift would silently
|
|
83
|
+
* invalidate the hash.
|
|
84
|
+
*/
|
|
85
|
+
export function computeContentHash(
|
|
86
|
+
manifest: WorkspaceImageManifest,
|
|
87
|
+
baseImageDigest: string,
|
|
88
|
+
): string {
|
|
89
|
+
if (!baseImageDigest || typeof baseImageDigest !== 'string') {
|
|
90
|
+
throw new Error('computeContentHash: baseImageDigest is required');
|
|
91
|
+
}
|
|
92
|
+
const canonical = canonicalize(manifest);
|
|
93
|
+
const serialized = JSON.stringify(canonical);
|
|
94
|
+
return createHash('sha256').update(`${serialized}\n${baseImageDigest}`).digest('hex');
|
|
95
|
+
}
|