@voce-engine/core 0.1.0-rc.3 → 0.1.0-rc.4

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/README.md CHANGED
@@ -2,8 +2,29 @@
2
2
 
3
3
  Deterministic ScenarioPack resolution, constraint compilation, reference planning, Prompt IR, offline execution, and evaluation primitives.
4
4
 
5
- > `0.1.0-rc.3` is a release candidate, not production-ready. APIs may change before `0.1.0`.
5
+ > `0.1.0-rc.4` is a release candidate, not production-ready. APIs may change before `0.1.0`.
6
6
 
7
7
  ```bash
8
- npm install @voce-engine/core@0.1.0-rc.3
8
+ npm install @voce-engine/core@0.1.0-rc.4
9
9
  ```
10
+
11
+ ## Visual composition presets
12
+
13
+ ```ts
14
+ import { compileConstraints, expandVisualCompositionPreset } from '@voce-engine/core'
15
+
16
+ const compositionChanges = expandVisualCompositionPreset('dutch-angle', {
17
+ inputs: { direction: 'right' },
18
+ sourceHintIds: ['ui-card:dutch-angle'],
19
+ })
20
+
21
+ const result = compileConstraints({
22
+ ...existingCompilationInput,
23
+ changeIntents: [
24
+ ...existingCompilationInput.changeIntents,
25
+ ...compositionChanges,
26
+ ],
27
+ })
28
+ ```
29
+
30
+ Hosts should display the example artwork but submit only the stable preset ID and typed inputs. The artwork is not a reference asset and the selector does not consume reference budget. See the [complete preset and integration guide](../../docs/visual-composition.md) or its [简体中文 version](../../docs/zh-CN/visual-composition.md).
@@ -0,0 +1,36 @@
1
+ import type { ChangeIntent, JsonValue, OntologyPathDefinition, Provenance } from '@voce-engine/contracts';
2
+ export declare const VISUAL_COMPOSITION_SCHEMA_VERSION: "voce.visual-composition/v1alpha1";
3
+ export declare const VISUAL_COMPOSITION_CATALOG_ID = "visual-composition.v1";
4
+ export declare const VISUAL_COMPOSITION_FIXED_TIME = "2026-01-01T00:00:00.000Z";
5
+ export interface CompositionChangeTemplate {
6
+ operation: ChangeIntent['operation'];
7
+ targetPath: string;
8
+ requestedValue?: JsonValue;
9
+ valueFrom?: 'direction' | 'surface' | 'silhouette';
10
+ }
11
+ export interface VisualCompositionPreset {
12
+ id: string;
13
+ labelKey: string;
14
+ descriptionKey: string;
15
+ category: 'framing' | 'view' | 'layout' | 'depth' | 'lens';
16
+ compatibilityHints?: string[];
17
+ changes: CompositionChangeTemplate[];
18
+ requiredInputs?: string[];
19
+ }
20
+ export interface VisualCompositionCatalog {
21
+ schemaVersion: typeof VISUAL_COMPOSITION_SCHEMA_VERSION;
22
+ id: string;
23
+ paths: OntologyPathDefinition[];
24
+ presets: VisualCompositionPreset[];
25
+ catalogHash: string;
26
+ }
27
+ export declare const VISUAL_COMPOSITION_PATHS: OntologyPathDefinition[];
28
+ export declare const VISUAL_COMPOSITION_PRESETS: VisualCompositionPreset[];
29
+ export declare const VISUAL_COMPOSITION_CATALOG: VisualCompositionCatalog;
30
+ export declare function computeVisualCompositionCatalogHash(catalog?: VisualCompositionCatalog): string;
31
+ export declare function expandVisualCompositionPreset(presetId: string, options?: {
32
+ inputs?: Record<string, JsonValue>;
33
+ provenance?: Provenance;
34
+ sourceHintIds?: string[];
35
+ }): ChangeIntent[];
36
+ export declare function compositionCatalogCanonicalJson(catalog?: VisualCompositionCatalog): string;
@@ -0,0 +1,108 @@
1
+ import { canonicalize, sha256 } from './canonical.js';
2
+ export const VISUAL_COMPOSITION_SCHEMA_VERSION = 'voce.visual-composition/v1alpha1';
3
+ export const VISUAL_COMPOSITION_CATALOG_ID = 'visual-composition.v1';
4
+ export const VISUAL_COMPOSITION_FIXED_TIME = '2026-01-01T00:00:00.000Z';
5
+ const enumPath = (path, allowedValues, defaultImportance = 'preferred') => ({ path, valueKind: 'enum', cardinality: 'one', allowedValues, defaultImportance });
6
+ const boolPath = (path, defaultImportance = 'preferred') => ({ path, valueKind: 'boolean', cardinality: 'one', defaultImportance });
7
+ const clone = (value) => JSON.parse(JSON.stringify(value));
8
+ function deepFreeze(value) {
9
+ if (value !== null && typeof value === 'object' && !Object.isFrozen(value)) {
10
+ for (const child of Object.values(value))
11
+ deepFreeze(child);
12
+ Object.freeze(value);
13
+ }
14
+ return value;
15
+ }
16
+ export const VISUAL_COMPOSITION_PATHS = [
17
+ enumPath('camera.framing.shotScale', ['extreme_close_up', 'close_up', 'head_and_shoulders', 'bust_shot', 'medium_close_up', 'medium_shot', 'knee_shot', 'full_shot', 'long_shot', 'extreme_long_shot']),
18
+ boolPath('camera.framing.crop.keepHead'), boolPath('camera.framing.crop.keepHands'), boolPath('camera.framing.crop.keepBothFeet'), boolPath('camera.framing.crop.keepProduct'), boolPath('camera.framing.crop.keepSignatureProp'),
19
+ enumPath('camera.view.elevation', ['eye_level', 'low_angle', 'high_angle', 'birds_eye']), enumPath('camera.view.relationship', ['front', 'three_quarter', 'profile', 'rear', 'over_the_shoulder']),
20
+ enumPath('camera.roll.mode', ['level', 'dutch_left', 'dutch_right']), enumPath('camera.lens.focalLengthClass', ['ultra_wide', 'wide', 'normal', 'telephoto', 'super_telephoto']), enumPath('camera.lens.perspective', ['expanded', 'natural', 'compressed']),
21
+ boolPath('camera.composition.patterns.centeredSymmetry'), boolPath('camera.composition.patterns.ruleOfThirds'), boolPath('camera.composition.patterns.leadingLines'), boolPath('camera.composition.patterns.diagonal'), boolPath('camera.composition.patterns.sCurve'), boolPath('camera.composition.patterns.triangle'),
22
+ enumPath('camera.composition.placement', ['center', 'left_third', 'right_third', 'upper_third', 'lower_third']), enumPath('camera.composition.negativeSpace', ['none', 'left', 'right', 'above', 'below', 'surrounding']),
23
+ boolPath('camera.composition.leadingRoom.enabled'), enumPath('camera.composition.leadingRoom.direction', ['left', 'right', 'forward', 'up', 'down']),
24
+ enumPath('camera.composition.foregroundTreatment', ['clear', 'soft_obstruction', 'strong_obstruction']), boolPath('camera.composition.framingDevices.frameWithinFrame'), boolPath('camera.composition.framingDevices.environmentalPortrait'),
25
+ boolPath('camera.composition.reflection.enabled'), enumPath('camera.composition.reflection.surface', ['mirror', 'glass', 'water', 'screen', 'polished_surface']), enumPath('camera.composition.reflection.role', ['supporting', 'co_primary', 'primary']), enumPath('camera.composition.environmentRelationship', ['isolated', 'contextual', 'environment_dominant']),
26
+ enumPath('lighting.subjectRendering', ['natural', 'silhouette']),
27
+ ];
28
+ const preset = (id, category, changes, requiredInputs = [], compatibilityHints = []) => ({ id, labelKey: `visualComposition.${id}.label`, descriptionKey: `visualComposition.${id}.description`, category, ...(compatibilityHints.length ? { compatibilityHints } : {}), changes, ...(requiredInputs.length ? { requiredInputs } : {}) });
29
+ export const VISUAL_COMPOSITION_PRESETS = [
30
+ preset('extreme-close-up', 'framing', [{ operation: 'replace', targetPath: 'camera.framing.shotScale', requestedValue: 'extreme_close_up' }]),
31
+ preset('close-up', 'framing', [{ operation: 'replace', targetPath: 'camera.framing.shotScale', requestedValue: 'close_up' }]),
32
+ preset('head-and-shoulders', 'framing', [{ operation: 'replace', targetPath: 'camera.framing.shotScale', requestedValue: 'head_and_shoulders' }, { operation: 'adjust', targetPath: 'camera.framing.crop.keepHead', requestedValue: true }]),
33
+ preset('bust-shot', 'framing', [{ operation: 'replace', targetPath: 'camera.framing.shotScale', requestedValue: 'bust_shot' }, { operation: 'adjust', targetPath: 'camera.framing.crop.keepHead', requestedValue: true }]),
34
+ preset('medium-close-up', 'framing', [{ operation: 'replace', targetPath: 'camera.framing.shotScale', requestedValue: 'medium_close_up' }]),
35
+ preset('medium-shot', 'framing', [{ operation: 'replace', targetPath: 'camera.framing.shotScale', requestedValue: 'medium_shot' }]),
36
+ preset('knee-shot', 'framing', [{ operation: 'replace', targetPath: 'camera.framing.shotScale', requestedValue: 'knee_shot' }]),
37
+ preset('full-shot', 'framing', [{ operation: 'replace', targetPath: 'camera.framing.shotScale', requestedValue: 'full_shot' }, { operation: 'adjust', targetPath: 'camera.framing.crop.keepBothFeet', requestedValue: true }]),
38
+ preset('long-shot', 'framing', [{ operation: 'replace', targetPath: 'camera.framing.shotScale', requestedValue: 'long_shot' }, { operation: 'adjust', targetPath: 'camera.composition.environmentRelationship', requestedValue: 'contextual' }]),
39
+ preset('extreme-long-shot', 'framing', [{ operation: 'replace', targetPath: 'camera.framing.shotScale', requestedValue: 'extreme_long_shot' }, { operation: 'adjust', targetPath: 'camera.composition.environmentRelationship', requestedValue: 'environment_dominant' }]),
40
+ preset('low-angle', 'view', [{ operation: 'adjust', targetPath: 'camera.view.elevation', requestedValue: 'low_angle' }]),
41
+ preset('high-angle', 'view', [{ operation: 'adjust', targetPath: 'camera.view.elevation', requestedValue: 'high_angle' }]),
42
+ preset('birds-eye-view', 'view', [{ operation: 'adjust', targetPath: 'camera.view.elevation', requestedValue: 'birds_eye' }]),
43
+ preset('over-the-shoulder', 'view', [{ operation: 'adjust', targetPath: 'camera.view.relationship', requestedValue: 'over_the_shoulder' }], [], ['foreground-shoulder-subject-or-declared-equivalent']),
44
+ preset('dutch-angle', 'view', [{ operation: 'adjust', targetPath: 'camera.roll.mode', valueFrom: 'direction' }], ['direction']),
45
+ preset('centered-symmetry', 'layout', [{ operation: 'adjust', targetPath: 'camera.composition.patterns.centeredSymmetry', requestedValue: true }, { operation: 'adjust', targetPath: 'camera.composition.placement', requestedValue: 'center' }]),
46
+ preset('rule-of-thirds', 'layout', [{ operation: 'adjust', targetPath: 'camera.composition.patterns.ruleOfThirds', requestedValue: true }]),
47
+ preset('leading-lines', 'layout', [{ operation: 'adjust', targetPath: 'camera.composition.patterns.leadingLines', requestedValue: true }], [], ['suitable-scene-geometry-or-review']),
48
+ preset('leading-room', 'layout', [{ operation: 'adjust', targetPath: 'camera.composition.leadingRoom.enabled', requestedValue: true }, { operation: 'adjust', targetPath: 'camera.composition.leadingRoom.direction', valueFrom: 'direction' }], ['direction']),
49
+ preset('diagonal-composition', 'layout', [{ operation: 'adjust', targetPath: 'camera.composition.patterns.diagonal', requestedValue: true }]),
50
+ preset('s-curve-composition', 'layout', [{ operation: 'adjust', targetPath: 'camera.composition.patterns.sCurve', requestedValue: true }]),
51
+ preset('triangle-composition', 'layout', [{ operation: 'adjust', targetPath: 'camera.composition.patterns.triangle', requestedValue: true }]),
52
+ preset('negative-space', 'layout', [{ operation: 'adjust', targetPath: 'camera.composition.negativeSpace', valueFrom: 'direction' }], ['direction']),
53
+ preset('frame-within-frame', 'depth', [{ operation: 'adjust', targetPath: 'camera.composition.framingDevices.frameWithinFrame', requestedValue: true }]),
54
+ preset('foreground-obstruction', 'depth', [{ operation: 'adjust', targetPath: 'camera.composition.foregroundTreatment', requestedValue: 'soft_obstruction' }]),
55
+ preset('profile-silhouette', 'view', [{ operation: 'adjust', targetPath: 'camera.view.relationship', requestedValue: 'profile' }]),
56
+ preset('reflection-composition', 'depth', [{ operation: 'adjust', targetPath: 'camera.composition.reflection.enabled', requestedValue: true }, { operation: 'adjust', targetPath: 'camera.composition.reflection.surface', valueFrom: 'surface' }], ['surface']),
57
+ preset('mirror-composition', 'depth', [{ operation: 'adjust', targetPath: 'camera.composition.reflection.enabled', requestedValue: true }, { operation: 'adjust', targetPath: 'camera.composition.reflection.surface', requestedValue: 'mirror' }]),
58
+ preset('telephoto-compression', 'lens', [{ operation: 'adjust', targetPath: 'camera.lens.focalLengthClass', requestedValue: 'telephoto' }, { operation: 'adjust', targetPath: 'camera.lens.perspective', requestedValue: 'compressed' }]),
59
+ preset('environmental-portrait', 'depth', [{ operation: 'adjust', targetPath: 'camera.composition.framingDevices.environmentalPortrait', requestedValue: true }, { operation: 'adjust', targetPath: 'camera.composition.environmentRelationship', requestedValue: 'contextual' }]),
60
+ ];
61
+ deepFreeze(VISUAL_COMPOSITION_PATHS);
62
+ deepFreeze(VISUAL_COMPOSITION_PRESETS);
63
+ const VISUAL_COMPOSITION_CATALOG_BODY = { schemaVersion: VISUAL_COMPOSITION_SCHEMA_VERSION, id: VISUAL_COMPOSITION_CATALOG_ID, paths: VISUAL_COMPOSITION_PATHS, presets: VISUAL_COMPOSITION_PRESETS };
64
+ export const VISUAL_COMPOSITION_CATALOG = deepFreeze({ ...VISUAL_COMPOSITION_CATALOG_BODY, catalogHash: sha256(VISUAL_COMPOSITION_CATALOG_BODY) });
65
+ export function computeVisualCompositionCatalogHash(catalog = VISUAL_COMPOSITION_CATALOG) {
66
+ return sha256({ schemaVersion: catalog.schemaVersion, id: catalog.id, paths: catalog.paths, presets: catalog.presets });
67
+ }
68
+ function valueForTemplate(template, options) {
69
+ let value = template.valueFrom === undefined ? template.requestedValue : options[template.valueFrom];
70
+ if (template.valueFrom !== undefined && value === undefined)
71
+ throw new Error('COMPOSITION_PRESET_INPUT_REQUIRED');
72
+ if (template.targetPath === 'camera.roll.mode') {
73
+ if (value !== 'left' && value !== 'right')
74
+ throw new Error('COMPOSITION_DIRECTION_INVALID');
75
+ value = value === 'left' ? 'dutch_left' : 'dutch_right';
76
+ }
77
+ if (value === undefined)
78
+ return undefined;
79
+ const definition = VISUAL_COMPOSITION_PATHS.find((item) => item.path === template.targetPath);
80
+ const valid = definition?.valueKind === 'boolean' ? typeof value === 'boolean'
81
+ : definition?.valueKind === 'string' ? typeof value === 'string'
82
+ : definition?.valueKind === 'number' ? typeof value === 'number' && Number.isFinite(value)
83
+ : definition?.valueKind === 'enum' && definition.allowedValues?.some((allowed) => canonicalize(allowed) === canonicalize(value));
84
+ if (!definition || !valid)
85
+ throw new Error('COMPOSITION_PRESET_INPUT_INVALID');
86
+ return clone(value);
87
+ }
88
+ export function expandVisualCompositionPreset(presetId, options = {}) {
89
+ const selected = VISUAL_COMPOSITION_PRESETS.find((item) => item.id === presetId);
90
+ if (!selected)
91
+ throw new Error('COMPOSITION_PRESET_NOT_FOUND');
92
+ const inputs = options.inputs ?? {};
93
+ for (const key of selected.requiredInputs ?? [])
94
+ if (inputs[key] === undefined)
95
+ throw new Error('COMPOSITION_PRESET_INPUT_REQUIRED');
96
+ const provenance = options.provenance ?? { source: 'user_explicit', sourceIds: [presetId], createdBy: 'visual-composition-preset', createdAt: VISUAL_COMPOSITION_FIXED_TIME };
97
+ const sourceHintIds = [...new Set([presetId, ...(options.sourceHintIds ?? [])])].sort();
98
+ const changes = selected.changes.map((template, index) => {
99
+ const requestedValue = valueForTemplate(template, inputs);
100
+ return { schemaVersion: 'voce.change-intent/v1alpha1', id: `composition.${presetId}.${index + 1}`, operation: template.operation, targetPath: template.targetPath, ...(requestedValue === undefined ? {} : { requestedValue }), sourceHintIds: [...sourceHintIds], importance: 'preferred', provenance: clone(provenance) };
101
+ });
102
+ if (presetId === 'profile-silhouette' && inputs.silhouette === true)
103
+ changes.push({ schemaVersion: 'voce.change-intent/v1alpha1', id: `composition.${presetId}.silhouette`, operation: 'adjust', targetPath: 'lighting.subjectRendering', requestedValue: 'silhouette', sourceHintIds: [...sourceHintIds], importance: 'preferred', provenance: clone(provenance) });
104
+ return changes;
105
+ }
106
+ export function compositionCatalogCanonicalJson(catalog = VISUAL_COMPOSITION_CATALOG) {
107
+ return canonicalize({ schemaVersion: catalog.schemaVersion, id: catalog.id, paths: catalog.paths, presets: catalog.presets });
108
+ }
package/dist/index.d.ts CHANGED
@@ -19,3 +19,4 @@ export * from './evidence.js';
19
19
  export * from './m4.js';
20
20
  export * from './m5.js';
21
21
  export * from './m6.js';
22
+ export * from './composition.js';
package/dist/index.js CHANGED
@@ -164,6 +164,74 @@ function contributionDigest(value) {
164
164
  delete copy.contentDigest;
165
165
  return sha256(copy);
166
166
  }
167
+ function validateTypedContribution(category, value) {
168
+ if (category === 'ontologyVocabulary') {
169
+ if (!Array.isArray(value.paths))
170
+ throw new Error('PACK_TYPED_CONTRIBUTION_INVALID');
171
+ const ids = new Set();
172
+ for (const raw of value.paths) {
173
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw))
174
+ throw new Error('PACK_TYPED_CONTRIBUTION_INVALID');
175
+ const path = raw;
176
+ if (typeof path.path !== 'string' || !path.path || (path.valueKind !== 'boolean' && path.valueKind !== 'enum' && path.valueKind !== 'string' && path.valueKind !== 'number') || (path.cardinality !== 'one' && path.cardinality !== 'many') || ids.has(path.path))
177
+ throw new Error('PACK_TYPED_CONTRIBUTION_INVALID');
178
+ if (path.valueKind === 'enum' && (!Array.isArray(path.allowedValues) || path.allowedValues.length === 0))
179
+ throw new Error('PACK_TYPED_CONTRIBUTION_INVALID');
180
+ ids.add(path.path);
181
+ }
182
+ return;
183
+ }
184
+ if (category === 'rulePacks') {
185
+ if (!Array.isArray(value.rules))
186
+ throw new Error('PACK_TYPED_CONTRIBUTION_INVALID');
187
+ const ids = new Set();
188
+ for (const raw of value.rules) {
189
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw))
190
+ throw new Error('PACK_TYPED_CONTRIBUTION_INVALID');
191
+ const rule = raw;
192
+ if (typeof rule.id !== 'string' || !rule.id || ids.has(rule.id) || !['incompatibility', 'dependency', 'cardinality', 'occlusion', 'resource'].includes(String(rule.kind)) || !Array.isArray(rule.operands) || rule.operands.length < 2 || !rule.resolution || typeof rule.resolution !== 'object' || Array.isArray(rule.resolution) || typeof rule.explanation !== 'string')
193
+ throw new Error('PACK_TYPED_CONTRIBUTION_INVALID');
194
+ const operandIds = new Set();
195
+ for (const rawOperand of rule.operands) {
196
+ if (!rawOperand || typeof rawOperand !== 'object' || Array.isArray(rawOperand))
197
+ throw new Error('PACK_TYPED_CONTRIBUTION_INVALID');
198
+ const operand = rawOperand;
199
+ if (typeof operand.id !== 'string' || !operand.id || operandIds.has(operand.id) || !Array.isArray(operand.conditions) || operand.conditions.length === 0)
200
+ throw new Error('PACK_TYPED_CONTRIBUTION_INVALID');
201
+ operandIds.add(operand.id);
202
+ for (const rawCondition of operand.conditions) {
203
+ if (!rawCondition || typeof rawCondition !== 'object' || Array.isArray(rawCondition))
204
+ throw new Error('PACK_TYPED_CONTRIBUTION_INVALID');
205
+ const condition = rawCondition;
206
+ if (typeof condition.path !== 'string' || !condition.path || !['present', 'absent', 'equals', 'contains'].includes(String(condition.operator)))
207
+ throw new Error('PACK_TYPED_CONTRIBUTION_INVALID');
208
+ if ((condition.operator === 'present' || condition.operator === 'absent') && condition.value !== undefined)
209
+ throw new Error('PACK_TYPED_CONTRIBUTION_INVALID');
210
+ if ((condition.operator === 'equals' || condition.operator === 'contains') && condition.value === undefined)
211
+ throw new Error('PACK_TYPED_CONTRIBUTION_INVALID');
212
+ }
213
+ }
214
+ const resolution = rule.resolution;
215
+ if ((resolution.strategy !== 'block' && resolution.strategy !== 'degrade_operand') || typeof resolution.reasonCode !== 'string' || !resolution.reasonCode || (resolution.strategy === 'degrade_operand' && (typeof resolution.operandId !== 'string' || !operandIds.has(resolution.operandId))))
216
+ throw new Error('PACK_TYPED_CONTRIBUTION_INVALID');
217
+ ids.add(rule.id);
218
+ }
219
+ return;
220
+ }
221
+ if (category === 'promptSections') {
222
+ if (!Array.isArray(value.sections))
223
+ throw new Error('PACK_TYPED_CONTRIBUTION_INVALID');
224
+ const ids = new Set();
225
+ for (const raw of value.sections) {
226
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw))
227
+ throw new Error('PACK_TYPED_CONTRIBUTION_INVALID');
228
+ const section = raw;
229
+ if (typeof section.id !== 'string' || !section.id || ids.has(section.id) || typeof section.group !== 'string' || !Number.isInteger(section.order) || !Array.isArray(section.pathPrefixes) || section.pathPrefixes.length === 0 || section.pathPrefixes.some((item) => typeof item !== 'string' || !item) || typeof section.templateKey !== 'string' || !section.templateKey)
230
+ throw new Error('PACK_TYPED_CONTRIBUTION_INVALID');
231
+ ids.add(section.id);
232
+ }
233
+ }
234
+ }
167
235
  function descriptorFor(definition, files) {
168
236
  if (!definition || typeof definition !== 'object' || Array.isArray(definition) || !definition.manifest)
169
237
  throw new Error('PACK_MANIFEST_INVALID');
@@ -197,6 +265,8 @@ function descriptorFor(definition, files) {
197
265
  bodyIds.add(id);
198
266
  if (indexed.get(id) !== contribution.contentDigest || contributionDigest(contribution) !== contribution.contentDigest)
199
267
  throw new Error('PACK_DIGEST_MISMATCH');
268
+ if ((category === 'ontologyVocabulary' && Array.isArray(contribution.paths)) || (category === 'rulePacks' && Array.isArray(contribution.rules)) || (category === 'promptSections' && Array.isArray(contribution.sections)))
269
+ validateTypedContribution(category, contribution);
200
270
  }
201
271
  }
202
272
  const manifestHash = sha256(json(definition.manifest));
@@ -335,6 +405,29 @@ function composeCategory(category, ordered, packages, disabled, defaultValues) {
335
405
  }
336
406
  return { values: [...byId.values()].map((item) => withSource(item.value, item.packId, category, item.sourcePackIds)) };
337
407
  }
408
+ function ontologyPathDefinitionCollision(contributions) {
409
+ const byPath = new Map();
410
+ for (const contribution of contributions) {
411
+ const paths = Array.isArray(contribution.paths) ? contribution.paths : [];
412
+ for (const raw of paths) {
413
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw))
414
+ continue;
415
+ const definition = raw;
416
+ if (typeof definition.path !== 'string')
417
+ continue;
418
+ const entries = byPath.get(definition.path) ?? [];
419
+ entries.push({ canonical: canonicalize(raw), packId: contribution.packId, contributionId: contribution.contributionId });
420
+ byPath.set(definition.path, entries);
421
+ }
422
+ }
423
+ for (const path of [...byPath.keys()].sort(compareCodeUnits)) {
424
+ const entries = byPath.get(path);
425
+ if (new Set(entries.map((entry) => entry.canonical)).size <= 1)
426
+ continue;
427
+ return conflict('PACK_RULE_CONFLICT', [...new Set(entries.map((entry) => entry.packId))].sort(compareCodeUnits), `Ontology path ${path} has conflicting definitions after ScenarioPack composition.`, 'Make duplicate path definitions canonically identical or use distinct ontology paths.', [...new Set(entries.map((entry) => entry.contributionId))].sort(compareCodeUnits));
428
+ }
429
+ return undefined;
430
+ }
338
431
  function overridePayload(override) {
339
432
  return { id: override.id, operation: override.operation, reasonCode: override.reasonCode };
340
433
  }
@@ -550,6 +643,9 @@ export function resolveScenario(selection, catalog, packages, descriptors = new
550
643
  return { status: 'blocked', report: failureReport(selected, dependencyTrace, compositionTrace, overrideTraces, [composed.collision], warnings) };
551
644
  effectiveValues[category] = composed.values;
552
645
  }
646
+ const pathCollision = ontologyPathDefinitionCollision(effectiveValues.ontologyVocabulary);
647
+ if (pathCollision)
648
+ return { status: 'blocked', report: failureReport(selected, dependencyTrace, compositionTrace, overrideTraces, [pathCollision], warnings) };
553
649
  const entries = ordered.map((entry) => ({
554
650
  packId: entry.manifest.packId,
555
651
  version: entry.manifest.version,
@@ -563,7 +659,7 @@ export function resolveScenario(selection, catalog, packages, descriptors = new
563
659
  const lockBase = { schemaVersion: 'voce.scenario-pack-lock/v1alpha1', contractVersion: CONTRACT_VERSION, resolverVersion: catalog.resolverVersion, catalogHash: catalog.catalogHash, canonicalization: 'voce.canonical-json/v1alpha1', rootPackId: root.manifest.packId, entries, compositionOrder: order, ...(overlay ? { hostPolicyOverlayHash: overlay.overlayHash } : {}), hostOverrideHashes: appliedOverrides.map((item) => item.contentHash) };
564
660
  const lock = { ...lockBase, compositionHash: sha256(json(lockBase)), lockHash: '' };
565
661
  lock.lockHash = hashWithoutSelf(lock, 'lockHash');
566
- const effective = { lockHash: lock.lockHash, rootPackId: root.manifest.packId, extensionPackIds: ordered.filter((entry) => entry.manifest.kind === 'extension').map((entry) => entry.manifest.packId), compositionOrder: order, configurations: Object.fromEntries([...configs].sort((left, right) => compareCodeUnits(left[0], right[0]))), ...effectiveValues, capabilityRequirements: ordered.flatMap((entry) => entry.manifest.capabilityRequirements), declarations: ordered.map((entry) => entry.manifest.declarations), appliedOverrides, effectiveScenarioHash: '' };
662
+ const effective = { lockHash: lock.lockHash, rootPackId: root.manifest.packId, extensionPackIds: ordered.filter((entry) => entry.manifest.kind === 'extension').map((entry) => entry.manifest.packId), compositionOrder: order, configurations: Object.fromEntries([...configs].sort((left, right) => compareCodeUnits(left[0], right[0]))), ontologyVocabulary: effectiveValues.ontologyVocabulary, rulePacks: effectiveValues.rulePacks, interpretationScopes: effectiveValues.interpretationScopes, promptSections: effectiveValues.promptSections, reviewTemplates: effectiveValues.reviewTemplates, defaults: effectiveValues.defaults, capabilityRequirements: ordered.flatMap((entry) => entry.manifest.capabilityRequirements), declarations: ordered.map((entry) => entry.manifest.declarations), appliedOverrides, effectiveScenarioHash: '' };
567
663
  effective.effectiveScenarioHash = hashWithoutSelf(effective, 'effectiveScenarioHash');
568
664
  const reportBase = { status: 'resolved', lockHash: lock.lockHash, effectiveScenarioHash: effective.effectiveScenarioHash, selected: selected.map((entry) => ({ packId: entry.manifest.packId, version: entry.manifest.version, kind: entry.manifest.kind, packageDigest: entry.packageDigest, manifestHash: entry.manifestHash })), dependencyTrace, compositionTrace, overrideTraces, conflicts: [], warnings };
569
665
  return { status: 'resolved', lock, effectiveScenario: effective, report: { ...reportBase, reportHash: hashWithoutSelf(reportBase, 'reportHash') } };
@@ -606,3 +702,4 @@ export * from './evidence.js';
606
702
  export * from './m4.js';
607
703
  export * from './m5.js';
608
704
  export * from './m6.js';
705
+ export * from './composition.js';
package/dist/m4.js CHANGED
@@ -450,27 +450,68 @@ function internalRule(value, contributionId) {
450
450
  const id = typeof object.id === 'string' ? object.id : typeof object.ruleId === 'string' ? object.ruleId : undefined;
451
451
  if (!id)
452
452
  return undefined;
453
- const type = object.ruleType ?? object.type ?? object.kind;
454
- const kind = type === 'resource' ? 'resource' : type === 'dependency' ? 'dependency' : type === 'occlusion' ? 'occlusion' : 'incompatibility';
455
- const leftPaths = Array.isArray(object.leftPaths) ? object.leftPaths.filter((item) => typeof item === 'string') : Array.isArray(object.whenPaths) ? object.whenPaths.filter((item) => typeof item === 'string') : [];
456
- const rightPaths = Array.isArray(object.rightPaths) ? object.rightPaths.filter((item) => typeof item === 'string') : Array.isArray(object.conflictingPaths) ? object.conflictingPaths.filter((item) => typeof item === 'string') : [];
457
- if (leftPaths.length === 0 || rightPaths.length === 0)
453
+ const kind = object.kind;
454
+ if (kind !== 'incompatibility' && kind !== 'dependency' && kind !== 'cardinality' && kind !== 'occlusion' && kind !== 'resource')
458
455
  return undefined;
459
- const tokenList = (field) => Array.isArray(object[field]) ? object[field].filter((item) => typeof item === 'string') : [];
460
- const severity = normalizeImportance(object.importance ?? object.severity);
456
+ if (!Array.isArray(object.operands) || object.operands.length < 2)
457
+ return undefined;
458
+ const conditionOperator = (value) => value === 'present' || value === 'absent' || value === 'equals' || value === 'contains';
459
+ const conditions = (value) => {
460
+ if (!Array.isArray(value) || value.length === 0)
461
+ return undefined;
462
+ const result = [];
463
+ for (const candidate of value) {
464
+ if (!candidate || typeof candidate !== 'object' || Array.isArray(candidate))
465
+ return undefined;
466
+ const condition = candidate;
467
+ if (typeof condition.path !== 'string' || !condition.path || !conditionOperator(condition.operator))
468
+ return undefined;
469
+ if ((condition.operator === 'present' || condition.operator === 'absent') && condition.value !== undefined)
470
+ return undefined;
471
+ if ((condition.operator === 'equals' || condition.operator === 'contains') && condition.value === undefined)
472
+ return undefined;
473
+ result.push({ path: condition.path, operator: condition.operator, ...(condition.value === undefined ? {} : { value: jsonReady(condition.value) }) });
474
+ }
475
+ return result;
476
+ };
477
+ const operands = [];
478
+ const operandIds = new Set();
479
+ for (const candidate of object.operands) {
480
+ if (!candidate || typeof candidate !== 'object' || Array.isArray(candidate))
481
+ return undefined;
482
+ const operand = candidate;
483
+ if (typeof operand.id !== 'string' || !operand.id || operandIds.has(operand.id))
484
+ return undefined;
485
+ const operandConditions = conditions(operand.conditions);
486
+ if (!operandConditions)
487
+ return undefined;
488
+ operandIds.add(operand.id);
489
+ operands.push({ id: operand.id, conditions: operandConditions, match: 'all' });
490
+ }
491
+ const resolutionValue = object.resolution;
492
+ if (!resolutionValue || typeof resolutionValue !== 'object' || Array.isArray(resolutionValue))
493
+ return undefined;
494
+ const resolutionObject = resolutionValue;
495
+ if (resolutionObject.strategy !== 'block' && resolutionObject.strategy !== 'degrade_operand')
496
+ return undefined;
497
+ if (typeof resolutionObject.reasonCode !== 'string' || !resolutionObject.reasonCode)
498
+ return undefined;
499
+ const operandId = resolutionObject.operandId;
500
+ if (resolutionObject.strategy === 'degrade_operand' && (typeof operandId !== 'string' || !operandIds.has(operandId)))
501
+ return undefined;
502
+ const resolution = { strategy: resolutionObject.strategy, reasonCode: resolutionObject.reasonCode, ...(operandId === undefined ? {} : { operandId: operandId }) };
503
+ const severity = normalizeImportance(object.importance);
461
504
  const code = typeof object.code === 'string' ? object.code : `RULE_${id.toUpperCase().replaceAll(/[^A-Z0-9]+/g, '_')}`;
462
- const reasonCode = typeof object.reasonCode === 'string' ? object.reasonCode : code;
463
- const message = typeof object.message === 'string' ? object.message : `Declarative rule ${id} found incompatible constraints.`;
505
+ const reasonCode = resolution.reasonCode;
506
+ const message = typeof object.explanation === 'string' ? object.explanation : typeof object.message === 'string' ? object.message : `Declarative rule ${id} found incompatible constraints.`;
464
507
  const dependencyKind = object.dependencyKind === 'parent_detail' || object.dependencyKind === 'identity_garment' || object.dependencyKind === 'source_isolation' || object.dependencyKind === 'visibility' || object.dependencyKind === 'occludes' || object.dependencyKind === 'ordered_before' || object.dependencyKind === 'supports' || object.dependencyKind === 'excludes' || object.dependencyKind === 'requires' ? object.dependencyKind : undefined;
465
508
  return {
466
509
  id,
467
510
  contributionId,
468
511
  code,
469
512
  kind,
470
- leftPaths: sortedStrings(leftPaths),
471
- rightPaths: sortedStrings(rightPaths),
472
- leftTokens: sortedStrings(tokenList('leftTokens')),
473
- rightTokens: sortedStrings(tokenList('rightTokens')),
513
+ operands,
514
+ resolution,
474
515
  importance: severity,
475
516
  reasonCode,
476
517
  message,
@@ -479,25 +520,40 @@ function internalRule(value, contributionId) {
479
520
  };
480
521
  }
481
522
  function allRules(effectiveScenario, collisionIds = []) {
482
- const values = M4_DECLARATIVE_RULE_FIXTURES.map((value) => ({ value }));
523
+ const values = [];
483
524
  for (const contribution of effectiveScenario?.rulePacks ?? []) {
484
525
  const object = contribution;
485
526
  const rules = Array.isArray(object.rules) ? object.rules : [];
486
527
  for (const rule of rules)
487
528
  values.push({ value: rule, contributionId: typeof object.contributionId === 'string' ? object.contributionId : undefined });
488
- if (rules.length === 0)
489
- values.push({ value: contribution, contributionId: typeof object.contributionId === 'string' ? object.contributionId : undefined });
490
529
  }
491
530
  const byId = new Map();
492
- for (const entry of values) {
493
- const rule = internalRule(entry.value, entry.contributionId);
494
- if (!rule)
495
- continue;
531
+ const add = (rule) => {
496
532
  const prior = byId.get(rule.id);
497
- if (prior && canonicalize(jsonReady(rule)) !== canonicalize(jsonReady(prior)))
533
+ const semantic = (value) => canonicalize(jsonReady(cleanWithout(value, 'contributionId')));
534
+ if (prior && semantic(rule) !== semantic(prior))
498
535
  collisionIds.push(rule.id);
499
536
  if (!prior || canonicalize(jsonReady(rule)) < canonicalize(jsonReady(prior)))
500
537
  byId.set(rule.id, rule);
538
+ };
539
+ for (const value of M4_DECLARATIVE_RULE_FIXTURES) {
540
+ const left = Array.from(value.leftPaths);
541
+ const right = Array.from(value.rightPaths);
542
+ if (left.length === 0 || right.length === 0)
543
+ continue;
544
+ const toConditions = (paths, tokens = []) => [
545
+ ...paths.map((path) => ({ path, operator: 'present' })),
546
+ ...tokens.map((token) => ({ path: paths[0], operator: 'contains', value: token })),
547
+ ];
548
+ const leftOperand = { id: 'left', conditions: left.map((path) => ({ path, operator: 'present' })), match: 'any' };
549
+ const rightOperand = { id: 'right', conditions: toConditions(right, value.rightTokens), match: 'any' };
550
+ add({ id: value.id, code: value.code, kind: value.ruleType === 'resource' ? 'resource' : 'occlusion', operands: [leftOperand, rightOperand], resolution: { strategy: 'block', reasonCode: value.reasonCode }, importance: value.importance, reasonCode: value.reasonCode, message: value.message, resourceId: 'resourceId' in value ? value.resourceId : undefined });
551
+ }
552
+ for (const entry of values) {
553
+ const rule = internalRule(entry.value, entry.contributionId);
554
+ if (!rule)
555
+ continue;
556
+ add(rule);
501
557
  }
502
558
  return sortedBy([...byId.values()], (item) => item.id);
503
559
  }
@@ -522,12 +578,174 @@ function semanticItems(input) {
522
578
  }));
523
579
  return sortedBy([...intentItems, ...factItems], (item) => `${item.path}|${item.id}`);
524
580
  }
581
+ function conditionMatches(condition, items) {
582
+ const candidates = items.filter((item) => pathMatches(item.path, condition.path));
583
+ if (condition.operator === 'absent')
584
+ return { matched: candidates.length === 0, items: [] };
585
+ if (condition.operator === 'present')
586
+ return { matched: candidates.length > 0, items: candidates };
587
+ if (condition.operator === 'equals') {
588
+ const matched = candidates.filter((item) => canonicalize(item.value) === canonicalize(condition.value));
589
+ return { matched: matched.length > 0, items: matched };
590
+ }
591
+ const value = condition.value;
592
+ const matched = typeof value === 'string'
593
+ ? candidates.filter((item) => valueHasToken(item.value, [value]))
594
+ : candidates.filter((item) => Array.isArray(item.value) && item.value.some((entry) => canonicalize(entry) === canonicalize(value)));
595
+ return { matched: matched.length > 0, items: matched };
596
+ }
597
+ function compositionVocabulary(effectiveScenario, collisionPaths = []) {
598
+ const definitions = new Map();
599
+ for (const contribution of effectiveScenario?.ontologyVocabulary ?? []) {
600
+ for (const raw of (contribution.paths ?? [])) {
601
+ if (!raw || typeof raw.path !== 'string' || !raw.path)
602
+ continue;
603
+ const prior = definitions.get(raw.path);
604
+ if (prior && canonicalize(jsonReady(raw)) !== canonicalize(jsonReady(prior)))
605
+ collisionPaths.push(raw.path);
606
+ else if (!prior)
607
+ definitions.set(raw.path, clone(raw));
608
+ }
609
+ }
610
+ return definitions;
611
+ }
612
+ function ontologyScalarMatches(definition, value) {
613
+ if (definition.valueKind === 'boolean')
614
+ return typeof value === 'boolean';
615
+ if (definition.valueKind === 'string')
616
+ return typeof value === 'string';
617
+ if (definition.valueKind === 'number')
618
+ return typeof value === 'number' && Number.isFinite(value);
619
+ return Array.isArray(definition.allowedValues) && definition.allowedValues.some((allowed) => canonicalize(allowed) === canonicalize(value));
620
+ }
621
+ function ontologyValueMatches(definition, value) {
622
+ if (definition.cardinality === 'many' && Array.isArray(value))
623
+ return value.every((entry) => ontologyScalarMatches(definition, entry));
624
+ return ontologyScalarMatches(definition, value);
625
+ }
626
+ function applyCardinalityResolution(constraints, goals, vocabulary) {
627
+ const remap = new Map();
628
+ const conflicts = [];
629
+ const degradations = [];
630
+ const traces = [];
631
+ const mergedConstraints = [];
632
+ const cardinalityConstraintIds = new Set();
633
+ const byPath = new Map();
634
+ for (const constraint of constraints) {
635
+ if (!constraint.targetPath || constraint.value === undefined || constraint.status === 'unsatisfied' || !vocabulary.has(constraint.targetPath))
636
+ continue;
637
+ if (vocabulary.get(constraint.targetPath).cardinality !== 'one')
638
+ continue;
639
+ byPath.set(constraint.targetPath, [...(byPath.get(constraint.targetPath) ?? []), constraint]);
640
+ }
641
+ const degraded = new Set();
642
+ const markUnsatisfied = (constraint, ruleId, reasonCode) => {
643
+ constraint.status = 'unsatisfied';
644
+ constraint.ruleId = ruleId;
645
+ constraint.reasonCode = reasonCode;
646
+ constraint.constraintHash = computeConstraintHash(constraint);
647
+ };
648
+ for (const [path, pathConstraints] of [...byPath.entries()].sort((left, right) => compareCodeUnits(left[0], right[0]))) {
649
+ for (const item of pathConstraints)
650
+ cardinalityConstraintIds.add(item.id);
651
+ const valueGroups = new Map();
652
+ for (const constraint of sortedBy(pathConstraints, (item) => item.id)) {
653
+ const key = canonicalize(jsonReady(constraint.value));
654
+ valueGroups.set(key, [...(valueGroups.get(key) ?? []), constraint]);
655
+ }
656
+ const merged = [];
657
+ for (const group of [...valueGroups.values()].sort((left, right) => compareCodeUnits(canonicalize(jsonReady(left[0].value)), canonicalize(jsonReady(right[0].value))))) {
658
+ const representative = sortedBy(group, (item) => item.id)[0];
659
+ const mergedConstraint = createConstraint({
660
+ ...representative,
661
+ id: hashId('constraint-value', { path, value: representative.value }),
662
+ status: group.some((item) => item.status === 'active') ? 'active' : 'satisfied',
663
+ importance: importanceFromValues(group.map((item) => item.importance), representative.importance),
664
+ sourceIds: sortedStrings(group.flatMap((item) => item.sourceIds)),
665
+ goalIds: sortedStrings(group.flatMap((item) => item.goalIds)),
666
+ });
667
+ merged.push(mergedConstraint);
668
+ mergedConstraints.push(mergedConstraint);
669
+ for (const item of group)
670
+ remap.set(item.id, mergedConstraint.id);
671
+ }
672
+ for (const goal of goals)
673
+ goal.constraintIds = sortedStrings(goal.constraintIds.map((id) => remap.get(id) ?? id));
674
+ const strongGroups = merged.filter((item) => item.importance === 'hard' || item.importance === 'required');
675
+ const ruleId = `rule.cardinality.${path}`;
676
+ if (merged.length > 1) {
677
+ const blocking = strongGroups.length !== 1;
678
+ const conflict = createConstraintConflict({
679
+ schemaVersion: CONSTRAINT_CONFLICT_SCHEMA_VERSION,
680
+ id: hashId('constraint-conflict', { code: 'CARDINALITY_CONFLICT', path, constraintIds: merged.map((item) => item.id) }),
681
+ code: 'CARDINALITY_CONFLICT',
682
+ severity: importanceFromValues(merged.map((item) => item.importance), 'preferred'),
683
+ targetPath: path,
684
+ constraintIds: merged.map((item) => item.id),
685
+ dependencyIds: [], resourceClaimIds: [],
686
+ message: `Cardinality one path ${path} received incompatible values.`,
687
+ blocking,
688
+ waiverAllowed: !blocking,
689
+ });
690
+ conflicts.push(conflict);
691
+ if (!blocking && strongGroups.length === 1) {
692
+ for (const loser of merged.filter((item) => item.importance === 'preferred')) {
693
+ markUnsatisfied(loser, ruleId, 'CARDINALITY_PREFERENCE_DEGRADED');
694
+ if (degraded.has(loser.id))
695
+ continue;
696
+ degraded.add(loser.id);
697
+ const degradation = createDegradation({
698
+ schemaVersion: DEGRADATION_SCHEMA_VERSION,
699
+ id: hashId('degradation', { conflictId: conflict.id, constraintId: loser.id }),
700
+ preferenceId: loser.id,
701
+ constraintId: loser.id,
702
+ reasonCode: 'CARDINALITY_PREFERENCE_DEGRADED',
703
+ impact: conflict.message,
704
+ affectedIds: [loser.id, strongGroups[0].id],
705
+ explanation: `Preferred value for ${path} was excluded in favor of the stronger constraint.`,
706
+ });
707
+ degradations.push(degradation);
708
+ traces.push(createRuleTrace({ schemaVersion: RULE_TRACE_SCHEMA_VERSION, id: hashId('rule-trace', { ruleId, constraintId: loser.id }), ruleId, inputIds: [loser.id, strongGroups[0].id], outputIds: [degradation.id], outcome: 'degraded', reasonCode: degradation.reasonCode, message: degradation.explanation }));
709
+ }
710
+ }
711
+ else {
712
+ traces.push(createRuleTrace({ schemaVersion: RULE_TRACE_SCHEMA_VERSION, id: hashId('rule-trace', { ruleId, constraintIds: merged.map((item) => item.id) }), ruleId, inputIds: merged.map((item) => item.id), outputIds: [conflict.id], outcome: 'blocked', reasonCode: 'CARDINALITY_CONFLICT', message: conflict.message }));
713
+ }
714
+ }
715
+ }
716
+ for (const goal of goals)
717
+ goal.constraintIds = sortedStrings(goal.constraintIds.map((id) => remap.get(id) ?? id));
718
+ return { constraints: sortedBy([...constraints.filter((item) => !cardinalityConstraintIds.has(item.id)), ...mergedConstraints], (item) => item.id), remap, conflicts, degradations, traces };
719
+ }
720
+ function operandMatches(operand, items) {
721
+ const matches = operand.conditions.map((condition) => conditionMatches(condition, items));
722
+ if (operand.match === 'any') {
723
+ const matched = matches.filter((value) => value.matched);
724
+ return matched.length > 0 ? uniqueSortedObjects(matched.flatMap((value) => value.items), (item) => item.id) : undefined;
725
+ }
726
+ if (matches.some((value) => !value.matched))
727
+ return undefined;
728
+ return uniqueSortedObjects(matches.flatMap((value) => value.items), (item) => item.id);
729
+ }
525
730
  function ruleMatches(rule, items) {
526
- const left = items.filter((item) => pathPresent([item.path], rule.leftPaths) && (rule.leftTokens.length === 0 || valueHasToken(item.value, rule.leftTokens)));
527
- const right = items.filter((item) => pathPresent([item.path], rule.rightPaths) && (rule.rightTokens.length === 0 || valueHasToken(item.value, rule.rightTokens)));
528
- if (left.length === 0 || right.length === 0)
731
+ const operands = new Map();
732
+ const missingOperandIds = [];
733
+ for (const operand of rule.operands) {
734
+ const match = operandMatches(operand, items);
735
+ if (match === undefined)
736
+ missingOperandIds.push(operand.id);
737
+ else
738
+ operands.set(operand.id, match);
739
+ }
740
+ if (missingOperandIds.length === 0)
741
+ return { operands, missingOperandIds };
742
+ if (rule.kind !== 'dependency')
743
+ return undefined;
744
+ const trigger = rule.operands[0];
745
+ const triggerMatch = operands.get(trigger.id);
746
+ if (triggerMatch === undefined || triggerMatch.length === 0)
529
747
  return undefined;
530
- return { left, right };
748
+ return { operands, missingOperandIds };
531
749
  }
532
750
  function goalForIntent(intent) {
533
751
  const base = {
@@ -608,20 +826,20 @@ function outputConstraints(contract) {
608
826
  function makeRuleTrace(rule, inputIds, outputIds, outcome, reasonCode = rule.reasonCode, message = rule.message) {
609
827
  return createRuleTrace({ schemaVersion: RULE_TRACE_SCHEMA_VERSION, id: hashId('rule-trace', { ruleId: rule.id, inputIds, outputIds, outcome, reasonCode }), ruleId: rule.id, ...(rule.contributionId ? { contributionId: rule.contributionId } : {}), inputIds, outputIds, outcome, reasonCode, message });
610
828
  }
611
- function conflictForRule(rule, left, right) {
612
- const constraintIds = sortedStrings([...left, ...right].map((item) => item.id));
613
- const severity = importanceFromValues([...left, ...right].map((item) => item.importance), rule.importance);
829
+ function conflictForRule(rule, constraints, blocking = true) {
830
+ const constraintIds = sortedStrings(constraints.map((item) => item.id));
831
+ const severity = importanceFromValues(constraints.map((item) => item.importance), rule.importance);
614
832
  return createConstraintConflict({
615
833
  schemaVersion: CONSTRAINT_CONFLICT_SCHEMA_VERSION,
616
834
  id: hashId('constraint-conflict', { code: rule.code, ruleId: rule.id, constraintIds }),
617
835
  code: rule.code,
618
836
  severity,
619
- targetPath: [...left, ...right].map((item) => item.targetPath).find((item) => typeof item === 'string'),
837
+ targetPath: constraints.map((item) => item.targetPath).find((item) => typeof item === 'string'),
620
838
  constraintIds,
621
839
  dependencyIds: [],
622
840
  resourceClaimIds: rule.resourceId ? [hashId('resource', { resourceId: rule.resourceId, constraintIds })] : [],
623
841
  message: rule.message,
624
- blocking: severity !== 'preferred',
842
+ blocking,
625
843
  waiverAllowed: severity === 'required',
626
844
  });
627
845
  }
@@ -822,6 +1040,16 @@ export class ConstraintGraphCompiler {
822
1040
  allRules(input.effectiveScenario, ruleCollisionIds);
823
1041
  if (ruleCollisionIds.length)
824
1042
  return blockedConstraintIR(input, ['DECLARATIVE_RULE_ID_COLLISION']);
1043
+ const vocabularyCollisionPaths = [];
1044
+ const vocabulary = compositionVocabulary(input.effectiveScenario, vocabularyCollisionPaths);
1045
+ if (vocabularyCollisionPaths.length)
1046
+ return blockedConstraintIR(input, ['ONTOLOGY_PATH_DEFINITION_COLLISION']);
1047
+ const invalidOntologyValues = semanticItems(input).filter((item) => {
1048
+ const definition = vocabulary.get(item.path);
1049
+ return definition !== undefined && !ontologyValueMatches(definition, item.value);
1050
+ });
1051
+ if (invalidOntologyValues.length)
1052
+ return blockedConstraintIR(input, ['ONTOLOGY_VALUE_INVALID']);
825
1053
  const goals = [];
826
1054
  const constraints = [];
827
1055
  const dependencies = [];
@@ -881,32 +1109,89 @@ export class ConstraintGraphCompiler {
881
1109
  const output = outputConstraints(input.outputContract);
882
1110
  constraints.push(...output);
883
1111
  traces.push(createRuleTrace({ schemaVersion: RULE_TRACE_SCHEMA_VERSION, id: hashId('rule-trace', { kind: 'output', id: output[0].id }), ruleId: 'rule.output-contract', inputIds: [], outputIds: output.map((item) => item.id), outcome: 'applied', reasonCode: 'OUTPUT_CONTRACT_REQUIRED', message: 'OutputContract was compiled into provider-neutral output constraints.' }));
1112
+ const cardinality = applyCardinalityResolution(constraints, goals, vocabulary);
1113
+ constraints.splice(0, constraints.length, ...cardinality.constraints);
1114
+ conflicts.push(...cardinality.conflicts);
1115
+ degradations.push(...cardinality.degradations);
1116
+ traces.push(...cardinality.traces);
1117
+ for (const goal of goals)
1118
+ goal.goalHash = computeGoalHash(goal);
1119
+ constraintByPath.clear();
1120
+ for (const constraint of constraints)
1121
+ if (constraint.targetPath)
1122
+ constraintByPath.set(constraint.targetPath, [...(constraintByPath.get(constraint.targetPath) ?? []), constraint]);
884
1123
  const ruleItems = items;
1124
+ const degradedConstraintIds = new Set(degradations.map((item) => item.constraintId).filter((item) => typeof item === 'string'));
1125
+ const markUnsatisfied = (constraint, rule, reasonCode) => {
1126
+ constraint.status = 'unsatisfied';
1127
+ constraint.ruleId = rule.id;
1128
+ constraint.reasonCode = reasonCode;
1129
+ constraint.constraintHash = computeConstraintHash(constraint);
1130
+ };
1131
+ const degrade = (constraint, rule, conflict, affectedIds) => {
1132
+ const target = constraints.find((item) => item.id === constraint.id);
1133
+ if (!target || target.importance !== 'preferred' || degradedConstraintIds.has(target.id))
1134
+ return undefined;
1135
+ markUnsatisfied(target, rule, rule.resolution.reasonCode);
1136
+ degradedConstraintIds.add(target.id);
1137
+ const degradation = createDegradation({ schemaVersion: DEGRADATION_SCHEMA_VERSION, id: hashId('degradation', { conflictId: conflict.id, constraintId: target.id }), preferenceId: target.id, constraintId: target.id, reasonCode: rule.resolution.reasonCode, impact: conflict.message, affectedIds: sortedStrings([target.id, ...affectedIds]), explanation: `Preferred constraint ${target.id} was excluded because ${conflict.message}` });
1138
+ degradations.push(degradation);
1139
+ return degradation;
1140
+ };
885
1141
  for (const rule of allRules(input.effectiveScenario)) {
886
1142
  const match = ruleMatches(rule, ruleItems);
887
1143
  if (!match) {
888
1144
  traces.push(makeRuleTrace(rule, [], [], 'skipped', 'RULE_PRECONDITION_NOT_MET', 'Declarative rule preconditions were not met.'));
889
1145
  continue;
890
1146
  }
891
- const leftConstraints = match.left.flatMap((item) => constraintByPath.get(item.path) ?? []);
892
- const rightConstraints = match.right.flatMap((item) => constraintByPath.get(item.path) ?? []);
1147
+ const matchedItems = [...match.operands.values()].flat();
1148
+ const matchedConstraints = uniqueSortedObjects(matchedItems.flatMap((item) => constraintByPath.get(item.path) ?? []), (item) => item.id);
1149
+ const inputIds = sortedStrings(matchedItems.map((item) => item.id));
893
1150
  if (rule.kind === 'dependency') {
894
- const parent = leftConstraints[0];
895
- const child = rightConstraints[0];
896
- if (parent && child) {
897
- const dependency = createConstraintDependency({ schemaVersion: CONSTRAINT_DEPENDENCY_SCHEMA_VERSION, id: hashId('constraint-dependency', { ruleId: rule.id, parent: parent.id, child: child.id }), parentId: parent.id, childId: child.id, kind: rule.dependencyKind ?? 'requires', importance: rule.importance, explanation: rule.message });
898
- dependencies.push(dependency);
899
- traces.push(makeRuleTrace(rule, [...match.left, ...match.right].map((item) => item.id), [dependency.id], 'applied', rule.reasonCode));
1151
+ const parentItems = match.operands.get(rule.operands[0].id) ?? [];
1152
+ const parentConstraints = uniqueSortedObjects(parentItems.flatMap((item) => constraintByPath.get(item.path) ?? []), (item) => item.id);
1153
+ if (match.missingOperandIds.length > 0) {
1154
+ const dependencyConflict = createConstraintConflict({ schemaVersion: CONSTRAINT_CONFLICT_SCHEMA_VERSION, id: hashId('constraint-conflict', { code: 'CONSTRAINT_DEPENDENCY_MISSING', ruleId: rule.id, constraintIds: parentConstraints.map((item) => item.id), missingOperandIds: match.missingOperandIds }), code: 'CONSTRAINT_DEPENDENCY_MISSING', severity: importanceFromValues(parentConstraints.map((item) => item.importance), rule.importance), targetPath: parentConstraints[0]?.targetPath, constraintIds: parentConstraints.map((item) => item.id), dependencyIds: [], resourceClaimIds: [], message: `${rule.message} Required dependency operand(s) missing: ${match.missingOperandIds.join(', ')}.`, blocking: !(rule.resolution.strategy === 'degrade_operand' && parentConstraints.every((item) => item.importance === 'preferred')), waiverAllowed: rule.importance === 'required' });
1155
+ conflicts.push(dependencyConflict);
1156
+ const dependencyDegradations = rule.resolution.strategy === 'degrade_operand' ? parentConstraints.map((constraint) => degrade(constraint, rule, dependencyConflict, parentConstraints.map((item) => item.id))).filter((item) => item !== undefined).map((item) => item.id) : [];
1157
+ traces.push(makeRuleTrace(rule, inputIds, [dependencyConflict.id, ...dependencyDegradations], dependencyConflict.blocking ? 'blocked' : 'degraded', 'CONSTRAINT_DEPENDENCY_MISSING', dependencyConflict.message));
1158
+ }
1159
+ else {
1160
+ const dependencyIds = [];
1161
+ for (const operand of rule.operands.slice(1)) {
1162
+ const childItems = match.operands.get(operand.id) ?? [];
1163
+ const child = uniqueSortedObjects(childItems.flatMap((item) => constraintByPath.get(item.path) ?? []), (item) => item.id)[0];
1164
+ const parent = parentConstraints[0];
1165
+ if (!parent || !child)
1166
+ continue;
1167
+ const dependency = createConstraintDependency({ schemaVersion: CONSTRAINT_DEPENDENCY_SCHEMA_VERSION, id: hashId('constraint-dependency', { ruleId: rule.id, parent: parent.id, child: child.id }), parentId: parent.id, childId: child.id, kind: rule.dependencyKind ?? 'requires', importance: rule.importance, explanation: rule.message });
1168
+ dependencies.push(dependency);
1169
+ dependencyIds.push(dependency.id);
1170
+ }
1171
+ traces.push(makeRuleTrace(rule, inputIds, dependencyIds, 'applied', rule.reasonCode));
900
1172
  }
901
1173
  continue;
902
1174
  }
903
- const conflict = conflictForRule(rule, leftConstraints, rightConstraints);
1175
+ if (matchedConstraints.length < 2) {
1176
+ traces.push(makeRuleTrace(rule, inputIds, [], 'skipped', 'RULE_SINGLE_OPERAND_MATCH', 'A declarative rule matched fewer than two constraint operands.'));
1177
+ continue;
1178
+ }
1179
+ const strong = matchedConstraints.filter((constraint) => constraint.importance === 'hard' || constraint.importance === 'required');
1180
+ let degradable = matchedConstraints.filter((constraint) => constraint.importance === 'preferred');
1181
+ if (strong.length === 0 && rule.resolution.strategy === 'degrade_operand' && rule.resolution.operandId) {
1182
+ const targetedItems = match.operands.get(rule.resolution.operandId) ?? [];
1183
+ const targetedIds = new Set(targetedItems.map((item) => item.id));
1184
+ degradable = matchedConstraints.filter((constraint) => constraint.importance === 'preferred' && constraint.sourceIds.some((sourceId) => targetedIds.has(sourceId)) || constraint.importance === 'preferred' && targetedItems.some((item) => item.path === constraint.targetPath));
1185
+ }
1186
+ const autoDegrade = strong.length === 1 ? matchedConstraints.filter((constraint) => constraint.importance === 'preferred') : strong.length === 0 && degradable.length > 0 && degradable.length < matchedConstraints.length ? degradable : [];
1187
+ const conflict = conflictForRule(rule, matchedConstraints, autoDegrade.length === 0);
904
1188
  conflicts.push(conflict);
1189
+ const degradationIds = autoDegrade.map((constraint) => degrade(constraint, rule, conflict, strong.map((item) => item.id))).filter((item) => item !== undefined).map((item) => item.id);
905
1190
  if (rule.resourceId) {
906
- const claim = createResourceClaim({ schemaVersion: RESOURCE_CLAIM_SCHEMA_VERSION, id: conflict.resourceClaimIds[0] ?? hashId('resource', { resourceId: rule.resourceId, constraintIds: conflict.constraintIds }), resourceId: rule.resourceId, mode: 'exclusive', claimantIds: [...match.left, ...match.right].map((item) => item.id), constraintIds: conflict.constraintIds, quantity: 1, explanation: rule.message });
1191
+ const claim = createResourceClaim({ schemaVersion: RESOURCE_CLAIM_SCHEMA_VERSION, id: conflict.resourceClaimIds[0] ?? hashId('resource', { resourceId: rule.resourceId, constraintIds: conflict.constraintIds }), resourceId: rule.resourceId, mode: 'exclusive', claimantIds: inputIds, constraintIds: conflict.constraintIds, quantity: 1, explanation: rule.message });
907
1192
  resourceClaims.push(claim);
908
1193
  }
909
- traces.push(makeRuleTrace(rule, [...match.left, ...match.right].map((item) => item.id), conflict.constraintIds, conflict.blocking ? 'blocked' : 'degraded', conflict.code));
1194
+ traces.push(makeRuleTrace(rule, inputIds, [conflict.id, ...degradationIds], conflict.blocking ? 'blocked' : 'degraded', conflict.code));
910
1195
  }
911
1196
  const allConstraintIds = new Set(constraints.map((constraint) => constraint.id));
912
1197
  for (const dependency of dependencies) {
@@ -936,12 +1221,6 @@ export class ConstraintGraphCompiler {
936
1221
  adjustedConflicts.push(rehashConflict({ ...conflict, blocking: false, message: `${conflict.message} Proceeding only under an explicit scoped waiver.` }));
937
1222
  warnings.push('REQUIRED_CONFLICT_WAIVED');
938
1223
  }
939
- else if (conflict.severity === 'preferred') {
940
- adjustedConflicts.push(rehashConflict({ ...conflict, blocking: false }));
941
- const degradation = createDegradation({ schemaVersion: DEGRADATION_SCHEMA_VERSION, id: hashId('degradation', { conflictId: conflict.id }), preferenceId: conflict.id, constraintId: conflict.constraintIds[0], reasonCode: conflict.code, impact: conflict.message, affectedIds: conflict.constraintIds, explanation: `Preferred constraint was degraded deterministically because ${conflict.message}` });
942
- degradations.push(degradation);
943
- traces.push(createRuleTrace({ schemaVersion: RULE_TRACE_SCHEMA_VERSION, id: hashId('rule-trace', { degradation: degradation.id }), ruleId: 'rule.preferred-degradation', inputIds: conflict.constraintIds, outputIds: [degradation.id], outcome: 'degraded', reasonCode: conflict.code, message: degradation.explanation }));
944
- }
945
1224
  else
946
1225
  adjustedConflicts.push(conflict);
947
1226
  }
package/dist/m5.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { ArtifactHandle, ArtifactReplayResult, CompilationContext, CompensationReceipt, ConstraintIR, CleanupReceipt, Evaluation, ExecutionAuthorization, ExecutionRun, ExecutionTraceProjection, HumanAcceptance, JsonObject, PipelinePlan, PipelineStep, PromptCandidateIR, PromptCompilationInput, PromptGuardInput, PromptGuardResult, PromptIR, PromptOptimizationInput, PromptOptimizer, PromptTransformation, ProviderAdapter, ProviderRenderRequest, ProviderRenderResult, ReferencePlan, RemoteCallRun, RemoteCallAuthorization, StepReceipt, VersionPin } from '@voce-engine/contracts';
1
+ import type { ArtifactHandle, ArtifactReplayResult, CompilationContext, CompensationReceipt, ConstraintIR, CleanupReceipt, Evaluation, ExecutionAuthorization, ExecutionRun, ExecutionRunState, ExecutionTraceProjection, HumanAcceptance, JsonObject, PipelinePlan, PipelineStep, PromptCandidateIR, PromptCompilationInput, PromptGuardInput, PromptGuardResult, PromptIR, PromptOptimizationInput, PromptOptimizer, PromptTransformation, ProviderAdapter, ProviderRenderRequest, ProviderRenderResult, ReferencePlan, RemoteCallRun, RemoteCallAuthorization, StepReceipt, VersionPin } from '@voce-engine/contracts';
2
2
  export declare const PROMPT_COMPILER_VERSION = "voce.prompt-compiler/v1alpha1";
3
3
  export declare const PROMPT_OPTIMIZER_VERSION = "voce.deterministic-prompt-optimizer/v1alpha1";
4
4
  export declare const PROMPT_GUARD_VERSION = "voce.prompt-guard/v1alpha1";
@@ -75,7 +75,7 @@ export interface OfflineExecutionInput {
75
75
  options?: OfflineExecutionOptions;
76
76
  }
77
77
  export interface OfflineExecutionResult {
78
- status: 'blocked' | 'completed' | 'failed' | 'submission_unknown' | 'cancelled' | 'needs_review';
78
+ status: 'blocked' | ExecutionRunState;
79
79
  code: string;
80
80
  reasons: string[];
81
81
  executionRun?: ExecutionRun;
package/dist/m5.js CHANGED
@@ -152,6 +152,7 @@ function normalizedPromptIRProjection(prompt) {
152
152
  forbidden: sortedBy(prompt.forbidden, (item) => item.id),
153
153
  output: clone(prompt.output),
154
154
  constraintCoverage: sortedBy(prompt.constraintCoverage, (item) => item.constraintId).map(promptCoverageProjection),
155
+ excludedConstraints: sortedBy(prompt.excludedConstraints, (item) => item.constraintId),
155
156
  sourceIds: sortedStrings(prompt.sourceIds),
156
157
  constraintIds: sortedStrings(prompt.constraintIds),
157
158
  decisionIds: sortedStrings(prompt.decisionIds),
@@ -230,6 +231,7 @@ function normalizedPromptCandidateProjection(candidate) {
230
231
  parameters: sortedBy(parameters, (item) => item.id).map(promptParameterProjection),
231
232
  referenceMappings: [...candidate.referenceMappings].sort((left, right) => left.order - right.order || compareCodeUnits(left.id, right.id)).map(promptReferenceMappingProjection),
232
233
  constraintCoverage: sortedBy(coverage, (item) => item.constraintId).map(promptCoverageProjection),
234
+ excludedConstraints: sortedBy(candidate.excludedConstraints, (item) => item.constraintId),
233
235
  transformations: candidate.transformations.map(transformationProjection),
234
236
  optimizer: clone(candidate.optimizer),
235
237
  mode: candidate.mode,
@@ -337,6 +339,24 @@ function integrityReasonsForConstraintIR(ir, context, caseId, caseRevision) {
337
339
  for (const item of ir.degradedPreferences)
338
340
  if (!isHash(item.degradationHash) || computeDegradationHash(item) !== item.degradationHash)
339
341
  reasons.push('DEGRADATION_HASH_MISMATCH');
342
+ const degradationCounts = new Map();
343
+ for (const degradation of ir.degradedPreferences) {
344
+ if (!degradation.constraintId) {
345
+ reasons.push('DEGRADATION_CONSTRAINT_MISSING');
346
+ continue;
347
+ }
348
+ degradationCounts.set(degradation.constraintId, (degradationCounts.get(degradation.constraintId) ?? 0) + 1);
349
+ const constraint = ir.constraints.find((item) => item.id === degradation.constraintId);
350
+ if (!constraint || constraint.importance !== 'preferred' || constraint.status !== 'unsatisfied')
351
+ reasons.push('DEGRADATION_DISPOSITION_INVALID');
352
+ }
353
+ for (const constraint of ir.constraints) {
354
+ const count = degradationCounts.get(constraint.id) ?? 0;
355
+ if (constraint.status === 'unsatisfied' && count !== 1)
356
+ reasons.push('UNSATISFIED_CONSTRAINT_DEGRADATION_MISMATCH');
357
+ if (constraint.status !== 'unsatisfied' && count > 0)
358
+ reasons.push('DEGRADATION_DISPOSITION_INVALID');
359
+ }
340
360
  for (const item of ir.reviewRequirements)
341
361
  if (!isHash(item.reviewHash) || computeReviewRequirementHash(item) !== item.reviewHash)
342
362
  reasons.push('REVIEW_REQUIREMENT_HASH_MISMATCH');
@@ -420,22 +440,57 @@ function promptCompilationInputReasons(input) {
420
440
  reasons.push('TARGET_ADAPTER_INVALID');
421
441
  if (!input.targetCapabilityProfile || !isHash(input.targetCapabilityProfile.digest))
422
442
  reasons.push('TARGET_PROFILE_INVALID');
443
+ if (!input.effectiveScenario || !isHash(input.context?.effectiveScenarioHash ?? '') || input.effectiveScenario?.effectiveScenarioHash !== input.context?.effectiveScenarioHash)
444
+ reasons.push('EFFECTIVE_SCENARIO_CONTEXT_MISMATCH');
445
+ if (input.effectiveScenario) {
446
+ const scenarioProjection = clone(input.effectiveScenario);
447
+ delete scenarioProjection.effectiveScenarioHash;
448
+ if (!isHash(input.effectiveScenario.effectiveScenarioHash) || sha256(jsonReady(scenarioProjection)) !== input.effectiveScenario.effectiveScenarioHash)
449
+ reasons.push('EFFECTIVE_SCENARIO_HASH_MISMATCH');
450
+ const definitions = input.effectiveScenario.promptSections.flatMap((contribution) => contribution.sections ?? []);
451
+ const definitionIds = new Set();
452
+ for (const definition of definitions) {
453
+ if (definitionIds.has(definition.id) || !Number.isInteger(definition.order) || !Array.isArray(definition.pathPrefixes) || definition.pathPrefixes.length === 0 || typeof definition.templateKey !== 'string')
454
+ reasons.push('PROMPT_SECTION_POLICY_INVALID');
455
+ definitionIds.add(definition.id);
456
+ }
457
+ }
423
458
  if (input.pipelinePlan && input.pipelinePlan.profileDigest !== input.targetCapabilityProfile?.digest)
424
459
  reasons.push('TARGET_PROFILE_PLAN_MISMATCH');
425
460
  if (input.pipelinePlan && !input.pipelinePlan.adapterDigests.includes(input.targetAdapter?.digest ?? ''))
426
461
  reasons.push('TARGET_ADAPTER_PLAN_MISMATCH');
427
462
  return sortedStrings(reasons);
428
463
  }
429
- function sectionForConstraint(constraint, order, decisionIds) {
464
+ function pathRelates(left, right) {
465
+ return left === right || left.startsWith(`${right}.`) || right.startsWith(`${left}.`);
466
+ }
467
+ function promptSectionDefinitions(scenario) {
468
+ const definitions = scenario.promptSections.flatMap((contribution) => contribution.sections ?? []).map((definition) => clone(definition));
469
+ return definitions.sort((left, right) => left.order - right.order || compareCodeUnits(left.id, right.id));
470
+ }
471
+ function sectionDefinitionForConstraint(constraint, definitions) {
472
+ const paths = constraint.targetPaths.length ? constraint.targetPaths : constraint.targetPath ? [constraint.targetPath] : [];
473
+ return definitions.find((definition) => definition.pathPrefixes.some((prefix) => paths.some((path) => pathRelates(path, prefix))));
474
+ }
475
+ function promptExclusions(constraintIR) {
476
+ return sortedBy(constraintIR.constraints.filter((constraint) => constraint.status === 'unsatisfied').map((constraint) => {
477
+ const degradations = constraintIR.degradedPreferences.filter((item) => item.constraintId === constraint.id);
478
+ if (degradations.length !== 1)
479
+ throw new Error('UNSATISFIED_CONSTRAINT_DEGRADATION_MISMATCH');
480
+ return { constraintId: constraint.id, degradationId: degradations[0].id, reasonCode: degradations[0].reasonCode, sourceIds: sortedStrings(constraint.sourceIds) };
481
+ }), (item) => item.constraintId);
482
+ }
483
+ function sectionForConstraint(constraint, order, decisionIds, definition) {
430
484
  const locked = constraint.importance === 'hard' || constraint.importance === 'required' || constraint.kind === 'output';
485
+ const content = `${definition?.templateKey ? `${definition.templateKey}: ` : ''}${constraintText(constraint)}`;
431
486
  return {
432
487
  schemaVersion: PROMPT_SECTION_SCHEMA_VERSION,
433
- id: hashId('prompt-section', { kind: constraint.importance, constraintId: constraint.id, order }),
488
+ id: hashId('prompt-section', { kind: constraint.importance, constraintId: constraint.id, order, definitionId: definition?.id }),
434
489
  kind: constraint.importance === 'hard' ? 'hard_constraint' : constraint.importance === 'required' ? 'required_constraint' : 'preferred',
435
490
  priority: constraint.importance === 'hard' ? 100 : constraint.importance === 'required' ? 80 : 40,
436
491
  order,
437
- content: constraintText(constraint),
438
- text: constraintText(constraint),
492
+ content,
493
+ text: content,
439
494
  constraintIds: [constraint.id],
440
495
  sourceIds: sortedStrings(constraint.sourceIds),
441
496
  decisionIds: sortedStrings(decisionIds),
@@ -446,7 +501,8 @@ function sectionForConstraint(constraint, order, decisionIds) {
446
501
  };
447
502
  }
448
503
  function referenceMapping(reference, constraints, decisionIds) {
449
- const required = reference.constraintIds.some((id) => constraints.find((constraint) => constraint.id === id)?.importance !== 'preferred') || reference.sourceBindingIds.length > 0;
504
+ const effectiveConstraintIds = sortedStrings(reference.constraintIds.filter((id) => constraints.some((constraint) => constraint.id === id)));
505
+ const required = effectiveConstraintIds.some((id) => constraints.find((constraint) => constraint.id === id)?.importance !== 'preferred') || reference.sourceBindingIds.length > 0;
450
506
  return {
451
507
  schemaVersion: PROMPT_REFERENCE_MAPPING_SCHEMA_VERSION,
452
508
  id: hashId('prompt-reference-mapping', { plannedReferenceId: reference.id, assetId: reference.assetId, contentHash: reference.contentHash, order: reference.order }),
@@ -458,7 +514,7 @@ function referenceMapping(reference, constraints, decisionIds) {
458
514
  role: reference.role,
459
515
  order: reference.order,
460
516
  required,
461
- constraintIds: sortedStrings(reference.constraintIds),
517
+ constraintIds: effectiveConstraintIds,
462
518
  sourceBindingIds: sortedStrings(reference.sourceBindingIds),
463
519
  decisionIds: sortedStrings(decisionIds),
464
520
  };
@@ -505,7 +561,9 @@ export class PromptCompiler {
505
561
  if (reasons.length)
506
562
  throw new Error(reasons.join('|'));
507
563
  const decisionIds = sortedStrings(safeInput.context.decisionHashes);
508
- const constraints = sortedBy(safeInput.constraintIR.constraints, (item) => item.id);
564
+ const excludedConstraints = promptExclusions(safeInput.constraintIR);
565
+ const constraints = sortedBy(safeInput.constraintIR.constraints.filter((constraint) => constraint.status === 'active' || constraint.status === 'satisfied'), (item) => item.id);
566
+ const definitions = promptSectionDefinitions(safeInput.effectiveScenario);
509
567
  const sections = [];
510
568
  const objective = safeInput.objective ?? 'Produce the requested visual result using only the approved constraints and references.';
511
569
  const positiveDescription = safeInput.positiveDescription ?? 'Express the approved target properties clearly and preserve all locked requirements.';
@@ -521,7 +579,11 @@ export class PromptCompiler {
521
579
  kind: 'positive', priority: 110, order: 1, content: positiveDescription, text: positiveDescription,
522
580
  constraintIds: [], sourceIds: [], decisionIds, assetIds: [], importance: 'required', mutability: 'rephraseable', locked: false,
523
581
  });
524
- constraints.forEach((constraint, index) => sections.push(sectionForConstraint(constraint, 10 + index, decisionIds)));
582
+ constraints.forEach((constraint, index) => {
583
+ const definition = sectionDefinitionForConstraint(constraint, definitions);
584
+ const policyOrder = definition?.order ?? 500;
585
+ sections.push(sectionForConstraint(constraint, 10 + policyOrder * 10 + index, decisionIds, definition));
586
+ });
525
587
  const mappings = [...safeInput.referencePlan.ordered].sort((left, right) => left.order - right.order || compareCodeUnits(left.id, right.id)).map((reference) => referenceMapping(reference, constraints, decisionIds));
526
588
  mappings.forEach((mapping, index) => sections.push({
527
589
  schemaVersion: PROMPT_SECTION_SCHEMA_VERSION,
@@ -566,6 +628,7 @@ export class PromptCompiler {
566
628
  forbidden: sortedBy(forbidden, (item) => item.id),
567
629
  output: clone(safeInput.outputContract),
568
630
  constraintCoverage: sortedBy(coverage, (item) => item.constraintId),
631
+ excludedConstraints,
569
632
  sourceIds: sortedStrings([...constraints.flatMap((constraint) => constraint.sourceIds), ...mappings.flatMap((mapping) => mapping.sourceBindingIds)]),
570
633
  constraintIds: sortedStrings(constraints.map((constraint) => constraint.id)),
571
634
  decisionIds,
@@ -654,6 +717,7 @@ export function createPromptCandidateIR(prompt, transformations = [], options =
654
717
  parameters: clone(parameters),
655
718
  referenceMappings: clone(safePrompt.referenceMappings),
656
719
  constraintCoverage: clone(safePrompt.constraintCoverage),
720
+ excludedConstraints: clone(safePrompt.excludedConstraints),
657
721
  transformations: normalizedTransformations,
658
722
  optimizer: clone(options.optimizer ?? { id: PROMPT_OPTIMIZER_VERSION, version: '1.0.0', digest: sha256({ optimizer: PROMPT_OPTIMIZER_VERSION }) }),
659
723
  mode: options.mode ?? 'strict',
@@ -733,7 +797,7 @@ function guardFinding(value) {
733
797
  }
734
798
  function guardInputReasons(input) {
735
799
  const reasons = [];
736
- if (!input || input.schemaVersion !== 'voce.prompt-guard-input/v1alpha1')
800
+ if (!input || input.schemaVersion !== 'voce.prompt-guard-input/v1alpha2')
737
801
  reasons.push('PROMPT_GUARD_INPUT_SCHEMA_INVALID');
738
802
  if (!input.promptIR || input.promptIR.schemaVersion !== PROMPT_IR_SCHEMA_VERSION || !isHash(input.promptIR.deterministicSignature) || computePromptIRHash(input.promptIR) !== input.promptIR.deterministicSignature)
739
803
  reasons.push('PROMPT_IR_SIGNATURE_MISMATCH');
@@ -751,6 +815,19 @@ function guardInputReasons(input) {
751
815
  reasons.push('PROMPT_CONTEXT_MISMATCH');
752
816
  if (input.constraintIR && integrityReasonsForConstraintIR(input.constraintIR, input.context, input.promptIR?.caseId ?? '', input.promptIR?.caseRevision ?? -1).length)
753
817
  reasons.push('CONSTRAINT_IR_INVALID');
818
+ if (input.promptIR && input.constraintIR) {
819
+ const excludedIds = new Set(input.promptIR.excludedConstraints.map((item) => item.constraintId));
820
+ if (excludedIds.size !== input.promptIR.excludedConstraints.length)
821
+ reasons.push('PROMPT_EXCLUSION_DUPLICATE');
822
+ for (const exclusion of input.promptIR.excludedConstraints) {
823
+ const constraint = input.constraintIR.constraints.find((item) => item.id === exclusion.constraintId);
824
+ const degradation = input.constraintIR.degradedPreferences.find((item) => item.id === exclusion.degradationId && item.constraintId === exclusion.constraintId);
825
+ if (!constraint || constraint.status !== 'unsatisfied' || !degradation || input.promptIR.constraintIds.includes(exclusion.constraintId))
826
+ reasons.push('PROMPT_EXCLUSION_INVALID');
827
+ }
828
+ if (input.constraintIR.constraints.some((constraint) => constraint.status === 'unsatisfied' && !excludedIds.has(constraint.id)))
829
+ reasons.push('PROMPT_EXCLUSION_MISSING');
830
+ }
754
831
  if (input.referencePlan && input.constraintIR && integrityReasonsForReferencePlan(input.referencePlan, input.constraintIR, input.promptIR?.caseId ?? '', input.promptIR?.caseRevision ?? -1, input.context?.contextHash ?? '').length)
755
832
  reasons.push('REFERENCE_PLAN_INVALID');
756
833
  if (input.pipelinePlan && input.constraintIR && input.referencePlan && integrityReasonsForPipelinePlan(input.pipelinePlan, input.constraintIR, input.referencePlan, input.outputContract, input.promptIR?.caseId ?? '', input.promptIR?.caseRevision ?? -1, input.context?.contextHash ?? '').length)
@@ -806,6 +883,24 @@ export class PromptGuard {
806
883
  const candidateMappings = candidate.referenceMappings;
807
884
  const baseCoverage = new Map(prompt.constraintCoverage.map((coverage) => [coverage.constraintId, coverage]));
808
885
  const candidateCoverageMap = new Map(candidateCoverage(candidate).map((coverage) => [coverage.constraintId, coverage]));
886
+ const excludedIds = new Set(prompt.excludedConstraints.map((item) => item.constraintId));
887
+ if (canonicalize(sortedBy(candidate.excludedConstraints, (item) => item.constraintId)) !== canonicalize(sortedBy(prompt.excludedConstraints, (item) => item.constraintId)))
888
+ addGuardFinding(findings, { code: 'PROMPT_EXCLUSION_SET_CHANGED', severity: 'critical', blocking: true, constraintIds: [...excludedIds], sourceIds: [], sectionIds: [], decisionIds: [], assetIds: [], explanation: 'Candidate changed the explicit Prompt IR exclusion set.' });
889
+ const reportExcludedLinks = (ids, sectionIds = []) => {
890
+ const linked = ids.filter((id) => excludedIds.has(id));
891
+ if (linked.length)
892
+ addGuardFinding(findings, { code: 'EXCLUDED_CONSTRAINT_REINTRODUCED', severity: 'critical', blocking: true, constraintIds: sortedStrings(linked), sourceIds: [], sectionIds, decisionIds: [], assetIds: [], explanation: 'Candidate re-linked an excluded constraint into prompt structure.' });
893
+ };
894
+ for (const section of candidateSectionList)
895
+ reportExcludedLinks(section.constraintIds, [section.id]);
896
+ for (const parameter of candidateParameterList)
897
+ reportExcludedLinks(parameter.constraintIds);
898
+ for (const mapping of candidateMappings)
899
+ reportExcludedLinks(mapping.constraintIds);
900
+ for (const coverage of candidateCoverage(candidate))
901
+ reportExcludedLinks([coverage.constraintId], coverage.sectionIds);
902
+ for (const transformation of candidate.transformations)
903
+ reportExcludedLinks('proof' in transformation ? transformation.proof?.preservedConstraintIds ?? [] : [], transformation.kind === 'rephrase' ? [transformation.sectionId] : []);
809
904
  for (const section of prompt.sections) {
810
905
  const candidateSection = candidateSectionMap.get(section.id);
811
906
  const locked = section.locked === true || section.mutability === 'locked' || section.kind === 'hard_constraint' || section.kind === 'required_constraint' || section.kind === 'output' || section.kind === 'reference';
package/dist/m6.d.ts CHANGED
@@ -52,6 +52,7 @@ export declare class RecordingMockTransport implements ProviderTransport {
52
52
  enqueue(response: ProviderResponseEnvelope): void;
53
53
  enqueueLookup(response: ProviderResponseEnvelope): void;
54
54
  private consume;
55
+ private consumeResponse;
55
56
  send(request: ProviderRequestEnvelope, context: ProviderTransportContext): Promise<ProviderResponseEnvelope>;
56
57
  lookup(request: ProviderSubmissionLookup, context: ProviderTransportContext): Promise<ProviderResponseEnvelope>;
57
58
  }
package/dist/m6.js CHANGED
@@ -436,6 +436,15 @@ export class RecordingMockTransport {
436
436
  throw new ProviderTransportError('REMOTE_CALL_BUDGET_EXHAUSTED', `Remote ${kind} budget has been exhausted.`);
437
437
  counts.set(key, count + 1);
438
438
  }
439
+ consumeResponse(queued, requestHash, missingCode, missingMessage) {
440
+ const exactIndex = queued.findIndex((item) => item.requestHash === requestHash);
441
+ if (exactIndex >= 0)
442
+ return queued.splice(exactIndex, 1)[0];
443
+ const wildcardIndex = queued.findIndex((item) => item.requestHash === '');
444
+ if (wildcardIndex >= 0)
445
+ return queued.splice(wildcardIndex, 1)[0];
446
+ return errorResponse(requestHash, missingCode, missingMessage);
447
+ }
439
448
  async send(request, context) {
440
449
  assertRemoteCallAuthorization(request, context);
441
450
  this.consume('send', context.authorization);
@@ -445,7 +454,7 @@ export class RecordingMockTransport {
445
454
  throw new ProviderTransportError('REMOTE_CALL_IDENTITY_REUSED', 'Idempotency identity cannot be reused for a different provider request.');
446
455
  this.sentRequests.set(identity, clone(request));
447
456
  this.calls.push({ requestId: request.id, requestHash: request.requestHash, adapterId: request.adapterId, profileDigest: request.profileDigest, destination: request.destination, ...(request.region === undefined ? {} : { region: request.region }), inputHash: request.inputHash, idempotencyKey: request.idempotencyKey });
448
- const response = this.responses.shift() ?? errorResponse(request.requestHash, 'MOCK_RESPONSE_MISSING', 'Recording mock response was not registered.');
457
+ const response = this.consumeResponse(this.responses, request.requestHash, 'MOCK_RESPONSE_MISSING', 'Recording mock response was not registered.');
449
458
  const normalized = response.requestHash === request.requestHash ? response : { ...response, requestHash: request.requestHash, responseHash: '' };
450
459
  if (!normalized.responseHash)
451
460
  normalized.responseHash = computeProviderResponseEnvelopeHash(normalized);
@@ -459,7 +468,7 @@ export class RecordingMockTransport {
459
468
  if (!original || !providerBindingMatches(original, request))
460
469
  throw new ProviderTransportError('PROVIDER_LOOKUP_BINDING_MISMATCH', 'Submission lookup is not bound to a prior provider request.');
461
470
  this.lookupCalls.push({ requestId: request.providerRequestId ?? request.lookupHash, requestHash: request.requestHash, adapterId: request.adapterId, profileDigest: request.profileDigest, destination: request.destination, ...(request.region === undefined ? {} : { region: request.region }), inputHash: request.inputHash, idempotencyKey: request.idempotencyKey });
462
- const response = this.lookups.shift() ?? errorResponse(request.requestHash, 'MOCK_LOOKUP_RESPONSE_MISSING', 'Recording lookup response was not registered.');
471
+ const response = this.consumeResponse(this.lookups, request.requestHash, 'MOCK_LOOKUP_RESPONSE_MISSING', 'Recording lookup response was not registered.');
463
472
  const normalized = response.requestHash === request.requestHash ? response : { ...response, requestHash: request.requestHash, responseHash: '' };
464
473
  if (!normalized.responseHash)
465
474
  normalized.responseHash = computeProviderResponseEnvelopeHash(normalized);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@voce-engine/core",
3
- "version": "0.1.0-rc.3",
3
+ "version": "0.1.0-rc.4",
4
4
  "description": "Deterministic visual ontology, constraint, reference, prompt, execution-planning, and evaluation runtime.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -21,6 +21,8 @@
21
21
  "dist/m5.d.ts",
22
22
  "dist/m6.js",
23
23
  "dist/m6.d.ts",
24
+ "dist/composition.js",
25
+ "dist/composition.d.ts",
24
26
  "README.md",
25
27
  "LICENSE"
26
28
  ],
@@ -41,7 +43,7 @@
41
43
  "tag": "next"
42
44
  },
43
45
  "dependencies": {
44
- "@voce-engine/contracts": "0.1.0-rc.3",
46
+ "@voce-engine/contracts": "0.1.0-rc.4",
45
47
  "semver": "7.7.2"
46
48
  },
47
49
  "devDependencies": {