@deepblend/dsh-blender-contracts 0.1.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/lib/asset-content.js +134 -0
- package/lib/bitmap-font.js +149 -0
- package/lib/canonical.js +98 -0
- package/lib/config-surface.js +88 -0
- package/lib/contact-sheet.js +233 -0
- package/lib/deployment-paths.js +153 -0
- package/lib/index.js +829 -0
- package/lib/json-schema.js +337 -0
- package/lib/png.js +452 -0
- package/lib/projections.js +209 -0
- package/lib/redact.js +53 -0
- package/lib/render-job.js +459 -0
- package/lib/scene-patch.js +947 -0
- package/lib/scene-spec.js +901 -0
- package/lib/schemas/job-result.schema.json +295 -0
- package/lib/schemas/scene-patch.schema.json +1307 -0
- package/lib/schemas/scene-spec.schema.json +791 -0
- package/lib/ui-api.js +695 -0
- package/lib/visual-composition.js +574 -0
- package/lib/visual-issue.js +738 -0
- package/lib/visual-loop.js +401 -0
- package/package.json +19 -0
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The visual iteration controller (SPEC §12.1 PREVIEW → VISUAL_REVIEW → PATCH loop).
|
|
3
|
+
*
|
|
4
|
+
* WHAT THIS FILE IS ALLOWED TO KNOW
|
|
5
|
+
* ---------------------------------
|
|
6
|
+
* It knows nothing about Blender, files, Cordis or HTTP. Everything it does happens
|
|
7
|
+
* through four injected ports:
|
|
8
|
+
*
|
|
9
|
+
* review() measure a revision and produce a scored VisualReview
|
|
10
|
+
* patch() commit a ScenePatch and return the new revision
|
|
11
|
+
* restore() move the current pointer back
|
|
12
|
+
* reviewer() ask the vision model what it sees, and what it would change
|
|
13
|
+
*
|
|
14
|
+
* That boundary is not decoration. It is what lets the loop's SEMANTICS — the
|
|
15
|
+
* iteration cap, the repeated-issue stop, "only adopt a change that improves the
|
|
16
|
+
* score", and the handover package — be tested deterministically against a stub,
|
|
17
|
+
* while "the model really sees the image" is tested once, live. Mixing those two
|
|
18
|
+
* claims into one untestable blob is how a project ends up asserting only one of
|
|
19
|
+
* them (decision D34).
|
|
20
|
+
*
|
|
21
|
+
* WHY A REJECTED FIX IS ROLLED BACK RATHER THAN LEFT IN PLACE
|
|
22
|
+
* ----------------------------------------------------------
|
|
23
|
+
* The revision history is append-only (M1), so the rejected revision stays on disk
|
|
24
|
+
* and stays in the history — that is the audit trail. But the CURRENT POINTER is a
|
|
25
|
+
* statement about which revision is the deliverable, and moving it to a revision
|
|
26
|
+
* that measured worse would make the project worse while claiming progress. So: keep
|
|
27
|
+
* the history, decline the pointer. The rollback is itself recorded, in the round
|
|
28
|
+
* record and in the run document.
|
|
29
|
+
*
|
|
30
|
+
* WHICH PACKAGE THIS LIVES IN
|
|
31
|
+
* --------------------------
|
|
32
|
+
* `contracts`, beside the scorer and the sheet compositor, and not in the host
|
|
33
|
+
* package that calls it. "Contracts" here means the project's own vocabulary and its
|
|
34
|
+
* pure rules — `visual-issue.js` already holds the scoring rules by that reading — and
|
|
35
|
+
* the loop controller is the rest of them: what may be adopted, when to stop, what a
|
|
36
|
+
* handover contains. It imports nothing outside this package, which is what lets the
|
|
37
|
+
* acceptance suite exercise it without a Blender, a Cordis context or a model.
|
|
38
|
+
*
|
|
39
|
+
* Owner: DeepBlend Studio — M2
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
import {
|
|
43
|
+
VISUAL_PASS_SCORE,
|
|
44
|
+
seedFingerprintCounters,
|
|
45
|
+
shouldHandOver,
|
|
46
|
+
updateFingerprintCounters,
|
|
47
|
+
validateFindings,
|
|
48
|
+
} from './visual-issue.js'
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* @typedef {object} VisualReviewRound
|
|
52
|
+
* @property {number} round - 1-based.
|
|
53
|
+
* @property {string} revision - the revision reviewed in this round.
|
|
54
|
+
* @property {number} score
|
|
55
|
+
* @property {number} issueCount
|
|
56
|
+
* @property {string[]} issueCodes
|
|
57
|
+
* @property {object[]} reported - findings the vision model authored and that passed validation.
|
|
58
|
+
* @property {object[]} rejectedFindings - findings the model authored that did not.
|
|
59
|
+
* @property {string} outcome - one of `baseline`, `applied`, `rejected`, `stopped`, `handover`.
|
|
60
|
+
* @property {string|null} reason
|
|
61
|
+
* @property {string|null} newRevision
|
|
62
|
+
* @property {object|null} appliedPatch
|
|
63
|
+
* @property {string} [rolledBackTo] - set on a round whose revision was committed and
|
|
64
|
+
* then declined: the revision stays in the history, the current pointer went back.
|
|
65
|
+
*/
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Run the visual loop.
|
|
69
|
+
*
|
|
70
|
+
* @param {object} input
|
|
71
|
+
* @param {string} input.projectId
|
|
72
|
+
* @param {string} input.revision - the starting (current) revision.
|
|
73
|
+
* @param {(request: object) => Promise<object>} input.review
|
|
74
|
+
* `({ projectId, revision, iteration, signal }) => VisualReview` — measures and scores.
|
|
75
|
+
* @param {(request: object) => Promise<object>} input.patch
|
|
76
|
+
* `({ projectId, baseRevision, operations, note, actor, stage, idempotencyKey, saveCheckpoint, signal }) => RevisionSummary`
|
|
77
|
+
* @param {(request: object) => Promise<object>} input.restore
|
|
78
|
+
* `({ projectId, revision, reason }) => unknown`
|
|
79
|
+
* @param {(request: object) => Promise<{ findings: object[], operations: object[], note: string|null, detail: object|null }>} input.reviewer
|
|
80
|
+
* The vision port. Receives `{ review, round, previousRounds, signal }`.
|
|
81
|
+
* @param {(line: string) => void} [input.log]
|
|
82
|
+
* @param {number} [input.maxIterations]
|
|
83
|
+
* @param {number} [input.stopOnRepeatedIssueCount]
|
|
84
|
+
* @param {number} [input.minConfidenceForAutoFix]
|
|
85
|
+
* @param {AbortSignal} [input.signal]
|
|
86
|
+
* @returns {Promise<object>} the run document: rounds, final score, open issues and
|
|
87
|
+
* a handover package whenever the loop stopped short of passing.
|
|
88
|
+
*/
|
|
89
|
+
export async function runVisualLoop(input) {
|
|
90
|
+
const maxIterations = positiveInteger(input.maxIterations, 5)
|
|
91
|
+
const stopOnRepeatedIssueCount = positiveInteger(input.stopOnRepeatedIssueCount, 2)
|
|
92
|
+
const minConfidence = numberInRange(input.minConfidenceForAutoFix, 0.8)
|
|
93
|
+
const log = typeof input.log === 'function' ? input.log : () => {}
|
|
94
|
+
|
|
95
|
+
/** @type {VisualReviewRound[]} */
|
|
96
|
+
const rounds = []
|
|
97
|
+
/** @type {Map<string, number>} */
|
|
98
|
+
const fingerprints = new Map()
|
|
99
|
+
|
|
100
|
+
let currentRevision = input.revision
|
|
101
|
+
let iteration = 0
|
|
102
|
+
let handedOver = null
|
|
103
|
+
|
|
104
|
+
/** The latest measured state. Replaced (never mutated) when a round is adopted. */
|
|
105
|
+
let currentReview = await input.review({
|
|
106
|
+
projectId: input.projectId,
|
|
107
|
+
revision: currentRevision,
|
|
108
|
+
iteration,
|
|
109
|
+
signal: input.signal,
|
|
110
|
+
})
|
|
111
|
+
let score = currentReview.score
|
|
112
|
+
// The baseline is an OBSERVATION, not an attempt, so it seeds the counters at zero
|
|
113
|
+
// rather than one. Counting it would make "the same issue recurred twice" true
|
|
114
|
+
// after a single failed fix — and the loop would then stop before trying the second
|
|
115
|
+
// thing it thought of, which is the opposite of what SPEC §12.3 asks for. The rule
|
|
116
|
+
// is about a problem that survives REPEATED FIX ATTEMPTS.
|
|
117
|
+
seedFingerprintCounters(fingerprints, currentReview.issues)
|
|
118
|
+
rounds.push({
|
|
119
|
+
round: 0,
|
|
120
|
+
revision: currentRevision,
|
|
121
|
+
score,
|
|
122
|
+
issueCount: currentReview.issues.length,
|
|
123
|
+
issueCodes: currentReview.issues.map(issue => issue.code),
|
|
124
|
+
reported: [],
|
|
125
|
+
rejectedFindings: [],
|
|
126
|
+
outcome: 'baseline',
|
|
127
|
+
reason: null,
|
|
128
|
+
newRevision: null,
|
|
129
|
+
appliedPatch: null,
|
|
130
|
+
})
|
|
131
|
+
log(`baseline ${currentRevision}: score ${score}, ${currentReview.issues.length} measured issue(s)`)
|
|
132
|
+
|
|
133
|
+
while (true) {
|
|
134
|
+
const decision = shouldHandOver({
|
|
135
|
+
score,
|
|
136
|
+
iteration,
|
|
137
|
+
maxIterations,
|
|
138
|
+
fingerprints,
|
|
139
|
+
stopOnRepeatedIssueCount,
|
|
140
|
+
})
|
|
141
|
+
if (decision.stop) {
|
|
142
|
+
handedOver = {
|
|
143
|
+
reason: decision.reason,
|
|
144
|
+
fingerprint: decision.fingerprint ?? null,
|
|
145
|
+
iteration,
|
|
146
|
+
score,
|
|
147
|
+
revision: currentRevision,
|
|
148
|
+
}
|
|
149
|
+
log(`stopping after ${iteration} iteration(s): ${decision.reason}`)
|
|
150
|
+
break
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
iteration += 1
|
|
154
|
+
const vision = await input.reviewer({
|
|
155
|
+
review: currentReview,
|
|
156
|
+
round: iteration,
|
|
157
|
+
previousRounds: rounds.filter(round => round.outcome === 'applied' || round.outcome === 'rejected'),
|
|
158
|
+
signal: input.signal,
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
// The model's findings are validated against the review it was shown, and the
|
|
162
|
+
// ones that survive are kept as the round's `reported` list. They are NOT added
|
|
163
|
+
// to the review's `issues`: the score must stay a function of measurements
|
|
164
|
+
// (D30), so a finding only the model can see is evidence for a human, never for
|
|
165
|
+
// the scorer.
|
|
166
|
+
const context = {
|
|
167
|
+
viewIds: new Set(currentReview.perView.map(entry => entry.viewId)),
|
|
168
|
+
objectIds: new Set(objectIdsOf(currentReview)),
|
|
169
|
+
}
|
|
170
|
+
const validated = validateFindings(vision.findings, context)
|
|
171
|
+
|
|
172
|
+
const candidates = (vision.operations ?? []).filter(operation => {
|
|
173
|
+
const confidence = typeof operation?.confidence === 'number' ? operation.confidence : 1
|
|
174
|
+
return confidence >= minConfidence
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
/** @type {VisualReviewRound} */
|
|
178
|
+
const round = {
|
|
179
|
+
round: iteration,
|
|
180
|
+
revision: currentRevision,
|
|
181
|
+
score,
|
|
182
|
+
issueCount: currentReview.issues.length,
|
|
183
|
+
issueCodes: currentReview.issues.map(issue => issue.code),
|
|
184
|
+
reported: validated.accepted,
|
|
185
|
+
rejectedFindings: validated.rejected,
|
|
186
|
+
outcome: 'stopped',
|
|
187
|
+
reason: null,
|
|
188
|
+
newRevision: null,
|
|
189
|
+
appliedPatch: null,
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (candidates.length === 0) {
|
|
193
|
+
round.reason = (vision.operations ?? []).length > 0 ? 'NO_CONFIDENT_FIX' : 'NO_FIX_PROPOSED'
|
|
194
|
+
rounds.push(round)
|
|
195
|
+
handedOver = { reason: round.reason, fingerprint: null, iteration, score, revision: currentRevision }
|
|
196
|
+
log(`round ${iteration}: the reviewer proposed no usable change (${round.reason})`)
|
|
197
|
+
break
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const operations = candidates.flatMap(operation => normaliseOperation(operation))
|
|
201
|
+
if (operations.length === 0) {
|
|
202
|
+
round.reason = 'NO_FIX_PROPOSED'
|
|
203
|
+
rounds.push(round)
|
|
204
|
+
handedOver = { reason: round.reason, fingerprint: null, iteration, score, revision: currentRevision }
|
|
205
|
+
break
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
let summary
|
|
209
|
+
try {
|
|
210
|
+
summary = await input.patch({
|
|
211
|
+
projectId: input.projectId,
|
|
212
|
+
baseRevision: currentRevision,
|
|
213
|
+
operations,
|
|
214
|
+
note: vision.note ?? `visual review round ${iteration}`,
|
|
215
|
+
actor: 'deepblend.visual-loop',
|
|
216
|
+
stage: 'VISUAL_REVIEW',
|
|
217
|
+
// A deterministic key: the same operations against the same base revision
|
|
218
|
+
// are the same intent, so a retry after a crash replays instead of
|
|
219
|
+
// committing twice (M1's idempotency semantics, for free).
|
|
220
|
+
idempotencyKey: `visual-${input.projectId}-${currentRevision}-${iteration}`,
|
|
221
|
+
saveCheckpoint: true,
|
|
222
|
+
signal: input.signal,
|
|
223
|
+
})
|
|
224
|
+
} catch (cause) {
|
|
225
|
+
// A refused patch is not a crash: it is the reviewer having proposed
|
|
226
|
+
// something the scene will not accept, which is information. Record it and
|
|
227
|
+
// keep going — the next round sees the failure in `previousRounds`.
|
|
228
|
+
round.outcome = 'rejected'
|
|
229
|
+
round.reason = `PATCH_REFUSED: ${cause instanceof Error ? cause.message : String(cause)}`
|
|
230
|
+
rounds.push(round)
|
|
231
|
+
// ...but the measured state is unchanged, so the same findings are present
|
|
232
|
+
// again and this round has to count towards the repeated-issue rule. Without
|
|
233
|
+
// this, a scene that refuses every proposal would run to the iteration cap
|
|
234
|
+
// instead of recognising what is happening after the second refusal.
|
|
235
|
+
updateFingerprintCounters(fingerprints, currentReview.issues)
|
|
236
|
+
continue
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
round.appliedPatch = { operations, note: vision.note ?? null }
|
|
240
|
+
round.newRevision = summary.revision
|
|
241
|
+
|
|
242
|
+
const after = await input.review({
|
|
243
|
+
projectId: input.projectId,
|
|
244
|
+
revision: summary.revision,
|
|
245
|
+
iteration,
|
|
246
|
+
signal: input.signal,
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
if (after.score > score) {
|
|
250
|
+
const previousScore = score
|
|
251
|
+
score = after.score
|
|
252
|
+
currentRevision = summary.revision
|
|
253
|
+
currentReview = after
|
|
254
|
+
round.outcome = 'applied'
|
|
255
|
+
round.reason = `score ${previousScore} -> ${score}`
|
|
256
|
+
rounds.push(round)
|
|
257
|
+
updateFingerprintCounters(fingerprints, after.issues)
|
|
258
|
+
log(`round ${iteration}: applied ${operations.length} operation(s), score ${previousScore} -> ${score} on ${currentRevision}`)
|
|
259
|
+
continue
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Not an improvement. Keep the history, decline the pointer.
|
|
263
|
+
await input.restore({
|
|
264
|
+
projectId: input.projectId,
|
|
265
|
+
revision: round.revision,
|
|
266
|
+
reason: `visual round ${iteration} measured ${after.score}, not better than ${score}`,
|
|
267
|
+
})
|
|
268
|
+
round.outcome = 'rejected'
|
|
269
|
+
round.reason = `score did not improve (${score} -> ${after.score})`
|
|
270
|
+
round.rolledBackTo = round.revision
|
|
271
|
+
rounds.push(round)
|
|
272
|
+
// The measured state is unchanged (the pointer went back), so the same
|
|
273
|
+
// fingerprints are present again — which is exactly what makes a fix that
|
|
274
|
+
// cannot work stop the loop instead of repeating until the cap.
|
|
275
|
+
updateFingerprintCounters(fingerprints, currentReview.issues)
|
|
276
|
+
log(`round ${iteration}: rejected — score did not improve (${score} -> ${after.score}); pointer restored to ${currentRevision}`)
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// A handover is something a human has to pick up, so a run that PASSED has none.
|
|
280
|
+
// Reporting one would tell a reader to take over work that is finished.
|
|
281
|
+
const handover = handedOver === null || handedOver.reason === 'PASSING_SCORE'
|
|
282
|
+
? null
|
|
283
|
+
: buildHandover({ handedOver, openIssues: currentReview.issues, rounds, review: currentReview })
|
|
284
|
+
|
|
285
|
+
return {
|
|
286
|
+
projectId: input.projectId,
|
|
287
|
+
startRevision: input.revision,
|
|
288
|
+
finalRevision: currentRevision,
|
|
289
|
+
startScore: rounds[0].score,
|
|
290
|
+
finalScore: score,
|
|
291
|
+
passed: score >= VISUAL_PASS_SCORE,
|
|
292
|
+
iterations: iteration,
|
|
293
|
+
maxIterations,
|
|
294
|
+
stopReason: handedOver?.reason ?? 'PASSING_SCORE',
|
|
295
|
+
rounds,
|
|
296
|
+
openIssues: currentReview.issues,
|
|
297
|
+
sheet: currentReview.sheet ?? null,
|
|
298
|
+
handover,
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Every object id named anywhere in a review, measured or reported.
|
|
304
|
+
*
|
|
305
|
+
* The set is the validation context for the model's findings: an object the review
|
|
306
|
+
* never measured is still legitimate to talk about IF the review mentions it (a
|
|
307
|
+
* finding may name a second object as the thing in the way), so the context is
|
|
308
|
+
* everything the review knows rather than only the subject.
|
|
309
|
+
*
|
|
310
|
+
* @param {object} review
|
|
311
|
+
* @returns {string[]}
|
|
312
|
+
*/
|
|
313
|
+
function objectIdsOf(review) {
|
|
314
|
+
const ids = new Set()
|
|
315
|
+
for (const issue of review.issues ?? []) {
|
|
316
|
+
if (issue.objectId) ids.add(issue.objectId)
|
|
317
|
+
}
|
|
318
|
+
return [...ids]
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* The package a human needs to take over (SPEC §20 M2 "失败可人工接管").
|
|
323
|
+
*
|
|
324
|
+
* Three things, and each one answers a different question a person asks when they
|
|
325
|
+
* open the project: WHERE is the work (a revision that is not polluted), WHAT is
|
|
326
|
+
* still wrong (the measured issues, with the numbers), and WHAT TO DO NEXT
|
|
327
|
+
* (concrete operations and views, not "adjust the lighting").
|
|
328
|
+
*
|
|
329
|
+
* @param {object} input
|
|
330
|
+
* @returns {object}
|
|
331
|
+
*/
|
|
332
|
+
function buildHandover(input) {
|
|
333
|
+
const { handedOver, openIssues, rounds, review } = input
|
|
334
|
+
const investigatedViews = [...new Set(openIssues.map(issue => issue.viewId).filter(Boolean))]
|
|
335
|
+
const attempted = rounds.filter(round => round.appliedPatch !== null)
|
|
336
|
+
|
|
337
|
+
const suggestions = []
|
|
338
|
+
if (openIssues.length > 0) {
|
|
339
|
+
suggestions.push(
|
|
340
|
+
`Look at the ${openIssues.length} open issue(s) below with ` +
|
|
341
|
+
`${review.sheet?.path ?? 'the contact sheet'} open; the measurements name the view and the object.`,
|
|
342
|
+
)
|
|
343
|
+
}
|
|
344
|
+
for (const viewId of investigatedViews) suggestions.push(`Re-render view "${viewId}" alone at full resolution before editing.`)
|
|
345
|
+
if (attempted.length > 0) {
|
|
346
|
+
suggestions.push(
|
|
347
|
+
`The automated rounds tried ${attempted.length} change(s) and none improved the measured score; ` +
|
|
348
|
+
'the review rounds below record exactly what each one was.',
|
|
349
|
+
)
|
|
350
|
+
}
|
|
351
|
+
if (handedOver.reason === 'REPEATED_ISSUE') {
|
|
352
|
+
suggestions.push(
|
|
353
|
+
`The same finding recurred without improving (${handedOver.fingerprint ?? 'see rounds'}), ` +
|
|
354
|
+
'so the loop stopped rather than spending another render on it.',
|
|
355
|
+
)
|
|
356
|
+
}
|
|
357
|
+
suggestions.push(
|
|
358
|
+
'Edit the scene with blender_scene_patch, or open the checkpoint in Blender and re-sync the scene summary afterwards.',
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
return {
|
|
362
|
+
reason: handedOver.reason,
|
|
363
|
+
iteration: handedOver.iteration,
|
|
364
|
+
score: handedOver.score,
|
|
365
|
+
/** The revision to work from: whatever the loop last accepted, current pointer included. */
|
|
366
|
+
revision: handedOver.revision,
|
|
367
|
+
/** Where the failing revision still lives, so a human can inspect what was tried. */
|
|
368
|
+
attemptedRevisions: attempted.map(round => round.newRevision).filter(revision => revision !== null),
|
|
369
|
+
openIssues,
|
|
370
|
+
views: investigatedViews,
|
|
371
|
+
suggestions,
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Coerce one proposed operation into a ScenePatch operation.
|
|
377
|
+
*
|
|
378
|
+
* The reviewer may hand back either a bare operation or one wrapped with a
|
|
379
|
+
* confidence score, because the prompt asks for the latter and models are
|
|
380
|
+
* inconsistent about honouring a shape. Accepting both is cheaper than a retry, and
|
|
381
|
+
* the wrapper is dropped here so nothing downstream has to know about it.
|
|
382
|
+
*
|
|
383
|
+
* @param {object} proposed
|
|
384
|
+
* @returns {object[]}
|
|
385
|
+
*/
|
|
386
|
+
function normaliseOperation(proposed) {
|
|
387
|
+
if (proposed === null || typeof proposed !== 'object') return []
|
|
388
|
+
const operation = 'op' in proposed ? proposed : proposed.operation
|
|
389
|
+
if (operation === null || typeof operation !== 'object' || typeof operation.op !== 'string') return []
|
|
390
|
+
return [operation]
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/** A positive integer option, with a default. */
|
|
394
|
+
function positiveInteger(value, fallback) {
|
|
395
|
+
return Number.isInteger(value) && value > 0 ? value : fallback
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/** A number in [0, 1], with a default. */
|
|
399
|
+
function numberInRange(value, fallback) {
|
|
400
|
+
return typeof value === 'number' && value >= 0 && value <= 1 ? value : fallback
|
|
401
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@deepblend/dsh-blender-contracts",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "DeepBlend Studio Service Definition, protocol contracts, and stable error codes.",
|
|
6
|
+
"main": "lib/index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"default": "./lib/index.js"
|
|
10
|
+
},
|
|
11
|
+
"./package.json": "./package.json"
|
|
12
|
+
},
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "git+https://github.com/pearjelly/deep-blend.git"
|
|
17
|
+
},
|
|
18
|
+
"dependencies": {}
|
|
19
|
+
}
|