@traits-dev/core 0.2.0 → 0.4.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.
@@ -25,13 +25,18 @@ interface ContextAdaptation {
25
25
  inject?: string[];
26
26
  priority?: number;
27
27
  }
28
+ interface LockedRule {
29
+ rule: string;
30
+ locked?: boolean;
31
+ }
32
+ type RuleConstraint = string | LockedRule;
28
33
  interface CapabilityHandoff {
29
34
  trigger: string;
30
35
  action: string;
31
36
  }
32
37
  interface ProfileCapabilities {
33
38
  tools: string[];
34
- constraints: string[];
39
+ constraints: RuleConstraint[];
35
40
  handoff: CapabilityHandoff;
36
41
  }
37
42
  interface PersonalityProfile {
@@ -60,12 +65,12 @@ interface PersonalityProfile {
60
65
  [key: string]: DimensionValue | HumorDimensionValue | undefined;
61
66
  };
62
67
  vocabulary?: VocabularyConstraints;
63
- behavioral_rules?: string[];
68
+ behavioral_rules?: RuleConstraint[];
64
69
  context_adaptations?: ContextAdaptation[];
65
70
  capabilities?: ProfileCapabilities;
66
71
  localization?: Record<string, unknown>;
67
72
  channel_adaptations?: Record<string, unknown>;
68
- extends?: string;
73
+ extends?: string | string[];
69
74
  behavioral_rules_remove?: string[];
70
75
  context_adaptations_remove?: string[];
71
76
  [key: string]: unknown;
@@ -143,6 +148,8 @@ interface ExtendsDiagnostics {
143
148
  interface ExtendsResult {
144
149
  profile: PersonalityProfile;
145
150
  parentPath: string | null;
151
+ parentPaths: string[];
152
+ parentProfile: PersonalityProfile | null;
146
153
  diagnostics: ExtendsDiagnostics;
147
154
  }
148
155
  interface ContextResolution {
@@ -189,6 +196,18 @@ declare function injectPersonality({ compiledPersonality, system, model }: {
189
196
  model?: string;
190
197
  }): string;
191
198
 
199
+ type EvalScenarioCategory = "standard" | "frustrated" | "edge" | "multi-turn" | "formal" | "casual" | "mixed";
200
+ type EvalScenarioMessage = {
201
+ role: "user" | "assistant";
202
+ content: string;
203
+ };
204
+ type EvalScenario = {
205
+ id: string;
206
+ category: EvalScenarioCategory;
207
+ domain?: string;
208
+ messages: EvalScenarioMessage[];
209
+ expected_behavior?: string;
210
+ };
192
211
  type EvalSample = {
193
212
  id?: string;
194
213
  prompt?: string;
@@ -359,4 +378,4 @@ declare function runImportAnalysis(promptText: unknown, options?: ImportOptions)
359
378
  yaml: string;
360
379
  }>;
361
380
 
362
- export { resolveActiveContext as A, resolveExtends as B, type CapabilityHandoff as C, type DimensionName as D, type EvalSample as E, runImportAnalysis as F, runTier1EvaluationForProfile as G, type HumorDimensionObject as H, type ImportOptions as I, runTier2Evaluation as J, runTier2EvaluationForProfile as K, type Level as L, runTier3Evaluation as M, runTier3EvaluationForProfile as N, validateEvalScenario as O, type PersonalityProfile as P, validateEvalScenarios as Q, validateProfile as R, validateResolvedProfile as S, type Tier1Options as T, type ValidationResult as V, type ValidationCheckSummary as a, type ValidationDiagnostic as b, type CompileOptions as c, type CompiledPersonality as d, type ContextAdaptation as e, type ContextResolution as f, type DimensionObject as g, type DimensionShorthand as h, type DimensionValue as i, type ExtendsDiagnostics as j, type ExtendsResult as k, type HumorDimensionValue as l, type HumorStyle as m, type ProfileCapabilities as n, type Tier2Options as o, type Tier3Options as p, type VocabularyConstraints as q, runTier1Evaluation as r, compileProfile as s, compileResolvedProfile as t, evaluateTier1Response as u, injectPersonality as v, loadProfileFile as w, mapImportAnalysisToProfile as x, normalizeProfile as y, renderImportedProfileYAML as z };
381
+ export { normalizeProfile as A, renderImportedProfileYAML as B, type CapabilityHandoff as C, type DimensionName as D, type EvalScenario as E, resolveActiveContext as F, resolveExtends as G, type HumorDimensionObject as H, type ImportOptions as I, runImportAnalysis as J, runTier1EvaluationForProfile as K, type Level as L, runTier2Evaluation as M, runTier2EvaluationForProfile as N, runTier3Evaluation as O, type PersonalityProfile as P, runTier3EvaluationForProfile as Q, type RuleConstraint as R, validateEvalScenario as S, type Tier1Options as T, validateEvalScenarios as U, type ValidationResult as V, validateProfile as W, validateResolvedProfile as X, type ValidationCheckSummary as a, type ValidationDiagnostic as b, type CompileOptions as c, type CompiledPersonality as d, type ContextAdaptation as e, type ContextResolution as f, type DimensionObject as g, type DimensionShorthand as h, type DimensionValue as i, type EvalSample as j, type ExtendsDiagnostics as k, type ExtendsResult as l, type HumorDimensionValue as m, type HumorStyle as n, type LockedRule as o, type ProfileCapabilities as p, type Tier2Options as q, runTier1Evaluation as r, type Tier3Options as s, type VocabularyConstraints as t, compileProfile as u, compileResolvedProfile as v, evaluateTier1Response as w, injectPersonality as x, loadProfileFile as y, mapImportAnalysisToProfile as z };
@@ -25,13 +25,18 @@ interface ContextAdaptation {
25
25
  inject?: string[];
26
26
  priority?: number;
27
27
  }
28
+ interface LockedRule {
29
+ rule: string;
30
+ locked?: boolean;
31
+ }
32
+ type RuleConstraint = string | LockedRule;
28
33
  interface CapabilityHandoff {
29
34
  trigger: string;
30
35
  action: string;
31
36
  }
32
37
  interface ProfileCapabilities {
33
38
  tools: string[];
34
- constraints: string[];
39
+ constraints: RuleConstraint[];
35
40
  handoff: CapabilityHandoff;
36
41
  }
37
42
  interface PersonalityProfile {
@@ -60,12 +65,12 @@ interface PersonalityProfile {
60
65
  [key: string]: DimensionValue | HumorDimensionValue | undefined;
61
66
  };
62
67
  vocabulary?: VocabularyConstraints;
63
- behavioral_rules?: string[];
68
+ behavioral_rules?: RuleConstraint[];
64
69
  context_adaptations?: ContextAdaptation[];
65
70
  capabilities?: ProfileCapabilities;
66
71
  localization?: Record<string, unknown>;
67
72
  channel_adaptations?: Record<string, unknown>;
68
- extends?: string;
73
+ extends?: string | string[];
69
74
  behavioral_rules_remove?: string[];
70
75
  context_adaptations_remove?: string[];
71
76
  [key: string]: unknown;
@@ -143,6 +148,8 @@ interface ExtendsDiagnostics {
143
148
  interface ExtendsResult {
144
149
  profile: PersonalityProfile;
145
150
  parentPath: string | null;
151
+ parentPaths: string[];
152
+ parentProfile: PersonalityProfile | null;
146
153
  diagnostics: ExtendsDiagnostics;
147
154
  }
148
155
  interface ContextResolution {
@@ -189,6 +196,18 @@ declare function injectPersonality({ compiledPersonality, system, model }: {
189
196
  model?: string;
190
197
  }): string;
191
198
 
199
+ type EvalScenarioCategory = "standard" | "frustrated" | "edge" | "multi-turn" | "formal" | "casual" | "mixed";
200
+ type EvalScenarioMessage = {
201
+ role: "user" | "assistant";
202
+ content: string;
203
+ };
204
+ type EvalScenario = {
205
+ id: string;
206
+ category: EvalScenarioCategory;
207
+ domain?: string;
208
+ messages: EvalScenarioMessage[];
209
+ expected_behavior?: string;
210
+ };
192
211
  type EvalSample = {
193
212
  id?: string;
194
213
  prompt?: string;
@@ -359,4 +378,4 @@ declare function runImportAnalysis(promptText: unknown, options?: ImportOptions)
359
378
  yaml: string;
360
379
  }>;
361
380
 
362
- export { resolveActiveContext as A, resolveExtends as B, type CapabilityHandoff as C, type DimensionName as D, type EvalSample as E, runImportAnalysis as F, runTier1EvaluationForProfile as G, type HumorDimensionObject as H, type ImportOptions as I, runTier2Evaluation as J, runTier2EvaluationForProfile as K, type Level as L, runTier3Evaluation as M, runTier3EvaluationForProfile as N, validateEvalScenario as O, type PersonalityProfile as P, validateEvalScenarios as Q, validateProfile as R, validateResolvedProfile as S, type Tier1Options as T, type ValidationResult as V, type ValidationCheckSummary as a, type ValidationDiagnostic as b, type CompileOptions as c, type CompiledPersonality as d, type ContextAdaptation as e, type ContextResolution as f, type DimensionObject as g, type DimensionShorthand as h, type DimensionValue as i, type ExtendsDiagnostics as j, type ExtendsResult as k, type HumorDimensionValue as l, type HumorStyle as m, type ProfileCapabilities as n, type Tier2Options as o, type Tier3Options as p, type VocabularyConstraints as q, runTier1Evaluation as r, compileProfile as s, compileResolvedProfile as t, evaluateTier1Response as u, injectPersonality as v, loadProfileFile as w, mapImportAnalysisToProfile as x, normalizeProfile as y, renderImportedProfileYAML as z };
381
+ export { normalizeProfile as A, renderImportedProfileYAML as B, type CapabilityHandoff as C, type DimensionName as D, type EvalScenario as E, resolveActiveContext as F, resolveExtends as G, type HumorDimensionObject as H, type ImportOptions as I, runImportAnalysis as J, runTier1EvaluationForProfile as K, type Level as L, runTier2Evaluation as M, runTier2EvaluationForProfile as N, runTier3Evaluation as O, type PersonalityProfile as P, runTier3EvaluationForProfile as Q, type RuleConstraint as R, validateEvalScenario as S, type Tier1Options as T, validateEvalScenarios as U, type ValidationResult as V, validateProfile as W, validateResolvedProfile as X, type ValidationCheckSummary as a, type ValidationDiagnostic as b, type CompileOptions as c, type CompiledPersonality as d, type ContextAdaptation as e, type ContextResolution as f, type DimensionObject as g, type DimensionShorthand as h, type DimensionValue as i, type EvalSample as j, type ExtendsDiagnostics as k, type ExtendsResult as l, type HumorDimensionValue as m, type HumorStyle as n, type LockedRule as o, type ProfileCapabilities as p, type Tier2Options as q, runTier1Evaluation as r, type Tier3Options as s, type VocabularyConstraints as t, compileProfile as u, compileResolvedProfile as v, evaluateTier1Response as w, injectPersonality as x, loadProfileFile as y, mapImportAnalysisToProfile as z };
package/dist/index.cjs CHANGED
@@ -102,6 +102,37 @@ function isClaudeModel(model) {
102
102
  function isGptModel(model) {
103
103
  return /gpt/i.test(String(model ?? ""));
104
104
  }
105
+ function isLockedRule(value) {
106
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
107
+ const candidate = value;
108
+ if (typeof candidate.rule !== "string" || candidate.rule.trim().length === 0) return false;
109
+ if (candidate.locked != null && typeof candidate.locked !== "boolean") return false;
110
+ return true;
111
+ }
112
+ function ruleConstraintText(entry) {
113
+ if (typeof entry === "string") {
114
+ const text = entry.trim();
115
+ return text.length > 0 ? text : null;
116
+ }
117
+ if (isLockedRule(entry)) {
118
+ return entry.rule.trim();
119
+ }
120
+ return null;
121
+ }
122
+ function normalizeRuleConstraints(value) {
123
+ const out = [];
124
+ for (const entry of asArray(value)) {
125
+ if (typeof entry === "string") {
126
+ const text = entry.trim();
127
+ if (!text) continue;
128
+ out.push({ rule: text, locked: false });
129
+ continue;
130
+ }
131
+ if (!isLockedRule(entry)) continue;
132
+ out.push({ rule: entry.rule.trim(), locked: Boolean(entry.locked) });
133
+ }
134
+ return out;
135
+ }
105
136
 
106
137
  // src/profile/merge.ts
107
138
  var PASS_THROUGH_FIELDS = /* @__PURE__ */ new Set([
@@ -117,17 +148,6 @@ var PASS_THROUGH_FIELDS = /* @__PURE__ */ new Set([
117
148
  "behavioral_rules_remove",
118
149
  "context_adaptations_remove"
119
150
  ]);
120
- function dedupExact(items) {
121
- const seen = /* @__PURE__ */ new Set();
122
- const out = [];
123
- for (const item of items) {
124
- const key = String(item);
125
- if (seen.has(key)) continue;
126
- seen.add(key);
127
- out.push(item);
128
- }
129
- return out;
130
- }
131
151
  function dedupCaseInsensitive(items) {
132
152
  const seen = /* @__PURE__ */ new Set();
133
153
  const out = [];
@@ -170,8 +190,29 @@ function mergeVocabulary(parentVocab = {}, childVocab = {}) {
170
190
  if (mergedForbidden.length) merged.forbidden_terms = mergedForbidden;
171
191
  return merged;
172
192
  }
193
+ function mergeRuleConstraints(parentRules = [], childRules = [], options) {
194
+ const out = [];
195
+ const byKey = /* @__PURE__ */ new Map();
196
+ const combined = [
197
+ ...normalizeRuleConstraints(parentRules),
198
+ ...normalizeRuleConstraints(childRules)
199
+ ];
200
+ for (const entry of combined) {
201
+ const key = options.caseInsensitive ? entry.rule.toLowerCase() : entry.rule;
202
+ const existingIndex = byKey.get(key);
203
+ if (existingIndex == null) {
204
+ byKey.set(key, out.length);
205
+ out.push({ rule: entry.rule, locked: entry.locked });
206
+ continue;
207
+ }
208
+ out[existingIndex].locked = out[existingIndex].locked || entry.locked;
209
+ }
210
+ return out.map(
211
+ (entry) => entry.locked ? { rule: entry.rule, locked: true } : entry.rule
212
+ );
213
+ }
173
214
  function mergeBehavioralRules(parentRules = [], childRules = []) {
174
- return dedupExact([...asArray(parentRules), ...asArray(childRules)]);
215
+ return mergeRuleConstraints(parentRules, childRules, { caseInsensitive: false });
175
216
  }
176
217
  function mergeContextAdaptations(parentAdaptations = [], childAdaptations = []) {
177
218
  const base = asArray(parentAdaptations).map((item) => clone(item));
@@ -203,13 +244,13 @@ function mergeCapabilities(parentCapabilities, childCapabilities) {
203
244
  ...asArray(parentCapabilities.tools),
204
245
  ...asArray(childCapabilities.tools)
205
246
  ]);
206
- const mergedConstraints = dedupCaseInsensitive([
207
- ...asArray(parentCapabilities.constraints),
208
- ...asArray(childCapabilities.constraints)
209
- ]);
210
247
  return {
211
248
  tools: mergedTools,
212
- constraints: mergedConstraints,
249
+ constraints: mergeRuleConstraints(
250
+ parentCapabilities.constraints,
251
+ childCapabilities.constraints,
252
+ { caseInsensitive: true }
253
+ ),
213
254
  handoff: {
214
255
  trigger: childCapabilities.handoff?.trigger ?? parentCapabilities.handoff?.trigger ?? "",
215
256
  action: childCapabilities.handoff?.action ?? parentCapabilities.handoff?.action ?? ""
@@ -232,9 +273,15 @@ function applyExplicitRemovals(childProfile, mergedProfile) {
232
273
  );
233
274
  const childAdaptationRemovals = asArray(childProfile.context_adaptations_remove);
234
275
  if (childBehavioralRemovals.length) {
235
- mergedProfile.behavioral_rules = asArray(mergedProfile.behavioral_rules).filter(
236
- (rule) => !childBehavioralRemovals.includes(rule)
237
- );
276
+ mergedProfile.behavioral_rules = asArray(
277
+ mergedProfile.behavioral_rules
278
+ ).filter((ruleEntry) => {
279
+ const ruleText = ruleConstraintText(ruleEntry);
280
+ if (!ruleText) return false;
281
+ if (!childBehavioralRemovals.includes(ruleText)) return true;
282
+ if (typeof ruleEntry === "object" && ruleEntry.locked === true) return true;
283
+ return false;
284
+ });
238
285
  }
239
286
  if (childForbiddenRemovals.length) {
240
287
  const nextForbidden = removeCaseInsensitive(
@@ -289,37 +336,89 @@ function mergeProfiles(parentProfile, childProfile) {
289
336
  }
290
337
 
291
338
  // src/profile/extends.ts
339
+ function normalizeExtendsTargets(value) {
340
+ if (value == null) return [];
341
+ if (typeof value === "string") {
342
+ return value.trim().length > 0 ? [value] : null;
343
+ }
344
+ if (!Array.isArray(value) || value.length === 0) return null;
345
+ const targets = [];
346
+ for (const item of value) {
347
+ if (typeof item !== "string" || item.trim().length === 0) {
348
+ return null;
349
+ }
350
+ targets.push(item);
351
+ }
352
+ return targets;
353
+ }
292
354
  function resolveExtends(profilePath, options = {}) {
293
355
  const diagnostics = { warnings: [], errors: [] };
294
356
  const childProfile = loadProfileFile(profilePath);
295
- if (!childProfile?.extends) {
357
+ const extendsTargets = normalizeExtendsTargets(childProfile?.extends);
358
+ if (!extendsTargets || extendsTargets.length === 0) {
296
359
  return {
297
360
  profile: childProfile,
298
361
  parentPath: null,
362
+ parentPaths: [],
363
+ parentProfile: null,
299
364
  diagnostics
300
365
  };
301
366
  }
302
- const parentPath = resolveParentPath(profilePath, childProfile.extends, options);
303
- if (!parentPath) {
304
- diagnostics.errors.push({
305
- code: "E_RESOLVE_EXTENDS",
306
- severity: "error",
307
- message: `Unable to resolve parent profile "${childProfile.extends}".`
308
- });
309
- return { profile: childProfile, parentPath: null, diagnostics };
367
+ const parentPaths = [];
368
+ let mergedParent = null;
369
+ for (const extendsName of extendsTargets) {
370
+ const parentPath = resolveParentPath(profilePath, extendsName, options);
371
+ if (!parentPath) {
372
+ diagnostics.errors.push({
373
+ code: "E_RESOLVE_EXTENDS",
374
+ severity: "error",
375
+ message: `Unable to resolve parent profile "${extendsName}".`
376
+ });
377
+ return {
378
+ profile: childProfile,
379
+ parentPath: parentPaths[0] ?? null,
380
+ parentPaths,
381
+ parentProfile: null,
382
+ diagnostics
383
+ };
384
+ }
385
+ const parentProfile2 = loadProfileFile(parentPath);
386
+ if (parentProfile2?.extends) {
387
+ diagnostics.errors.push({
388
+ code: "E_EXTENDS_CHAIN",
389
+ severity: "error",
390
+ message: "extends chains are not supported in MVP."
391
+ });
392
+ return {
393
+ profile: childProfile,
394
+ parentPath: parentPaths[0] ?? parentPath,
395
+ parentPaths: [...parentPaths, parentPath],
396
+ parentProfile: null,
397
+ diagnostics
398
+ };
399
+ }
400
+ parentPaths.push(parentPath);
401
+ mergedParent = mergedParent ? mergeProfiles(mergedParent, parentProfile2) : parentProfile2;
310
402
  }
311
- const parentProfile = loadProfileFile(parentPath);
312
- if (parentProfile?.extends) {
313
- diagnostics.errors.push({
314
- code: "E_EXTENDS_CHAIN",
315
- severity: "error",
316
- message: "extends chains are not supported in MVP."
317
- });
318
- return { profile: childProfile, parentPath, diagnostics };
403
+ const parentProfile = mergedParent;
404
+ if (!parentProfile) {
405
+ return {
406
+ profile: childProfile,
407
+ parentPath: null,
408
+ parentPaths: [],
409
+ parentProfile: null,
410
+ diagnostics
411
+ };
319
412
  }
320
413
  const merged = mergeProfiles(parentProfile, childProfile);
321
414
  delete merged.extends;
322
- return { profile: merged, parentPath, diagnostics };
415
+ return {
416
+ profile: merged,
417
+ parentPath: parentPaths[0] ?? null,
418
+ parentPaths,
419
+ parentProfile,
420
+ diagnostics
421
+ };
323
422
  }
324
423
 
325
424
  // src/profile/normalize.ts
@@ -364,7 +463,7 @@ function resolveActiveContext(profile, context = {}) {
364
463
 
365
464
  // src/validator/overspec.ts
366
465
  function computeConstraintCount(profile) {
367
- const behavioralRules = asArray(profile?.behavioral_rules).length;
466
+ const behavioralRules = normalizeRuleConstraints(profile?.behavioral_rules).length;
368
467
  const preferredTerms = asArray(profile?.vocabulary?.preferred_terms).length;
369
468
  const forbiddenTerms = asArray(profile?.vocabulary?.forbidden_terms).length;
370
469
  const contextAdaptations = asArray(profile?.context_adaptations).length;
@@ -406,7 +505,7 @@ function checkOverspec(profile) {
406
505
 
407
506
  // src/validator/schema.ts
408
507
  var HUMOR_STYLES = ["none", "dry", "subtle-wit", "playful"];
409
- var SUPPORTED_SCHEMAS = /* @__PURE__ */ new Set(["v1.4", "v1.5"]);
508
+ var SUPPORTED_SCHEMAS = /* @__PURE__ */ new Set(["v1.4", "v1.5", "v1.6"]);
410
509
  var TOP_LEVEL_KEYS = /* @__PURE__ */ new Set([
411
510
  "schema",
412
511
  "meta",
@@ -438,6 +537,9 @@ function isString(value) {
438
537
  function isStringArray(value) {
439
538
  return Array.isArray(value) && value.every((item) => typeof item === "string");
440
539
  }
540
+ function isNonEmptyStringArray(value) {
541
+ return Array.isArray(value) && value.length > 0 && value.every((item) => typeof item === "string" && item.trim().length > 0);
542
+ }
441
543
  function pushDiagnostic(target, code, message, location) {
442
544
  target.push({
443
545
  code,
@@ -463,6 +565,66 @@ function validateScalarField(parent, key, location, diagnostics) {
463
565
  );
464
566
  }
465
567
  }
568
+ function validateRuleConstraintArray(value, field, diagnostics, options) {
569
+ if (!Array.isArray(value)) {
570
+ pushDiagnostic(
571
+ diagnostics,
572
+ "V001",
573
+ `Expected "${field}" to be an array`,
574
+ field
575
+ );
576
+ return;
577
+ }
578
+ value.forEach((entry, idx) => {
579
+ const location = `${field}[${idx}]`;
580
+ if (typeof entry === "string") return;
581
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
582
+ pushDiagnostic(
583
+ diagnostics,
584
+ "V001",
585
+ `Expected "${location}" to be a string or { rule, locked? } object`,
586
+ location
587
+ );
588
+ return;
589
+ }
590
+ if (!options.allowObjects) {
591
+ pushDiagnostic(
592
+ diagnostics,
593
+ "V001",
594
+ `Object rule entries in "${field}" require schema version "v1.6"`,
595
+ location
596
+ );
597
+ return;
598
+ }
599
+ const ruleObject = entry;
600
+ for (const key of Object.keys(ruleObject)) {
601
+ if (key !== "rule" && key !== "locked") {
602
+ pushDiagnostic(
603
+ diagnostics,
604
+ "V001",
605
+ `Unknown key "${key}" in ${location}`,
606
+ `${location}.${key}`
607
+ );
608
+ }
609
+ }
610
+ if (!isString(ruleObject.rule)) {
611
+ pushDiagnostic(
612
+ diagnostics,
613
+ "V001",
614
+ `Expected "${location}.rule" to be a non-empty string`,
615
+ `${location}.rule`
616
+ );
617
+ }
618
+ if (ruleObject.locked != null && typeof ruleObject.locked !== "boolean") {
619
+ pushDiagnostic(
620
+ diagnostics,
621
+ "V001",
622
+ `Expected "${location}.locked" to be a boolean`,
623
+ `${location}.locked`
624
+ );
625
+ }
626
+ });
627
+ }
466
628
  function validateDimensionValue(value, dimension, location, dimensionsDiagnostics, rangeDiagnostics) {
467
629
  if (typeof value === "string") {
468
630
  if (!LEVEL_INDEX.has(value)) {
@@ -613,13 +775,25 @@ function validateSchema(profile) {
613
775
  "schema"
614
776
  );
615
777
  }
616
- if (profile.extends != null && !isString(profile.extends)) {
617
- pushDiagnostic(
618
- structureDiagnostics,
619
- "V001",
620
- `Expected "extends" to be a non-empty string`,
621
- "extends"
622
- );
778
+ if (profile.extends != null) {
779
+ const isStringExtends = isString(profile.extends);
780
+ const isArrayExtends = isNonEmptyStringArray(profile.extends);
781
+ if (!isStringExtends && !isArrayExtends) {
782
+ pushDiagnostic(
783
+ structureDiagnostics,
784
+ "V001",
785
+ `Expected "extends" to be a non-empty string or non-empty array of non-empty strings`,
786
+ "extends"
787
+ );
788
+ }
789
+ if (Array.isArray(profile.extends) && profile.schema !== "v1.6") {
790
+ pushDiagnostic(
791
+ structureDiagnostics,
792
+ "V001",
793
+ `Array "extends" requires schema version "v1.6"`,
794
+ "extends"
795
+ );
796
+ }
623
797
  }
624
798
  if (!isObject(profile.meta)) {
625
799
  pushDiagnostic(structureDiagnostics, "V001", `Missing required "meta" section`, "meta");
@@ -745,20 +919,17 @@ function validateSchema(profile) {
745
919
  }
746
920
  }
747
921
  }
748
- if (profile.behavioral_rules != null && !isStringArray(profile.behavioral_rules)) {
749
- pushDiagnostic(
750
- structureDiagnostics,
751
- "V001",
752
- `Expected "behavioral_rules" to be an array of strings`,
753
- "behavioral_rules"
754
- );
922
+ if (profile.behavioral_rules != null) {
923
+ validateRuleConstraintArray(profile.behavioral_rules, "behavioral_rules", structureDiagnostics, {
924
+ allowObjects: profile.schema === "v1.6"
925
+ });
755
926
  }
756
927
  if (profile.capabilities != null) {
757
- if (profile.schema !== "v1.5") {
928
+ if (profile.schema !== "v1.5" && profile.schema !== "v1.6") {
758
929
  pushDiagnostic(
759
930
  structureDiagnostics,
760
931
  "V001",
761
- `The "capabilities" section requires schema version "v1.5"`,
932
+ `The "capabilities" section requires schema version "v1.5" or "v1.6"`,
762
933
  "capabilities"
763
934
  );
764
935
  }
@@ -788,13 +959,20 @@ function validateSchema(profile) {
788
959
  "capabilities.tools"
789
960
  );
790
961
  }
791
- if (!isStringArray(profile.capabilities.constraints)) {
962
+ if (profile.capabilities.constraints == null) {
792
963
  pushDiagnostic(
793
964
  structureDiagnostics,
794
965
  "V001",
795
- `Expected "capabilities.constraints" to be an array of strings`,
966
+ `Expected "capabilities.constraints" to be an array`,
796
967
  "capabilities.constraints"
797
968
  );
969
+ } else {
970
+ validateRuleConstraintArray(
971
+ profile.capabilities.constraints,
972
+ "capabilities.constraints",
973
+ structureDiagnostics,
974
+ { allowObjects: profile.schema === "v1.6" }
975
+ );
798
976
  }
799
977
  if (!isObject(profile.capabilities.handoff)) {
800
978
  pushDiagnostic(
@@ -1091,7 +1269,9 @@ function collectS001Candidates(profile) {
1091
1269
  text: normalizeText(profile.identity.backstory)
1092
1270
  });
1093
1271
  }
1094
- asArray(profile?.behavioral_rules).forEach((rule, idx) => {
1272
+ asArray(profile?.behavioral_rules).forEach((ruleEntry, idx) => {
1273
+ const rule = ruleConstraintText(ruleEntry);
1274
+ if (!rule) return;
1095
1275
  candidates.push({
1096
1276
  location: `behavioral_rules[${idx}]`,
1097
1277
  text: normalizeText(rule)
@@ -1111,7 +1291,9 @@ function collectS001Candidates(profile) {
1111
1291
  }
1112
1292
  function collectS005Candidates(profile) {
1113
1293
  const candidates = [];
1114
- asArray(profile?.behavioral_rules).forEach((rule, idx) => {
1294
+ asArray(profile?.behavioral_rules).forEach((ruleEntry, idx) => {
1295
+ const rule = ruleConstraintText(ruleEntry);
1296
+ if (!rule) return;
1115
1297
  candidates.push({
1116
1298
  location: `behavioral_rules[${idx}]`,
1117
1299
  text: normalizeText(rule)
@@ -1143,7 +1325,9 @@ function collectS005Candidates(profile) {
1143
1325
  }
1144
1326
  function collectS008Candidates(profile) {
1145
1327
  const candidates = [];
1146
- asArray(profile?.behavioral_rules).forEach((rule, idx) => {
1328
+ asArray(profile?.behavioral_rules).forEach((ruleEntry, idx) => {
1329
+ const rule = ruleConstraintText(ruleEntry);
1330
+ if (!rule) return;
1147
1331
  candidates.push({
1148
1332
  location: `behavioral_rules[${idx}]`,
1149
1333
  text: normalizeText(rule)
@@ -1287,6 +1471,19 @@ function checkS006(parentProfile, childProfile, mergedProfile) {
1287
1471
  severity: "warning",
1288
1472
  message: "Explicit behavioral_rules_remove detected. Behavioral rules are safety-relevant."
1289
1473
  });
1474
+ const lockedParentRules = new Set(
1475
+ normalizeRuleConstraints(parentProfile.behavioral_rules).filter((rule) => rule.locked).map((rule) => rule.rule)
1476
+ );
1477
+ const lockedRemovals = childBehavioralRemovals.filter(
1478
+ (rule) => lockedParentRules.has(rule)
1479
+ );
1480
+ if (lockedRemovals.length > 0) {
1481
+ diagnostics.push({
1482
+ code: "S006",
1483
+ severity: "error",
1484
+ message: `behavioral_rules_remove attempted to remove locked inherited rules: ${lockedRemovals.join("; ")}`
1485
+ });
1486
+ }
1290
1487
  }
1291
1488
  if (childForbiddenRemovals.length) {
1292
1489
  diagnostics.push({
@@ -1295,9 +1492,9 @@ function checkS006(parentProfile, childProfile, mergedProfile) {
1295
1492
  message: "Explicit vocabulary.forbidden_terms_remove detected. Forbidden terms are safety-relevant."
1296
1493
  });
1297
1494
  }
1298
- const parentBehavioralCount = asArray(parentProfile.behavioral_rules).length;
1495
+ const parentBehavioralCount = normalizeRuleConstraints(parentProfile.behavioral_rules).length;
1299
1496
  const parentForbiddenCount = asArray(parentProfile?.vocabulary?.forbidden_terms).length;
1300
- const mergedBehavioralCount = asArray(mergedProfile.behavioral_rules).length;
1497
+ const mergedBehavioralCount = normalizeRuleConstraints(mergedProfile.behavioral_rules).length;
1301
1498
  const mergedForbiddenCount = asArray(mergedProfile?.vocabulary?.forbidden_terms).length;
1302
1499
  if (mergedBehavioralCount < parentBehavioralCount || mergedForbiddenCount < parentForbiddenCount) {
1303
1500
  diagnostics.push({
@@ -1467,11 +1664,10 @@ function validateProfile(profilePath, options = {}) {
1467
1664
  (diagnostic) => normalizeDiagnosticSeverity(diagnostic, "error")
1468
1665
  );
1469
1666
  let s006Diagnostics = [];
1470
- if (resolvedErrors.length === 0 && resolved.parentPath) {
1667
+ if (resolvedErrors.length === 0 && resolved.parentProfile) {
1471
1668
  try {
1472
1669
  const childProfile = loadProfileFile(profilePath);
1473
- const parentProfile = loadProfileFile(resolved.parentPath);
1474
- s006Diagnostics = checkS006(parentProfile, childProfile, resolved.profile);
1670
+ s006Diagnostics = checkS006(resolved.parentProfile, childProfile, resolved.profile);
1475
1671
  } catch (error) {
1476
1672
  s006Diagnostics = [
1477
1673
  {
@@ -1833,7 +2029,7 @@ function renderPersonalityText(profile, model, contextResolution, compileOptions
1833
2029
  lines.push(`Protected refusal terms (always available): ${PROTECTED_REFUSAL_TERMS.join("; ")}`);
1834
2030
  lines.push("");
1835
2031
  lines.push("[BEHAVIORAL RULES]");
1836
- const rules = asArray(profile.behavioral_rules);
2032
+ const rules = normalizeRuleConstraints(profile.behavioral_rules).map((entry) => entry.rule);
1837
2033
  if (rules.length === 0) {
1838
2034
  lines.push("- (none)");
1839
2035
  } else {
@@ -1841,10 +2037,12 @@ function renderPersonalityText(profile, model, contextResolution, compileOptions
1841
2037
  lines.push(`- ${rule}`);
1842
2038
  }
1843
2039
  }
1844
- if (profile.schema === "v1.5" && profile.capabilities) {
2040
+ if ((profile.schema === "v1.5" || profile.schema === "v1.6") && profile.capabilities) {
1845
2041
  const capabilities = profile.capabilities;
1846
2042
  const tools = asArray(capabilities.tools);
1847
- const constraints = asArray(capabilities.constraints);
2043
+ const constraints = normalizeRuleConstraints(capabilities.constraints).map(
2044
+ (entry) => entry.rule
2045
+ );
1848
2046
  lines.push("");
1849
2047
  lines.push("[CAPABILITY BOUNDARIES]");
1850
2048
  lines.push(
@@ -2138,7 +2336,7 @@ function evaluateTier1Response(profile, responseText, options = {}) {
2138
2336
  forbidden_matched: forbiddenMatches,
2139
2337
  pass: forbiddenMatches === 0
2140
2338
  };
2141
- const behavioralRules = asArray(profile?.behavioral_rules);
2339
+ const behavioralRules = normalizeRuleConstraints(profile?.behavioral_rules);
2142
2340
  const structureCheck = {
2143
2341
  behavioral_rule_count: behavioralRules.length,
2144
2342
  response_non_empty: response.trim().length > 0,
@@ -2820,7 +3018,9 @@ function buildJudgeUserPrompt(profile, sample) {
2820
3018
  const targets = collectVoiceTargets(profile);
2821
3019
  const preferredTerms = asArray(profile?.vocabulary?.preferred_terms);
2822
3020
  const forbiddenTerms = asArray(profile?.vocabulary?.forbidden_terms);
2823
- const behavioralRules = asArray(profile?.behavioral_rules);
3021
+ const behavioralRules = normalizeRuleConstraints(profile?.behavioral_rules).map(
3022
+ (entry) => entry.rule
3023
+ );
2824
3024
  return [
2825
3025
  `Profile: ${profile?.meta?.name ?? "unknown"}`,
2826
3026
  `Role: ${profile?.identity?.role ?? "assistant"}`,
package/dist/index.d.cts CHANGED
@@ -1 +1 @@
1
- export { C as CapabilityHandoff, c as CompileOptions, d as CompiledPersonality, e as ContextAdaptation, f as ContextResolution, D as DimensionName, g as DimensionObject, h as DimensionShorthand, i as DimensionValue, E as EvalSample, j as ExtendsDiagnostics, k as ExtendsResult, H as HumorDimensionObject, l as HumorDimensionValue, m as HumorStyle, I as ImportOptions, L as Level, P as PersonalityProfile, n as ProfileCapabilities, T as Tier1Options, o as Tier2Options, p as Tier3Options, b as ValidationDiagnostic, V as ValidationResult, q as VocabularyConstraints, s as compileProfile, t as compileResolvedProfile, u as evaluateTier1Response, v as injectPersonality, w as loadProfileFile, y as normalizeProfile, A as resolveActiveContext, B as resolveExtends, F as runImportAnalysis, r as runTier1Evaluation, G as runTier1EvaluationForProfile, J as runTier2Evaluation, K as runTier2EvaluationForProfile, M as runTier3Evaluation, N as runTier3EvaluationForProfile, O as validateEvalScenario, Q as validateEvalScenarios, R as validateProfile, S as validateResolvedProfile } from './index-1c7xQG2q.cjs';
1
+ export { C as CapabilityHandoff, c as CompileOptions, d as CompiledPersonality, e as ContextAdaptation, f as ContextResolution, D as DimensionName, g as DimensionObject, h as DimensionShorthand, i as DimensionValue, j as EvalSample, k as ExtendsDiagnostics, l as ExtendsResult, H as HumorDimensionObject, m as HumorDimensionValue, n as HumorStyle, I as ImportOptions, L as Level, o as LockedRule, P as PersonalityProfile, p as ProfileCapabilities, R as RuleConstraint, T as Tier1Options, q as Tier2Options, s as Tier3Options, b as ValidationDiagnostic, V as ValidationResult, t as VocabularyConstraints, u as compileProfile, v as compileResolvedProfile, w as evaluateTier1Response, x as injectPersonality, y as loadProfileFile, A as normalizeProfile, F as resolveActiveContext, G as resolveExtends, J as runImportAnalysis, r as runTier1Evaluation, K as runTier1EvaluationForProfile, M as runTier2Evaluation, N as runTier2EvaluationForProfile, O as runTier3Evaluation, Q as runTier3EvaluationForProfile, S as validateEvalScenario, U as validateEvalScenarios, W as validateProfile, X as validateResolvedProfile } from './index-Ct4kuPk7.cjs';