slidemind 0.3.20 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/AGENTS.md +592 -16
- package/LICENSE +4 -0
- package/README.md +203 -8
- package/THIRD-PARTY-NOTICES.md +201 -0
- package/bin/slidemind.mjs +50 -2
- package/package.json +5 -3
- package/skills/create-slides/SKILL.md +484 -886
- package/skills/search/SKILL.md +46 -23
- package/src/bridge/build-and-push.mjs +548 -73
- package/src/bridge/bundle.mjs +100 -31
- package/src/bridge/capture.mjs +14 -17
- package/src/bridge/client/brands.mjs +157 -18
- package/src/bridge/client/engine-routing.mjs +48 -13
- package/src/bridge/client/engine-search.mjs +24 -1
- package/src/bridge/client/render.mjs +27 -1
- package/src/bridge/client/transport.mjs +499 -23
- package/src/bridge/client.mjs +10 -0
- package/src/bridge/config.mjs +67 -6
- package/src/bridge/engine-download.mjs +0 -10
- package/src/bridge/graph-auth.mjs +55 -3
- package/src/bridge/image-intent-envelope.mjs +239 -0
- package/src/bridge/package-preview.mjs +67 -0
- package/src/bridge/sharepoint.mjs +914 -0
- package/src/builders/asset-fetch.mjs +203 -0
- package/src/builders/block-dividers.mjs +83 -0
- package/src/builders/build.mjs +171 -87
- package/src/builders/card-height.mjs +294 -0
- package/src/builders/chart-hole.mjs +183 -0
- package/src/builders/chart-jsx.mjs +410 -0
- package/src/builders/chart-labels.mjs +326 -0
- package/src/builders/concept-geometry.mjs +370 -0
- package/src/builders/concept-intent.mjs +200 -0
- package/src/builders/concept-layout.mjs +79 -0
- package/src/builders/concept-lines.mjs +509 -0
- package/src/builders/concept-lists.mjs +270 -0
- package/src/builders/concept-marks.mjs +84 -0
- package/src/builders/concept-relationships.mjs +12 -0
- package/src/builders/concept-spec.mjs +645 -0
- package/src/builders/concept-text.mjs +140 -0
- package/src/builders/concept-waves.mjs +57 -0
- package/src/builders/copy-fit.mjs +139 -0
- package/src/builders/design-tokens.mjs +347 -0
- package/src/builders/draft-check.mjs +552 -340
- package/src/builders/draft-lines.mjs +369 -0
- package/src/builders/edit-route.mjs +29 -0
- package/src/builders/figure-sketch.mjs +375 -0
- package/src/builders/fold-sets.mjs +639 -0
- package/src/builders/gate-server.mjs +9 -4
- package/src/builders/gate-session.js +273 -12
- package/src/builders/gate-shell.js +1494 -0
- package/src/builders/image-intent-check.mjs +308 -0
- package/src/builders/image-use-reconcile.mjs +107 -0
- package/src/builders/kit-helpers.mjs +121 -0
- package/src/builders/layout-names.mjs +32 -0
- package/src/builders/legacy-reference-uses.mjs +179 -0
- package/src/builders/line-fit.mjs +224 -0
- package/src/builders/line-native.mjs +895 -0
- package/src/builders/line-relations.mjs +230 -0
- package/src/builders/lint-fix.mjs +117 -9
- package/src/builders/lint.mjs +25 -20
- package/src/builders/list-jsx.mjs +88 -0
- package/src/builders/placement.mjs +133 -0
- package/src/builders/plan-checks.mjs +135 -0
- package/src/builders/plan-comments.mjs +19 -0
- package/src/builders/plan-handlers.mjs +95 -0
- package/src/builders/plan-markdown-edit.mjs +3 -3
- package/src/builders/plan-overlay.js +1530 -1608
- package/src/builders/plan.mjs +689 -186
- package/src/builders/preset-definitions.json +1 -0
- package/src/builders/preset-eval.mjs +244 -0
- package/src/builders/preview.html +51 -120
- package/src/builders/red-thread-backbone.mjs +135 -36
- package/src/builders/repair.mjs +205 -0
- package/src/builders/repeat-groups.mjs +540 -0
- package/src/builders/resolve-skill.mjs +5 -4
- package/src/builders/review-overlay.js +296 -293
- package/src/builders/review.mjs +305 -84
- package/src/builders/rule-shift.mjs +96 -0
- package/src/builders/seat.mjs +354 -0
- package/src/builders/settle-driver.mjs +236 -0
- package/src/builders/settle.mjs +1448 -0
- package/src/builders/slide-prep.mjs +67 -0
- package/src/builders/spec-assets.mjs +236 -0
- package/src/builders/spec-match.mjs +56 -0
- package/src/builders/stack.mjs +413 -0
- package/src/builders/storyboard-markdown.mjs +70 -27
- package/src/builders/storyboard-schema.mjs +54 -114
- package/src/builders/text-blocks.mjs +212 -0
- package/src/builders/text-card-fit.mjs +552 -0
- package/src/builders/text-stack.mjs +276 -0
- package/src/builders/transcribe.mjs +1069 -0
- package/src/cli/assets.mjs +164 -0
- package/src/cli/common.mjs +173 -12
- package/src/cli/concept-spec-support.mjs +76 -0
- package/src/cli/concepts.mjs +1002 -103
- package/src/cli/deck.mjs +401 -65
- package/src/cli/diagnose.mjs +4 -2
- package/src/cli/engine-helpers.mjs +17 -10
- package/src/cli/engine-results.mjs +76 -3
- package/src/cli/help.mjs +232 -28
- package/src/cli/install.mjs +9 -0
- package/src/cli/maintenance.mjs +104 -0
- package/src/cli/read.mjs +2 -1
- package/src/cli/ready.mjs +6 -3
- package/src/cli/record.mjs +522 -0
- package/src/cli/remote.mjs +818 -0
- package/src/cli/revision.mjs +686 -0
- package/src/cli/search.mjs +30 -11
- package/src/cli/session.mjs +56 -13
- package/src/cli/setup.mjs +46 -2
- package/src/cli/status.mjs +142 -33
- package/src/cli/tasks.mjs +246 -0
- package/src/cli/transcribe.mjs +854 -0
- package/src/cli/workspace-ui.mjs +260 -0
- package/src/cli/workspace.mjs +212 -34
- package/src/install-channel.mjs +64 -7
- package/src/lib/adopt.mjs +346 -25
- package/src/lib/concept-spec-binding.mjs +96 -0
- package/src/lib/conversions.mjs +70 -6
- package/src/lib/convert-folded.mjs +107 -0
- package/src/lib/deck-paths.mjs +418 -0
- package/src/lib/durable-ids.mjs +47 -0
- package/src/lib/edit-operations.mjs +316 -0
- package/src/lib/edit-state.mjs +1244 -0
- package/src/lib/edit-transitions.mjs +120 -0
- package/src/lib/extensions.mjs +501 -0
- package/src/lib/image-assets.mjs +465 -0
- package/src/lib/image-derivatives.mjs +234 -0
- package/src/lib/image-intent-fingerprint.mjs +70 -0
- package/src/lib/image-intent-tags.mjs +176 -0
- package/src/lib/image-intent.mjs +802 -0
- package/src/lib/image-use-overrides.mjs +279 -0
- package/src/lib/inline-components.mjs +613 -0
- package/src/lib/lint-converted.mjs +68 -4
- package/src/lib/merge-json.mjs +163 -0
- package/src/lib/overwrite-guard.mjs +3 -3
- package/src/lib/presentation-context.mjs +531 -0
- package/src/lib/preservation-tags.mjs +157 -0
- package/src/lib/preservation-verdict.mjs +1342 -0
- package/src/lib/publish.mjs +1557 -0
- package/src/lib/rebase.mjs +945 -0
- package/src/lib/reconcile-ack.mjs +4 -6
- package/src/lib/reconcile.mjs +62 -24
- package/src/lib/record-client.mjs +175 -0
- package/src/lib/record-state.mjs +151 -0
- package/src/lib/remote.mjs +661 -0
- package/src/lib/revision.mjs +1210 -0
- package/src/lib/slide-guids.mjs +4 -6
- package/src/lib/slide-preservation.mjs +423 -0
- package/src/lib/slide-selection.mjs +9 -0
- package/src/lib/workspace-transaction.mjs +117 -0
- package/src/pptx/content-digest.mjs +334 -0
- package/src/pptx/deck-assemble.mjs +1278 -0
- package/src/pptx/fill/engine.mjs +2 -3
- package/src/pptx/fill/freeform.mjs +6 -3
- package/src/pptx/ooxml.mjs +20 -0
- package/src/pptx/package-evidence.mjs +383 -0
- package/src/pptx/tags.mjs +163 -11
- package/src/update-check.mjs +22 -4
- package/src/workspace/adapters.mjs +222 -0
- package/src/workspace/auth.mjs +256 -0
- package/src/workspace/autostart.mjs +204 -0
- package/src/workspace/daemon.mjs +360 -0
- package/src/workspace/discovery.mjs +180 -0
- package/src/workspace/edit.mjs +167 -0
- package/src/workspace/events.mjs +52 -0
- package/src/workspace/gate-client.mjs +290 -0
- package/src/workspace/handback.mjs +73 -0
- package/src/workspace/lifecycle.mjs +635 -0
- package/src/workspace/operations.mjs +200 -0
- package/src/workspace/pick-folder.mjs +152 -0
- package/src/workspace/registry.mjs +359 -0
- package/src/workspace/remote-links.mjs +397 -0
- package/src/workspace/remote-status.mjs +359 -0
- package/src/workspace/routes.mjs +1331 -0
- package/src/workspace/server.mjs +159 -0
- package/src/workspace/sync.mjs +1119 -0
- package/src/workspace/task-actions.mjs +215 -0
- package/src/workspace/tasks.mjs +507 -0
- package/src/workspace/ui/app.js +1886 -0
- package/src/workspace/ui/index.html +141 -0
- package/src/workspace/ui/plan-bridge.js +120 -0
- package/src/workspace/ui/sharepoint.svg +59 -0
- package/src/workspace/ui/styles.css +423 -0
- package/src/builders/visual-families.mjs +0 -46
|
@@ -0,0 +1,802 @@
|
|
|
1
|
+
// The image-intent contract, version 1 — the schema half.
|
|
2
|
+
//
|
|
3
|
+
// An **asset** is immutable bytes (sha256 + sniffed mime + intrinsic size +
|
|
4
|
+
// a portable workspace locator). A **use** is what those bytes are for on one
|
|
5
|
+
// slide: `original` (the saved picture must appear), `reconstruct` (rebuild it
|
|
6
|
+
// as editable content) or `reference` (guidance only). One asset carries many
|
|
7
|
+
// independent uses; the registry itself obliges nothing.
|
|
8
|
+
//
|
|
9
|
+
// Everything here is STRICT. The storyboard parser next door normalizes away
|
|
10
|
+
// what it does not recognise — it has to, it reads a file humans edit. This one
|
|
11
|
+
// must not: an unknown fidelity, a crop that leaves the image, a `fit` on a
|
|
12
|
+
// reference use are all bugs in whatever wrote the record, and silently
|
|
13
|
+
// dropping them is how an approved obligation disappears between the plan and
|
|
14
|
+
// the .pptx. So every parser returns `{ value, issues }` with `value === null`
|
|
15
|
+
// whenever `issues` is non-empty, and each issue is the typed
|
|
16
|
+
// `{code, assetId?, useId?, slide?, message, action}` shape the guide's error
|
|
17
|
+
// table defines.
|
|
18
|
+
//
|
|
19
|
+
// Canonical here; the add-in imports a byte-identical mirror (typed by its
|
|
20
|
+
// `src/ui/src/shared/image-intent.ts`) through scripts/sync-transcription-runtime.mjs,
|
|
21
|
+
// which also mirrors the shared fixture `test/fixtures/image-intent/contract.json`.
|
|
22
|
+
//
|
|
23
|
+
// NO Node builtins, and module-scope helpers carry an `ii`/`II_` prefix. That is the shape
|
|
24
|
+
// `src/builders/plan.mjs` requires of anything it concatenates into the plan
|
|
25
|
+
// gate's browser IIFE (imports stripped, one shared scope), which the plan-review
|
|
26
|
+
// image-use summary will need. The SHA-256 half is therefore a separate module,
|
|
27
|
+
// `image-intent-fingerprint.mjs`.
|
|
28
|
+
|
|
29
|
+
export const IMAGE_INTENT_VERSION = 1;
|
|
30
|
+
|
|
31
|
+
export const IMAGE_FIDELITIES = ["original", "reconstruct", "reference"];
|
|
32
|
+
export const RECONSTRUCTION_SCOPES = ["slide", "element"];
|
|
33
|
+
export const PROVENANCE_KINDS = ["attachment", "local", "engine", "generated", "web"];
|
|
34
|
+
|
|
35
|
+
// What the existing image pipeline can already decode and ship. Grounded in the
|
|
36
|
+
// two MIME tables both repos already keep (CLI `src/bridge/engine-download.mjs`,
|
|
37
|
+
// add-in `RT/agents/design/runtime/dispatcher.ts`). Anything else is rejected at
|
|
38
|
+
// registration — this contract adds no decoder.
|
|
39
|
+
export const SUPPORTED_IMAGE_MIME_TYPES = [
|
|
40
|
+
"image/png",
|
|
41
|
+
"image/jpeg",
|
|
42
|
+
"image/gif",
|
|
43
|
+
"image/webp",
|
|
44
|
+
"image/svg+xml",
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
export const MAX_INSTRUCTION_LENGTH = 2000;
|
|
48
|
+
export const MAX_ID_LENGTH = 128;
|
|
49
|
+
export const MAX_DISPLAY_NAME_LENGTH = 200;
|
|
50
|
+
export const MAX_SLOT_LENGTH = 64;
|
|
51
|
+
|
|
52
|
+
const II_ID = /^[A-Za-z0-9_-]+$/;
|
|
53
|
+
const II_SHA256 = /^[0-9a-f]{64}$/;
|
|
54
|
+
// `assets/local/<safe-id>.<ext>` and nothing else: no absolute path, no `..`,
|
|
55
|
+
// no second directory. A28 (traversal) is refused here, before any byte I/O.
|
|
56
|
+
const II_WORKSPACE_PATH = /^assets\/local\/[A-Za-z0-9_-]+\.[A-Za-z0-9]+$/;
|
|
57
|
+
const II_DATA_URI = /^data:([a-z]+\/[a-z0-9.+-]+);base64,[A-Za-z0-9+/]+=*$/;
|
|
58
|
+
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
// issues
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
|
|
63
|
+
// The contract's WHOLE issue vocabulary, in the order the implementation
|
|
64
|
+
// guide's error table lists it. Every module on either side of the wire — the
|
|
65
|
+
// byte half, the derivative half, the route validators, the export checks —
|
|
66
|
+
// draws its codes from here; none may invent one of its own, because a code
|
|
67
|
+
// this list does not have is a code the other repository cannot recognise.
|
|
68
|
+
//
|
|
69
|
+
// Not every code is raised by THIS module: the schema half never touches bytes,
|
|
70
|
+
// so `image_asset_changed` and `image_asset_too_large` come from
|
|
71
|
+
// `image-assets.mjs`, and the `image_use_*` / `image_intent_stale` codes from
|
|
72
|
+
// the later export and fingerprint checks. The vocabulary is still declared in
|
|
73
|
+
// one place, and the shared fixture's `codes` array holds both repositories to
|
|
74
|
+
// it (`test/fixtures/image-intent/contract.json`).
|
|
75
|
+
export const IMAGE_INTENT_ISSUE_CODES = [
|
|
76
|
+
"image_intent_invalid",
|
|
77
|
+
"image_intent_conflict",
|
|
78
|
+
"image_asset_missing",
|
|
79
|
+
"image_asset_changed",
|
|
80
|
+
"image_asset_unsupported",
|
|
81
|
+
"image_asset_too_large",
|
|
82
|
+
"image_use_unresolved",
|
|
83
|
+
"image_use_mismatch",
|
|
84
|
+
"image_intent_unsupported",
|
|
85
|
+
"image_intent_stale",
|
|
86
|
+
];
|
|
87
|
+
|
|
88
|
+
/** One typed failure. `action` is what the agent or the user does about it. */
|
|
89
|
+
function iiIssue(code, message, action, extra = {}) {
|
|
90
|
+
const issue = { code, message, action };
|
|
91
|
+
if (extra.assetId !== undefined) issue.assetId = extra.assetId;
|
|
92
|
+
if (extra.useId !== undefined) issue.useId = extra.useId;
|
|
93
|
+
if (extra.slide !== undefined) issue.slide = extra.slide;
|
|
94
|
+
return issue;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const II_ACTIONS = {
|
|
98
|
+
invalid: "Correct the named field and re-record the use.",
|
|
99
|
+
conflict: "Resolve the duplicate or competing record in the plan before generation.",
|
|
100
|
+
missing: "Import the source again and rebind the use.",
|
|
101
|
+
unsupported: "Supply a source in a supported image format.",
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
function iiInvalid(message, extra) {
|
|
105
|
+
return iiIssue("image_intent_invalid", message, II_ACTIONS.invalid, extra);
|
|
106
|
+
}
|
|
107
|
+
function iiConflict(message, extra) {
|
|
108
|
+
return iiIssue("image_intent_conflict", message, II_ACTIONS.conflict, extra);
|
|
109
|
+
}
|
|
110
|
+
function iiMissing(message, extra) {
|
|
111
|
+
return iiIssue("image_asset_missing", message, II_ACTIONS.missing, extra);
|
|
112
|
+
}
|
|
113
|
+
function iiUnsupported(message, extra) {
|
|
114
|
+
return iiIssue("image_asset_unsupported", message, II_ACTIONS.unsupported, extra);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** `{ value, issues }`, with value withheld whenever anything failed. */
|
|
118
|
+
function iiResult(value, issues) {
|
|
119
|
+
return issues.length ? { value: null, issues } : { value, issues: [] };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ---------------------------------------------------------------------------
|
|
123
|
+
// primitives
|
|
124
|
+
// ---------------------------------------------------------------------------
|
|
125
|
+
|
|
126
|
+
function iiIsRecord(value) {
|
|
127
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function iiIsFraction(value) {
|
|
131
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 && value <= 1;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function iiIsPositiveInt(value) {
|
|
135
|
+
return typeof value === "number" && Number.isInteger(value) && value > 0;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function iiId(value, field, issues, extra) {
|
|
139
|
+
if (typeof value !== "string" || !II_ID.test(value) || value.length > MAX_ID_LENGTH) {
|
|
140
|
+
issues.push(
|
|
141
|
+
iiInvalid(
|
|
142
|
+
`${field} must match /^[A-Za-z0-9_-]+$/ and be at most ${MAX_ID_LENGTH} characters.`,
|
|
143
|
+
extra,
|
|
144
|
+
),
|
|
145
|
+
);
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
return value;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// ---------------------------------------------------------------------------
|
|
152
|
+
// AssetUse
|
|
153
|
+
// ---------------------------------------------------------------------------
|
|
154
|
+
|
|
155
|
+
const II_ORIGINAL_ONLY = ["slot", "fit", "position", "crop"];
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Parse one `assetUses[]` entry.
|
|
159
|
+
*
|
|
160
|
+
* Normalization is exactly two things, both of them specified: the instruction
|
|
161
|
+
* is trimmed, and an `original` use gains its defaults (`fit: "contain"`,
|
|
162
|
+
* `position: {x: 0.5, y: 0.5}`). Nothing else is repaired.
|
|
163
|
+
*
|
|
164
|
+
* @param {unknown} raw
|
|
165
|
+
* @param {{slide?: number}} [ctx] slide number to stamp onto issues
|
|
166
|
+
*/
|
|
167
|
+
export function parseAssetUse(raw, ctx = {}) {
|
|
168
|
+
const issues = [];
|
|
169
|
+
const slide = ctx.slide;
|
|
170
|
+
if (!iiIsRecord(raw)) {
|
|
171
|
+
return iiResult(null, [iiInvalid("An image use must be a JSON object.", { slide })]);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const useId = iiId(raw.useId, "useId", issues, { slide });
|
|
175
|
+
const where = { slide, useId: useId ?? undefined };
|
|
176
|
+
const assetId = iiId(raw.assetId, "assetId", issues, where);
|
|
177
|
+
if (assetId) where.assetId = assetId;
|
|
178
|
+
|
|
179
|
+
const fidelity = raw.fidelity;
|
|
180
|
+
if (!IMAGE_FIDELITIES.includes(fidelity)) {
|
|
181
|
+
issues.push(
|
|
182
|
+
iiInvalid(`fidelity must be one of ${IMAGE_FIDELITIES.join(", ")}.`, where),
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
let instruction = null;
|
|
187
|
+
if (typeof raw.instruction !== "string") {
|
|
188
|
+
issues.push(iiInvalid("instruction must be a string.", where));
|
|
189
|
+
} else {
|
|
190
|
+
instruction = raw.instruction.trim();
|
|
191
|
+
if (!instruction) {
|
|
192
|
+
issues.push(iiInvalid("instruction must not be empty.", where));
|
|
193
|
+
instruction = null;
|
|
194
|
+
} else if (instruction.length > MAX_INSTRUCTION_LENGTH) {
|
|
195
|
+
// Not truncated: a clipped instruction is a changed obligation, and the
|
|
196
|
+
// agent that wrote it is the one that must shorten it.
|
|
197
|
+
issues.push(
|
|
198
|
+
iiInvalid(
|
|
199
|
+
`instruction must be at most ${MAX_INSTRUCTION_LENGTH} characters (got ${instruction.length}).`,
|
|
200
|
+
where,
|
|
201
|
+
),
|
|
202
|
+
);
|
|
203
|
+
instruction = null;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const isReconstruct = fidelity === "reconstruct";
|
|
208
|
+
const isOriginal = fidelity === "original";
|
|
209
|
+
|
|
210
|
+
let reconstructionScope;
|
|
211
|
+
if (isReconstruct) {
|
|
212
|
+
if (!RECONSTRUCTION_SCOPES.includes(raw.reconstructionScope)) {
|
|
213
|
+
issues.push(
|
|
214
|
+
iiInvalid(
|
|
215
|
+
`reconstructionScope is required for a reconstruct use and must be one of ${RECONSTRUCTION_SCOPES.join(", ")}.`,
|
|
216
|
+
where,
|
|
217
|
+
),
|
|
218
|
+
);
|
|
219
|
+
} else {
|
|
220
|
+
reconstructionScope = raw.reconstructionScope;
|
|
221
|
+
}
|
|
222
|
+
} else if (raw.reconstructionScope !== undefined) {
|
|
223
|
+
issues.push(
|
|
224
|
+
iiInvalid("reconstructionScope is only valid on a reconstruct use.", where),
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const framing = {};
|
|
229
|
+
if (!isOriginal) {
|
|
230
|
+
for (const field of II_ORIGINAL_ONLY) {
|
|
231
|
+
if (raw[field] !== undefined) {
|
|
232
|
+
issues.push(iiInvalid(`${field} is only valid on an original use.`, where));
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
} else {
|
|
236
|
+
if (raw.slot !== undefined) {
|
|
237
|
+
if (
|
|
238
|
+
typeof raw.slot !== "string" ||
|
|
239
|
+
!raw.slot.trim() ||
|
|
240
|
+
raw.slot.length > MAX_SLOT_LENGTH
|
|
241
|
+
) {
|
|
242
|
+
issues.push(
|
|
243
|
+
iiInvalid(
|
|
244
|
+
`slot must be a non-empty string of at most ${MAX_SLOT_LENGTH} characters.`,
|
|
245
|
+
where,
|
|
246
|
+
),
|
|
247
|
+
);
|
|
248
|
+
} else {
|
|
249
|
+
framing.slot = raw.slot.trim();
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (raw.fit === undefined) {
|
|
254
|
+
framing.fit = "contain";
|
|
255
|
+
} else if (raw.fit === "contain" || raw.fit === "cover") {
|
|
256
|
+
framing.fit = raw.fit;
|
|
257
|
+
} else {
|
|
258
|
+
issues.push(iiInvalid('fit must be "contain" or "cover".', where));
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (raw.position === undefined) {
|
|
262
|
+
framing.position = { x: 0.5, y: 0.5 };
|
|
263
|
+
} else if (
|
|
264
|
+
iiIsRecord(raw.position) &&
|
|
265
|
+
iiIsFraction(raw.position.x) &&
|
|
266
|
+
iiIsFraction(raw.position.y)
|
|
267
|
+
) {
|
|
268
|
+
framing.position = { x: raw.position.x, y: raw.position.y };
|
|
269
|
+
} else {
|
|
270
|
+
issues.push(iiInvalid("position must be {x, y} with both in [0, 1].", where));
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (raw.crop !== undefined) {
|
|
274
|
+
const c = raw.crop;
|
|
275
|
+
const shaped =
|
|
276
|
+
iiIsRecord(c) &&
|
|
277
|
+
iiIsFraction(c.x) &&
|
|
278
|
+
iiIsFraction(c.y) &&
|
|
279
|
+
iiIsFraction(c.w) &&
|
|
280
|
+
iiIsFraction(c.h);
|
|
281
|
+
if (!shaped) {
|
|
282
|
+
issues.push(
|
|
283
|
+
iiInvalid("crop must be {x, y, w, h} with every value in [0, 1].", where),
|
|
284
|
+
);
|
|
285
|
+
} else if (c.w <= 0 || c.h <= 0) {
|
|
286
|
+
issues.push(iiInvalid("crop w and h must be greater than 0.", where));
|
|
287
|
+
} else if (c.x + c.w > 1 || c.y + c.h > 1) {
|
|
288
|
+
issues.push(
|
|
289
|
+
iiInvalid("crop must stay inside the source: x+w <= 1 and y+h <= 1.", where),
|
|
290
|
+
);
|
|
291
|
+
} else {
|
|
292
|
+
framing.crop = { x: c.x, y: c.y, w: c.w, h: c.h };
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if (issues.length) return iiResult(null, issues);
|
|
298
|
+
|
|
299
|
+
return iiResult(
|
|
300
|
+
{
|
|
301
|
+
useId,
|
|
302
|
+
assetId,
|
|
303
|
+
fidelity,
|
|
304
|
+
instruction,
|
|
305
|
+
...(reconstructionScope ? { reconstructionScope } : {}),
|
|
306
|
+
...(framing.slot ? { slot: framing.slot } : {}),
|
|
307
|
+
...(framing.fit ? { fit: framing.fit } : {}),
|
|
308
|
+
...(framing.position ? { position: framing.position } : {}),
|
|
309
|
+
...(framing.crop ? { crop: framing.crop } : {}),
|
|
310
|
+
},
|
|
311
|
+
issues,
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// ---------------------------------------------------------------------------
|
|
316
|
+
// ImageAsset / manifest
|
|
317
|
+
// ---------------------------------------------------------------------------
|
|
318
|
+
|
|
319
|
+
/** Parse one registry entry (`state/image-assets.json` → `assets[]`). */
|
|
320
|
+
export function parseImageAsset(raw) {
|
|
321
|
+
const issues = [];
|
|
322
|
+
if (!iiIsRecord(raw)) {
|
|
323
|
+
return iiResult(null, [iiInvalid("An image asset must be a JSON object.")]);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const assetId = iiId(raw.assetId, "assetId", issues, {});
|
|
327
|
+
const where = assetId ? { assetId } : {};
|
|
328
|
+
|
|
329
|
+
if (typeof raw.sha256 !== "string" || !II_SHA256.test(raw.sha256)) {
|
|
330
|
+
issues.push(iiInvalid("sha256 must be 64 lowercase hex digits.", where));
|
|
331
|
+
}
|
|
332
|
+
if (typeof raw.mimeType !== "string") {
|
|
333
|
+
issues.push(iiInvalid("mimeType must be a string.", where));
|
|
334
|
+
} else if (!SUPPORTED_IMAGE_MIME_TYPES.includes(raw.mimeType)) {
|
|
335
|
+
issues.push(
|
|
336
|
+
iiUnsupported(
|
|
337
|
+
`mimeType ${raw.mimeType} is not one of ${SUPPORTED_IMAGE_MIME_TYPES.join(", ")}.`,
|
|
338
|
+
where,
|
|
339
|
+
),
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
if (!iiIsPositiveInt(raw.width) || !iiIsPositiveInt(raw.height)) {
|
|
343
|
+
issues.push(iiInvalid("width and height must be positive integers.", where));
|
|
344
|
+
}
|
|
345
|
+
if (!iiIsPositiveInt(raw.byteLength)) {
|
|
346
|
+
issues.push(iiInvalid("byteLength must be a positive integer.", where));
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
let displayName = null;
|
|
350
|
+
if (typeof raw.displayName !== "string" || !raw.displayName.trim()) {
|
|
351
|
+
issues.push(iiInvalid("displayName must be a non-empty string.", where));
|
|
352
|
+
} else {
|
|
353
|
+
displayName = raw.displayName.trim().slice(0, MAX_DISPLAY_NAME_LENGTH);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
if (typeof raw.workspacePath !== "string" || !II_WORKSPACE_PATH.test(raw.workspacePath)) {
|
|
357
|
+
issues.push(
|
|
358
|
+
iiInvalid("workspacePath must be assets/local/<id>.<ext> with no path segments.", where),
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
let provenance = null;
|
|
363
|
+
if (!iiIsRecord(raw.provenance) || !PROVENANCE_KINDS.includes(raw.provenance.kind)) {
|
|
364
|
+
issues.push(
|
|
365
|
+
iiInvalid(`provenance.kind must be one of ${PROVENANCE_KINDS.join(", ")}.`, where),
|
|
366
|
+
);
|
|
367
|
+
} else if (
|
|
368
|
+
raw.provenance.sourceId !== undefined &&
|
|
369
|
+
(typeof raw.provenance.sourceId !== "string" || !raw.provenance.sourceId.trim())
|
|
370
|
+
) {
|
|
371
|
+
issues.push(iiInvalid("provenance.sourceId must be a non-empty string.", where));
|
|
372
|
+
} else {
|
|
373
|
+
provenance = { kind: raw.provenance.kind };
|
|
374
|
+
if (raw.provenance.sourceId !== undefined) {
|
|
375
|
+
provenance.sourceId = raw.provenance.sourceId.trim();
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
if (issues.length) return iiResult(null, issues);
|
|
380
|
+
|
|
381
|
+
return iiResult(
|
|
382
|
+
{
|
|
383
|
+
assetId,
|
|
384
|
+
sha256: raw.sha256,
|
|
385
|
+
mimeType: raw.mimeType,
|
|
386
|
+
width: raw.width,
|
|
387
|
+
height: raw.height,
|
|
388
|
+
byteLength: raw.byteLength,
|
|
389
|
+
displayName,
|
|
390
|
+
workspacePath: raw.workspacePath,
|
|
391
|
+
provenance,
|
|
392
|
+
},
|
|
393
|
+
issues,
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Parse `{version: 1, assets: []}`.
|
|
399
|
+
*
|
|
400
|
+
* An unknown version is refused outright — a v2 manifest read by a v1 reader
|
|
401
|
+
* would lose whatever v2 added, which is exactly the silent downgrade the
|
|
402
|
+
* contract forbids.
|
|
403
|
+
*/
|
|
404
|
+
export function parseImageAssetManifest(raw) {
|
|
405
|
+
if (!iiIsRecord(raw)) {
|
|
406
|
+
return iiResult(null, [iiInvalid("The image asset manifest must be a JSON object.")]);
|
|
407
|
+
}
|
|
408
|
+
if (raw.version !== IMAGE_INTENT_VERSION) {
|
|
409
|
+
return iiResult(null, [
|
|
410
|
+
iiInvalid(
|
|
411
|
+
`Unsupported image asset manifest version ${JSON.stringify(raw.version)}; this client speaks version ${IMAGE_INTENT_VERSION}.`,
|
|
412
|
+
),
|
|
413
|
+
]);
|
|
414
|
+
}
|
|
415
|
+
if (!Array.isArray(raw.assets)) {
|
|
416
|
+
return iiResult(null, [iiInvalid("The image asset manifest needs an assets array.")]);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
const issues = [];
|
|
420
|
+
const assets = [];
|
|
421
|
+
const seen = new Set();
|
|
422
|
+
for (const entry of raw.assets) {
|
|
423
|
+
const parsed = parseImageAsset(entry);
|
|
424
|
+
if (!parsed.value) {
|
|
425
|
+
issues.push(...parsed.issues);
|
|
426
|
+
continue;
|
|
427
|
+
}
|
|
428
|
+
if (seen.has(parsed.value.assetId)) {
|
|
429
|
+
issues.push(
|
|
430
|
+
iiConflict(`Duplicate assetId ${parsed.value.assetId} in the manifest.`, {
|
|
431
|
+
assetId: parsed.value.assetId,
|
|
432
|
+
}),
|
|
433
|
+
);
|
|
434
|
+
continue;
|
|
435
|
+
}
|
|
436
|
+
seen.add(parsed.value.assetId);
|
|
437
|
+
assets.push(parsed.value);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
return iiResult({ version: IMAGE_INTENT_VERSION, assets }, issues);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// ---------------------------------------------------------------------------
|
|
444
|
+
// slide uses
|
|
445
|
+
// ---------------------------------------------------------------------------
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* Validate one slide's `assetUses[]` against the known assets.
|
|
449
|
+
*
|
|
450
|
+
* Beyond per-record validity this enforces the two whole-slide rules: use IDs
|
|
451
|
+
* are unique, and a slide has at most one whole-slide reconstruction blueprint.
|
|
452
|
+
*
|
|
453
|
+
* @param {unknown} rawUses
|
|
454
|
+
* @param {{slide?: number, assetIds?: Iterable<string>, seenUseIds?: Set<string>}} [opts]
|
|
455
|
+
* `assetIds` cross-references the manifest (omit to skip that check);
|
|
456
|
+
* `seenUseIds` carries deck-wide uniqueness across several slides.
|
|
457
|
+
*/
|
|
458
|
+
export function validateSlideUses(rawUses, opts = {}) {
|
|
459
|
+
const { slide, assetIds, seenUseIds } = opts;
|
|
460
|
+
if (rawUses === undefined) return iiResult([], []);
|
|
461
|
+
if (!Array.isArray(rawUses)) {
|
|
462
|
+
return iiResult(null, [iiInvalid("assetUses must be an array.", { slide })]);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
const known = assetIds ? new Set(assetIds) : null;
|
|
466
|
+
const seen = seenUseIds ?? new Set();
|
|
467
|
+
const issues = [];
|
|
468
|
+
const uses = [];
|
|
469
|
+
let blueprint = null;
|
|
470
|
+
|
|
471
|
+
for (const raw of rawUses) {
|
|
472
|
+
const parsed = parseAssetUse(raw, { slide });
|
|
473
|
+
if (!parsed.value) {
|
|
474
|
+
issues.push(...parsed.issues);
|
|
475
|
+
continue;
|
|
476
|
+
}
|
|
477
|
+
const use = parsed.value;
|
|
478
|
+
if (seen.has(use.useId)) {
|
|
479
|
+
issues.push(
|
|
480
|
+
iiConflict(`Duplicate useId ${use.useId}.`, {
|
|
481
|
+
slide,
|
|
482
|
+
useId: use.useId,
|
|
483
|
+
assetId: use.assetId,
|
|
484
|
+
}),
|
|
485
|
+
);
|
|
486
|
+
continue;
|
|
487
|
+
}
|
|
488
|
+
if (known && !known.has(use.assetId)) {
|
|
489
|
+
issues.push(
|
|
490
|
+
iiMissing(`Use ${use.useId} references unregistered asset ${use.assetId}.`, {
|
|
491
|
+
slide,
|
|
492
|
+
useId: use.useId,
|
|
493
|
+
assetId: use.assetId,
|
|
494
|
+
}),
|
|
495
|
+
);
|
|
496
|
+
continue;
|
|
497
|
+
}
|
|
498
|
+
if (use.fidelity === "reconstruct" && use.reconstructionScope === "slide") {
|
|
499
|
+
if (blueprint) {
|
|
500
|
+
issues.push(
|
|
501
|
+
iiConflict(
|
|
502
|
+
`A slide takes at most one whole-slide reconstruction source; ${blueprint} and ${use.useId} both claim it.`,
|
|
503
|
+
{ slide, useId: use.useId, assetId: use.assetId },
|
|
504
|
+
),
|
|
505
|
+
);
|
|
506
|
+
continue;
|
|
507
|
+
}
|
|
508
|
+
blueprint = use.useId;
|
|
509
|
+
}
|
|
510
|
+
seen.add(use.useId);
|
|
511
|
+
uses.push(use);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
return iiResult(uses, issues);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// ---------------------------------------------------------------------------
|
|
518
|
+
// targeted-edit overrides and effective uses
|
|
519
|
+
// ---------------------------------------------------------------------------
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* Parse `state/image-use-overrides.json` —
|
|
523
|
+
* `{version: 1, slides: [{slideId, assetUses}]}`.
|
|
524
|
+
*
|
|
525
|
+
* Each entry is a COMPLETE replacement list for that slide, never a patch, and
|
|
526
|
+
* it is keyed by the durable slide identity (CLI slide GUID / add-in slide id),
|
|
527
|
+
* never by a position that reordering invalidates. An explicit `[]` clears the
|
|
528
|
+
* slide's obligations and is therefore valid.
|
|
529
|
+
*/
|
|
530
|
+
export function parseImageUseOverrides(raw, opts = {}) {
|
|
531
|
+
if (!iiIsRecord(raw)) {
|
|
532
|
+
return iiResult(null, [iiInvalid("The image use overrides file must be a JSON object.")]);
|
|
533
|
+
}
|
|
534
|
+
if (raw.version !== IMAGE_INTENT_VERSION) {
|
|
535
|
+
return iiResult(null, [
|
|
536
|
+
iiInvalid(
|
|
537
|
+
`Unsupported image use overrides version ${JSON.stringify(raw.version)}; this client speaks version ${IMAGE_INTENT_VERSION}.`,
|
|
538
|
+
),
|
|
539
|
+
]);
|
|
540
|
+
}
|
|
541
|
+
if (!Array.isArray(raw.slides)) {
|
|
542
|
+
return iiResult(null, [iiInvalid("The image use overrides file needs a slides array.")]);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
const issues = [];
|
|
546
|
+
const slides = [];
|
|
547
|
+
const seenSlideIds = new Set();
|
|
548
|
+
const seenUseIds = new Set();
|
|
549
|
+
for (const entry of raw.slides) {
|
|
550
|
+
if (!iiIsRecord(entry)) {
|
|
551
|
+
issues.push(iiInvalid("Each override entry must be a JSON object."));
|
|
552
|
+
continue;
|
|
553
|
+
}
|
|
554
|
+
if (typeof entry.slideId !== "string" || !entry.slideId.trim()) {
|
|
555
|
+
issues.push(iiInvalid("Each override entry needs a non-empty slideId."));
|
|
556
|
+
continue;
|
|
557
|
+
}
|
|
558
|
+
const slideId = entry.slideId.trim();
|
|
559
|
+
if (seenSlideIds.has(slideId)) {
|
|
560
|
+
issues.push(iiConflict(`Duplicate override entry for slide ${slideId}.`));
|
|
561
|
+
continue;
|
|
562
|
+
}
|
|
563
|
+
const uses = validateSlideUses(entry.assetUses ?? [], {
|
|
564
|
+
assetIds: opts.assetIds,
|
|
565
|
+
seenUseIds,
|
|
566
|
+
});
|
|
567
|
+
if (!uses.value) {
|
|
568
|
+
issues.push(...uses.issues);
|
|
569
|
+
continue;
|
|
570
|
+
}
|
|
571
|
+
seenSlideIds.add(slideId);
|
|
572
|
+
slides.push({ slideId, assetUses: uses.value });
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
return iiResult({ version: IMAGE_INTENT_VERSION, slides }, issues);
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
/**
|
|
579
|
+
* The uses that actually apply to a slide: its override if one exists (even an
|
|
580
|
+
* empty one — that is a deliberate clearing), otherwise its storyboard uses.
|
|
581
|
+
*
|
|
582
|
+
* @param {{slideId?: string|null, storyboardUses?: Array, overrides?: {slides: Array}|null}} args
|
|
583
|
+
*/
|
|
584
|
+
export function selectImageIntent({ slideId = null, storyboardUses = [], overrides = null }) {
|
|
585
|
+
const entry =
|
|
586
|
+
slideId && overrides?.slides
|
|
587
|
+
? overrides.slides.find((s) => s.slideId === slideId)
|
|
588
|
+
: undefined;
|
|
589
|
+
if (entry) return { uses: entry.assetUses, source: "override" };
|
|
590
|
+
return { uses: storyboardUses ?? [], source: "storyboard" };
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
/**
|
|
594
|
+
* One use's obligation in words — for a conflict message, a plan summary, or
|
|
595
|
+
* the line that tells an agent what it must place on a slide with no mockup.
|
|
596
|
+
*
|
|
597
|
+
* The add-in says the same four things in `RT/image-intent-plan.ts`; keep them
|
|
598
|
+
* worded the same, because a user reads both.
|
|
599
|
+
*/
|
|
600
|
+
export function describeUse(use) {
|
|
601
|
+
if (use.fidelity === "reconstruct") {
|
|
602
|
+
return use.reconstructionScope === "slide" ? "a whole-slide rebuild" : "an element rebuild";
|
|
603
|
+
}
|
|
604
|
+
return use.fidelity === "original" ? "a placed original" : "a visual reference";
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
// ---------------------------------------------------------------------------
|
|
608
|
+
// transport envelope
|
|
609
|
+
// ---------------------------------------------------------------------------
|
|
610
|
+
|
|
611
|
+
/** Parse one `imageIntent.assets[]` descriptor (transport, not the registry). */
|
|
612
|
+
function iiParseTransportAsset(raw) {
|
|
613
|
+
const issues = [];
|
|
614
|
+
if (!iiIsRecord(raw)) {
|
|
615
|
+
return iiResult(null, [iiInvalid("An image intent asset must be a JSON object.")]);
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
const assetId = iiId(raw.assetId, "assetId", issues, {});
|
|
619
|
+
const where = assetId ? { assetId } : {};
|
|
620
|
+
|
|
621
|
+
if (typeof raw.sha256 !== "string" || !II_SHA256.test(raw.sha256)) {
|
|
622
|
+
issues.push(iiInvalid("sha256 must be 64 lowercase hex digits.", where));
|
|
623
|
+
}
|
|
624
|
+
if (typeof raw.mimeType !== "string") {
|
|
625
|
+
issues.push(iiInvalid("mimeType must be a string.", where));
|
|
626
|
+
} else if (!SUPPORTED_IMAGE_MIME_TYPES.includes(raw.mimeType)) {
|
|
627
|
+
issues.push(
|
|
628
|
+
iiUnsupported(
|
|
629
|
+
`mimeType ${raw.mimeType} is not one of ${SUPPORTED_IMAGE_MIME_TYPES.join(", ")}.`,
|
|
630
|
+
where,
|
|
631
|
+
),
|
|
632
|
+
);
|
|
633
|
+
}
|
|
634
|
+
if (!iiIsPositiveInt(raw.width) || !iiIsPositiveInt(raw.height)) {
|
|
635
|
+
issues.push(iiInvalid("width and height must be positive integers.", where));
|
|
636
|
+
}
|
|
637
|
+
if (!iiIsPositiveInt(raw.byteLength)) {
|
|
638
|
+
issues.push(iiInvalid("byteLength must be a positive integer.", where));
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
let displayName = null;
|
|
642
|
+
if (typeof raw.displayName !== "string" || !raw.displayName.trim()) {
|
|
643
|
+
issues.push(iiInvalid("displayName must be a non-empty string.", where));
|
|
644
|
+
} else {
|
|
645
|
+
displayName = raw.displayName.trim().slice(0, MAX_DISPLAY_NAME_LENGTH);
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// The concept/spec routes carry the bytes inline; render/translate resolve
|
|
649
|
+
// them from the deck's existing asset byte map. Carrying both leaves the
|
|
650
|
+
// receiver to guess which one is authoritative, so it is refused.
|
|
651
|
+
const hasImage = raw.image !== undefined;
|
|
652
|
+
const hasPath = raw.workspacePath !== undefined;
|
|
653
|
+
if (hasImage && hasPath) {
|
|
654
|
+
issues.push(
|
|
655
|
+
iiInvalid("An asset descriptor carries either image or workspacePath, not both.", where),
|
|
656
|
+
);
|
|
657
|
+
}
|
|
658
|
+
if (hasImage) {
|
|
659
|
+
const m = typeof raw.image === "string" ? II_DATA_URI.exec(raw.image) : null;
|
|
660
|
+
if (!m) {
|
|
661
|
+
issues.push(iiInvalid("image must be a data:<mime>;base64,<data> URI.", where));
|
|
662
|
+
} else if (m[1] !== raw.mimeType) {
|
|
663
|
+
issues.push(
|
|
664
|
+
iiInvalid(`image data URI declares ${m[1]} but mimeType is ${raw.mimeType}.`, where),
|
|
665
|
+
);
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
if (hasPath && (typeof raw.workspacePath !== "string" || !II_WORKSPACE_PATH.test(raw.workspacePath))) {
|
|
669
|
+
issues.push(
|
|
670
|
+
iiInvalid("workspacePath must be assets/local/<id>.<ext> with no path segments.", where),
|
|
671
|
+
);
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
if (issues.length) return iiResult(null, issues);
|
|
675
|
+
|
|
676
|
+
return iiResult(
|
|
677
|
+
{
|
|
678
|
+
assetId,
|
|
679
|
+
sha256: raw.sha256,
|
|
680
|
+
mimeType: raw.mimeType,
|
|
681
|
+
width: raw.width,
|
|
682
|
+
height: raw.height,
|
|
683
|
+
byteLength: raw.byteLength,
|
|
684
|
+
displayName,
|
|
685
|
+
...(hasImage ? { image: raw.image } : {}),
|
|
686
|
+
...(hasPath ? { workspacePath: raw.workspacePath } : {}),
|
|
687
|
+
},
|
|
688
|
+
issues,
|
|
689
|
+
);
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
/**
|
|
693
|
+
* Parse the `imageIntent` request envelope — the snapshot of effective uses a
|
|
694
|
+
* concept/spec/render call carries. Both the sync and the job handler on the
|
|
695
|
+
* add-in side, and every CLI transport builder, go through this one function.
|
|
696
|
+
*/
|
|
697
|
+
export function parseImageIntentEnvelope(raw) {
|
|
698
|
+
if (raw === undefined || raw === null) return iiResult(null, []);
|
|
699
|
+
if (!iiIsRecord(raw)) {
|
|
700
|
+
return iiResult(null, [iiInvalid("imageIntent must be a JSON object.")]);
|
|
701
|
+
}
|
|
702
|
+
if (raw.version !== IMAGE_INTENT_VERSION) {
|
|
703
|
+
return iiResult(null, [
|
|
704
|
+
iiInvalid(
|
|
705
|
+
`Unsupported imageIntent version ${JSON.stringify(raw.version)}; this client speaks version ${IMAGE_INTENT_VERSION}.`,
|
|
706
|
+
),
|
|
707
|
+
]);
|
|
708
|
+
}
|
|
709
|
+
if (!Array.isArray(raw.assets) || !Array.isArray(raw.slides)) {
|
|
710
|
+
return iiResult(null, [iiInvalid("imageIntent needs an assets array and a slides array.")]);
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
const issues = [];
|
|
714
|
+
const assets = [];
|
|
715
|
+
const known = new Set();
|
|
716
|
+
for (const entry of raw.assets) {
|
|
717
|
+
const parsed = iiParseTransportAsset(entry);
|
|
718
|
+
if (!parsed.value) {
|
|
719
|
+
issues.push(...parsed.issues);
|
|
720
|
+
continue;
|
|
721
|
+
}
|
|
722
|
+
if (known.has(parsed.value.assetId)) {
|
|
723
|
+
issues.push(
|
|
724
|
+
iiConflict(`Duplicate assetId ${parsed.value.assetId} in imageIntent.assets.`, {
|
|
725
|
+
assetId: parsed.value.assetId,
|
|
726
|
+
}),
|
|
727
|
+
);
|
|
728
|
+
continue;
|
|
729
|
+
}
|
|
730
|
+
known.add(parsed.value.assetId);
|
|
731
|
+
assets.push(parsed.value);
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
const slides = [];
|
|
735
|
+
const seenSlides = new Set();
|
|
736
|
+
const seenUseIds = new Set();
|
|
737
|
+
for (const entry of raw.slides) {
|
|
738
|
+
if (!iiIsRecord(entry)) {
|
|
739
|
+
issues.push(iiInvalid("Each imageIntent.slides entry must be a JSON object."));
|
|
740
|
+
continue;
|
|
741
|
+
}
|
|
742
|
+
if (!iiIsPositiveInt(entry.slide)) {
|
|
743
|
+
issues.push(iiInvalid("Each imageIntent.slides entry needs a positive integer slide."));
|
|
744
|
+
continue;
|
|
745
|
+
}
|
|
746
|
+
const slide = entry.slide;
|
|
747
|
+
if (seenSlides.has(slide)) {
|
|
748
|
+
issues.push(iiConflict(`Duplicate imageIntent entry for slide ${slide}.`, { slide }));
|
|
749
|
+
continue;
|
|
750
|
+
}
|
|
751
|
+
const uses = validateSlideUses(entry.uses, {
|
|
752
|
+
slide,
|
|
753
|
+
assetIds: known,
|
|
754
|
+
seenUseIds,
|
|
755
|
+
});
|
|
756
|
+
if (!uses.value) {
|
|
757
|
+
issues.push(...uses.issues);
|
|
758
|
+
continue;
|
|
759
|
+
}
|
|
760
|
+
seenSlides.add(slide);
|
|
761
|
+
slides.push({ slide, uses: uses.value });
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
return iiResult({ version: IMAGE_INTENT_VERSION, assets, slides }, issues);
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
// ---------------------------------------------------------------------------
|
|
768
|
+
// fingerprint input (design decision 6)
|
|
769
|
+
// ---------------------------------------------------------------------------
|
|
770
|
+
|
|
771
|
+
// What an asset contributes to a derived output's identity: its bytes and what
|
|
772
|
+
// they intrinsically are. NOT displayName, NOT workspacePath, NOT provenance —
|
|
773
|
+
// renaming a file or moving a workspace must not invalidate a concept, while
|
|
774
|
+
// swapping the bytes behind an unchanged filename must.
|
|
775
|
+
function iiAssetFingerprint(asset) {
|
|
776
|
+
return {
|
|
777
|
+
assetId: asset.assetId,
|
|
778
|
+
sha256: asset.sha256,
|
|
779
|
+
mimeType: asset.mimeType,
|
|
780
|
+
width: asset.width,
|
|
781
|
+
height: asset.height,
|
|
782
|
+
};
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
/**
|
|
786
|
+
* The canonical object a concept/spec/evidence fingerprint is computed over.
|
|
787
|
+
*
|
|
788
|
+
* Pure and deterministic: uses sorted by `useId`, asset descriptors sorted by
|
|
789
|
+
* `assetId`, array order otherwise preserved. `context` is whatever the calling
|
|
790
|
+
* cache already keys on (slide storyboard, layout/bundle inputs, spec schema
|
|
791
|
+
* version) and is hashed alongside — never timestamps, never temporary paths.
|
|
792
|
+
*
|
|
793
|
+
* Hashing this is `imageIntentFingerprint()` in `image-intent-fingerprint.mjs`.
|
|
794
|
+
*/
|
|
795
|
+
export function imageIntentFingerprintInput({ uses = [], assets = [], context } = {}) {
|
|
796
|
+
return {
|
|
797
|
+
version: IMAGE_INTENT_VERSION,
|
|
798
|
+
assets: [...assets].map(iiAssetFingerprint).sort((a, b) => (a.assetId < b.assetId ? -1 : a.assetId > b.assetId ? 1 : 0)),
|
|
799
|
+
uses: [...uses].sort((a, b) => (a.useId < b.useId ? -1 : a.useId > b.useId ? 1 : 0)),
|
|
800
|
+
...(context === undefined ? {} : { context }),
|
|
801
|
+
};
|
|
802
|
+
}
|