@personaxis/spec 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/generated/schemas.d.ts +5 -0
- package/dist/generated/schemas.js +7 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.js +416 -0
- package/package.json +34 -0
- package/schema/legacy/persona-0.10.schema.json +932 -0
- package/schema/memory.schema.json +48 -0
- package/schema/persona.schema.json +2095 -0
- package/schema/policy.schema.json +192 -0
- package/schema/state.schema.json +150 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @personaxis/spec — the personaxis.md spec as a package.
|
|
3
|
+
*
|
|
4
|
+
* Canonical JSON Schemas (v1.0 + the frozen 0.10 legacy schema for the 1.x
|
|
5
|
+
* read-compat window), the five-state validator with version dispatch, and the
|
|
6
|
+
* twelve universal invariants (SPEC.md §13.1). Consumed by the CLI, MCP server,
|
|
7
|
+
* SDK, and SaaS — the single source that replaces the manual byte-identical
|
|
8
|
+
* schema mirror between repos.
|
|
9
|
+
*/
|
|
10
|
+
import Ajv from "ajv";
|
|
11
|
+
import addFormats from "ajv-formats";
|
|
12
|
+
import { personaSchema as schema, personaSchemaLegacy as legacySchema } from "./generated/schemas.js";
|
|
13
|
+
const ajv = new Ajv({ allErrors: true, strict: false });
|
|
14
|
+
addFormats(ajv);
|
|
15
|
+
/** v1.0 structural validator (schema/persona.schema.json). */
|
|
16
|
+
export const validate = ajv.compile(schema);
|
|
17
|
+
/** Frozen 0.10 validator for 0.3.0–0.10.0 documents (1.x read-compat window). */
|
|
18
|
+
export const validateLegacy = ajv.compile(legacySchema);
|
|
19
|
+
/** Version dispatch: v1.0 documents validate against the v1 schema + v1 universal paths. */
|
|
20
|
+
function isV1Document(data) {
|
|
21
|
+
const sv = data.spec_version;
|
|
22
|
+
return typeof sv === "string" && sv.startsWith("1.");
|
|
23
|
+
}
|
|
24
|
+
const UNIVERSAL_HARD_LIMITS = [
|
|
25
|
+
"No claim of subjective consciousness.",
|
|
26
|
+
"No persistent memory write without policy pass.",
|
|
27
|
+
"No unauthorized identity change.",
|
|
28
|
+
];
|
|
29
|
+
const NEAR_UNIVERSAL_AUTONOMY_ENVELOPE = "role_fidelity";
|
|
30
|
+
const NEAR_UNIVERSAL_APPROVAL_POLICY = "human_for_core_changes";
|
|
31
|
+
const NEAR_UNIVERSAL_WRITE_DEFAULT = "ephemeral";
|
|
32
|
+
function asObj(value) {
|
|
33
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : undefined;
|
|
34
|
+
}
|
|
35
|
+
function asArr(value) {
|
|
36
|
+
return Array.isArray(value) ? value : undefined;
|
|
37
|
+
}
|
|
38
|
+
function asStr(value) {
|
|
39
|
+
return typeof value === "string" ? value : undefined;
|
|
40
|
+
}
|
|
41
|
+
function asNum(value) {
|
|
42
|
+
return typeof value === "number" ? value : undefined;
|
|
43
|
+
}
|
|
44
|
+
function asBool(value) {
|
|
45
|
+
return typeof value === "boolean" ? value : undefined;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* U1–U4 (SPEC.md §12.1) — apply to EVERY document. Checks are unconditional: a
|
|
49
|
+
* missing block reports the universal violation instead of silently skipping
|
|
50
|
+
* (the Ajv pass normally guarantees presence; this keeps the semantic layer
|
|
51
|
+
* sound even if the structural schema drifts).
|
|
52
|
+
*/
|
|
53
|
+
function checkConceptualUniversals(data, errors) {
|
|
54
|
+
const expectedApi = isV1Document(data) ? "personaxis.com/v1" : "persona.dev/v1";
|
|
55
|
+
if (asStr(data.apiVersion) !== expectedApi) {
|
|
56
|
+
errors.push({
|
|
57
|
+
field: "apiVersion",
|
|
58
|
+
message: `U1: apiVersion must be exactly '${expectedApi}'.`,
|
|
59
|
+
category: "FAIL_CONCEPTUAL",
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
// D9 — EXPLICIT UserPersona universals subset (not a silent bypass): U1 applies
|
|
63
|
+
// to every document. U2–U4 reference the affect/persona layers, which are MUST
|
|
64
|
+
// for AgentPersona but OPTIONAL for UserPersona (a description of a human, not
|
|
65
|
+
// an agent behavioral contract) — so for UserPersona they apply exactly when
|
|
66
|
+
// the referenced layer is declared.
|
|
67
|
+
const agent = asStr(data.kind) === "AgentPersona";
|
|
68
|
+
const affect = asObj(data.affect);
|
|
69
|
+
if (agent || affect) {
|
|
70
|
+
if (asStr(affect?.representation) !== "hybrid_dimensional_appraisal_discrete_mood") {
|
|
71
|
+
errors.push({
|
|
72
|
+
field: "affect.representation",
|
|
73
|
+
message: "U2: representation must be 'hybrid_dimensional_appraisal_discrete_mood'.",
|
|
74
|
+
category: "FAIL_CONCEPTUAL",
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
const reg = asObj(affect?.regulation_policy);
|
|
78
|
+
if (asBool(reg?.never_claim_real_feeling) !== true) {
|
|
79
|
+
errors.push({
|
|
80
|
+
field: "affect.regulation_policy.never_claim_real_feeling",
|
|
81
|
+
message: "U3: never_claim_real_feeling must be true.",
|
|
82
|
+
category: "FAIL_CONCEPTUAL",
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
const persona = asObj(data.persona);
|
|
87
|
+
if (agent || persona) {
|
|
88
|
+
const constraints = asObj(persona?.constraints);
|
|
89
|
+
if (asBool(constraints?.cannot_claim_real_emotion) !== true) {
|
|
90
|
+
errors.push({
|
|
91
|
+
field: "persona.constraints.cannot_claim_real_emotion",
|
|
92
|
+
message: "U4: persona cannot claim real emotion.",
|
|
93
|
+
category: "FAIL_CONCEPTUAL",
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* U5–U12 (SPEC.md §12.1) — apply to AgentPersona only. This kind-scoping is
|
|
100
|
+
* explicit and intentional (a UserPersona describes a human and carries no agent
|
|
101
|
+
* behavioral contract); U1–U4 above apply to every document.
|
|
102
|
+
*/
|
|
103
|
+
function checkPolicyUniversals(data, errors) {
|
|
104
|
+
const kind = asStr(data.kind);
|
|
105
|
+
if (kind !== "AgentPersona")
|
|
106
|
+
return;
|
|
107
|
+
const v1 = isV1Document(data);
|
|
108
|
+
// v1.0 renamed layer 9; read whichever the document carries.
|
|
109
|
+
const layer9Field = v1 ? "self_regulation" : "reflexive_self_regulation";
|
|
110
|
+
const character = asObj(data.character);
|
|
111
|
+
const virtues = character ? asObj(character.virtues) : undefined;
|
|
112
|
+
const honesty = virtues ? asObj(virtues.honesty) : undefined;
|
|
113
|
+
if (!honesty) {
|
|
114
|
+
errors.push({
|
|
115
|
+
field: "character.virtues.honesty",
|
|
116
|
+
message: "Universal: virtue 'honesty' is required.",
|
|
117
|
+
category: "FAIL_POLICY",
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
else if (asStr(honesty.enforcement) !== "hard") {
|
|
121
|
+
errors.push({
|
|
122
|
+
field: "character.virtues.honesty.enforcement",
|
|
123
|
+
message: "Universal: honesty.enforcement must be 'hard'.",
|
|
124
|
+
category: "FAIL_POLICY",
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
const vad = asObj(data.values_and_drives);
|
|
128
|
+
const values = vad ? asObj(vad.values) : undefined;
|
|
129
|
+
const safety = values ? asObj(values.safety) : undefined;
|
|
130
|
+
if (!safety) {
|
|
131
|
+
errors.push({
|
|
132
|
+
field: "values_and_drives.values.safety",
|
|
133
|
+
message: "Universal: value 'safety' is required.",
|
|
134
|
+
category: "FAIL_POLICY",
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
const weight = asNum(safety.weight);
|
|
139
|
+
if (weight === undefined || weight < 0.9) {
|
|
140
|
+
errors.push({
|
|
141
|
+
field: "values_and_drives.values.safety.weight",
|
|
142
|
+
message: "Universal: safety.weight must be >= 0.90.",
|
|
143
|
+
category: "FAIL_POLICY",
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
if (asStr(safety.type) !== "governance") {
|
|
147
|
+
errors.push({
|
|
148
|
+
field: "values_and_drives.values.safety.type",
|
|
149
|
+
message: "Universal: safety.type must be 'governance'.",
|
|
150
|
+
category: "FAIL_POLICY",
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
const cr = vad ? asObj(vad.conflict_resolution) : undefined;
|
|
155
|
+
if (!cr || cr.safety_over_completion !== true) {
|
|
156
|
+
errors.push({
|
|
157
|
+
field: "values_and_drives.conflict_resolution.safety_over_completion",
|
|
158
|
+
message: "Universal: safety_over_completion must be true.",
|
|
159
|
+
category: "FAIL_POLICY",
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
const layer9 = asObj(data[layer9Field]);
|
|
163
|
+
const hardLimits = layer9 ? asArr(layer9.hard_limits) : undefined;
|
|
164
|
+
const hardLimitStrings = (hardLimits ?? []).filter((v) => typeof v === "string");
|
|
165
|
+
for (const required of UNIVERSAL_HARD_LIMITS) {
|
|
166
|
+
if (!hardLimitStrings.includes(required)) {
|
|
167
|
+
errors.push({
|
|
168
|
+
field: `${layer9Field}.hard_limits`,
|
|
169
|
+
message: `U8: universal hard_limit missing: "${required}"`,
|
|
170
|
+
category: "FAIL_POLICY",
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
// U9. v0.6+: per_layer_edit_policy lives in governance, not on the layer itself.
|
|
175
|
+
// Backward-compat: v0.5 personas with reflexive.edit_policy still validated.
|
|
176
|
+
const governance = asObj(data.governance);
|
|
177
|
+
const perLayerEditPolicy = governance ? asObj(governance.per_layer_edit_policy) : undefined;
|
|
178
|
+
const layer9EditPolicy = perLayerEditPolicy
|
|
179
|
+
? asStr(perLayerEditPolicy[layer9Field])
|
|
180
|
+
: layer9
|
|
181
|
+
? asStr(layer9.edit_policy)
|
|
182
|
+
: undefined;
|
|
183
|
+
if (layer9EditPolicy && layer9EditPolicy !== "governance_controlled") {
|
|
184
|
+
errors.push({
|
|
185
|
+
field: perLayerEditPolicy
|
|
186
|
+
? `governance.per_layer_edit_policy.${layer9Field}`
|
|
187
|
+
: `${layer9Field}.edit_policy`,
|
|
188
|
+
message: `U9: edit policy for ${layer9Field} must be 'governance_controlled'.`,
|
|
189
|
+
category: "FAIL_POLICY",
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
// v1.0 composition rule: a virtue's refs must resolve, and a HARD virtue's
|
|
193
|
+
// referenced trait envelope must not permit contradiction (its floor must stay
|
|
194
|
+
// above the low band — a trait allowed to drift to "low" cannot back a hard
|
|
195
|
+
// virtue; change the envelope or the enforcement, not the state).
|
|
196
|
+
if (v1) {
|
|
197
|
+
const character = asObj(data.character);
|
|
198
|
+
const virtues = character ? asObj(character.virtues) : undefined;
|
|
199
|
+
for (const [vName, vRaw] of Object.entries(virtues ?? {})) {
|
|
200
|
+
const v = asObj(vRaw);
|
|
201
|
+
const refs = v ? asArr(v.refs) : undefined;
|
|
202
|
+
if (!refs)
|
|
203
|
+
continue;
|
|
204
|
+
for (const refRaw of refs) {
|
|
205
|
+
const ref = asStr(refRaw);
|
|
206
|
+
if (!ref)
|
|
207
|
+
continue;
|
|
208
|
+
const parts = ref.split(".");
|
|
209
|
+
let node = data;
|
|
210
|
+
for (const part of parts) {
|
|
211
|
+
node = asObj(node)?.[part];
|
|
212
|
+
if (node === undefined)
|
|
213
|
+
break;
|
|
214
|
+
}
|
|
215
|
+
if (node === undefined) {
|
|
216
|
+
errors.push({
|
|
217
|
+
field: `character.virtues.${vName}.refs`,
|
|
218
|
+
message: `Coherence: ref '${ref}' does not resolve to a declared field.`,
|
|
219
|
+
category: "FAIL_POLICY",
|
|
220
|
+
});
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
if (asStr(v?.enforcement) === "hard" && ref.startsWith("personality.traits.")) {
|
|
224
|
+
const trait = asObj(node);
|
|
225
|
+
const range = trait ? asArr(trait.range) : undefined;
|
|
226
|
+
const bands = trait ? asObj(trait.bands) : undefined;
|
|
227
|
+
const lowMax = asNum(bands?.low_max) ?? 0.33;
|
|
228
|
+
const floor = asNum(range?.[0]);
|
|
229
|
+
if (floor !== undefined && floor <= lowMax) {
|
|
230
|
+
errors.push({
|
|
231
|
+
field: `character.virtues.${vName}.refs`,
|
|
232
|
+
message: `Coherence: hard virtue '${vName}' references '${ref}' whose envelope floor ` +
|
|
233
|
+
`(${floor}) permits the low band (≤ ${lowMax}) — a hard virtue cannot be backed ` +
|
|
234
|
+
`by a trait allowed to contradict it.`,
|
|
235
|
+
category: "FAIL_POLICY",
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
const persona = asObj(data.persona);
|
|
243
|
+
const constraints = persona ? asObj(persona.constraints) : undefined;
|
|
244
|
+
if (constraints) {
|
|
245
|
+
if (asBool(constraints.cannot_override_identity) !== true) {
|
|
246
|
+
errors.push({
|
|
247
|
+
field: "persona.constraints.cannot_override_identity",
|
|
248
|
+
message: "Universal: cannot_override_identity must be true.",
|
|
249
|
+
category: "FAIL_POLICY",
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
if (asBool(constraints.cannot_override_character) !== true) {
|
|
253
|
+
errors.push({
|
|
254
|
+
field: "persona.constraints.cannot_override_character",
|
|
255
|
+
message: "Universal: cannot_override_character must be true.",
|
|
256
|
+
category: "FAIL_POLICY",
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
const memory = asObj(data.memory);
|
|
261
|
+
const deletion = memory ? asObj(memory.deletion_policy) : undefined;
|
|
262
|
+
if (deletion && asBool(deletion.user_request_supported) !== true) {
|
|
263
|
+
errors.push({
|
|
264
|
+
field: "memory.deletion_policy.user_request_supported",
|
|
265
|
+
message: "Universal: user_request_supported must be true (privacy).",
|
|
266
|
+
category: "FAIL_POLICY",
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
const cognition = asObj(data.cognition);
|
|
270
|
+
const up = cognition ? asObj(cognition.uncertainty_policy) : undefined;
|
|
271
|
+
const disclose = up ? asNum(up.disclose_when_above) : undefined;
|
|
272
|
+
const abstain = up ? asNum(up.abstain_when_above) : undefined;
|
|
273
|
+
if (disclose !== undefined && abstain !== undefined && abstain <= disclose) {
|
|
274
|
+
errors.push({
|
|
275
|
+
field: "cognition.uncertainty_policy",
|
|
276
|
+
message: "Constraint: abstain_when_above must be strictly greater than disclose_when_above.",
|
|
277
|
+
category: "FAIL_POLICY",
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
function collectWarnings(data, warnings) {
|
|
282
|
+
const kind = asStr(data.kind);
|
|
283
|
+
if (kind !== "AgentPersona")
|
|
284
|
+
return;
|
|
285
|
+
const identity = asObj(data.identity);
|
|
286
|
+
if (identity && !asObj(identity.narrative_identity)) {
|
|
287
|
+
warnings.push({
|
|
288
|
+
field: "identity.narrative_identity",
|
|
289
|
+
message: "SHOULD: narrative_identity provides origin, self_concept, and continuity principles.",
|
|
290
|
+
category: "PASS_WITH_WARNINGS",
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
// v0.6: drift_thresholds moved to governance block (per layer).
|
|
294
|
+
// v0.5 backward-compat: personality.drift_threshold still recognised as warning anchor.
|
|
295
|
+
const personality = asObj(data.personality);
|
|
296
|
+
const governanceWarn = asObj(data.governance);
|
|
297
|
+
const driftThresholds = governanceWarn ? asObj(governanceWarn.drift_thresholds) : undefined;
|
|
298
|
+
const hasV6DriftThresholds = driftThresholds && Object.keys(driftThresholds).length > 0;
|
|
299
|
+
const hasV5DriftThreshold = personality && asNum(personality.drift_threshold) !== undefined;
|
|
300
|
+
if (!hasV6DriftThresholds && !hasV5DriftThreshold) {
|
|
301
|
+
warnings.push({
|
|
302
|
+
field: "governance.drift_thresholds",
|
|
303
|
+
message: "SHOULD: declare drift_thresholds (v0.6, per layer) or personality.drift_threshold (v0.5 legacy) to enable drift detection.",
|
|
304
|
+
category: "PASS_WITH_WARNINGS",
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
const metacognition = asObj(data.metacognition);
|
|
308
|
+
if (metacognition && !asStr(metacognition.drift_monitor)) {
|
|
309
|
+
warnings.push({
|
|
310
|
+
field: "metacognition.drift_monitor",
|
|
311
|
+
message: "SHOULD: drift_monitor describes what to observe to detect drift.",
|
|
312
|
+
category: "PASS_WITH_WARNINGS",
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
// v0.5+: `evaluation` block lives in policy.yaml, not in PERSONA.md.
|
|
316
|
+
// The v0.3-style inline `evaluation` block is still accepted for backward
|
|
317
|
+
// compatibility but no longer raises a warning when absent. The sibling
|
|
318
|
+
// policy.yaml validation handles `evaluation.required_suites` instead.
|
|
319
|
+
const governance = asObj(data.governance);
|
|
320
|
+
if (governance) {
|
|
321
|
+
if (asStr(governance.autonomy_envelope) !== NEAR_UNIVERSAL_AUTONOMY_ENVELOPE) {
|
|
322
|
+
warnings.push({
|
|
323
|
+
field: "governance.autonomy_envelope",
|
|
324
|
+
message: `NEAR-UNIVERSAL recommendation: '${NEAR_UNIVERSAL_AUTONOMY_ENVELOPE}'.`,
|
|
325
|
+
category: "PASS_WITH_WARNINGS",
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
if (asStr(governance.approval_policy) !== NEAR_UNIVERSAL_APPROVAL_POLICY) {
|
|
329
|
+
warnings.push({
|
|
330
|
+
field: "governance.approval_policy",
|
|
331
|
+
message: `NEAR-UNIVERSAL recommendation: '${NEAR_UNIVERSAL_APPROVAL_POLICY}'.`,
|
|
332
|
+
category: "PASS_WITH_WARNINGS",
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
// v0.6 update: previously NEAR-UNIVERSAL was strictly "ephemeral" for privacy.
|
|
337
|
+
// v0.6 accepts "session" as equally valid (some personas need continuity within
|
|
338
|
+
// a session without persisting across sessions). Only "persistent" triggers
|
|
339
|
+
// a warning because it has the strongest privacy implications.
|
|
340
|
+
const memory = asObj(data.memory);
|
|
341
|
+
const writePolicy = memory ? asObj(memory.write_policy) : undefined;
|
|
342
|
+
const writeDefault = writePolicy ? asStr(writePolicy.default) : undefined;
|
|
343
|
+
if (writePolicy && writeDefault === "persistent") {
|
|
344
|
+
warnings.push({
|
|
345
|
+
field: "memory.write_policy.default",
|
|
346
|
+
message: "Privacy note: write_policy.default='persistent' writes by default. NEAR-UNIVERSAL is 'ephemeral' or 'session'. Confirm consent flow and persistent_requires is set.",
|
|
347
|
+
category: "PASS_WITH_WARNINGS",
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
const vad = asObj(data.values_and_drives);
|
|
351
|
+
const drives = vad ? asObj(vad.drives) : undefined;
|
|
352
|
+
const seekApproval = drives ? asObj(drives.seek_approval_for_identity_change) : undefined;
|
|
353
|
+
if (!seekApproval || seekApproval.allowed !== true) {
|
|
354
|
+
warnings.push({
|
|
355
|
+
field: "values_and_drives.drives.seek_approval_for_identity_change",
|
|
356
|
+
message: "NEAR-UNIVERSAL: include seek_approval_for_identity_change with intensity=1.00 and allowed=true.",
|
|
357
|
+
category: "PASS_WITH_WARNINGS",
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
export function validatePersona(data) {
|
|
362
|
+
if (!data || typeof data !== "object" || Array.isArray(data)) {
|
|
363
|
+
return {
|
|
364
|
+
status: "FAIL_SCHEMA",
|
|
365
|
+
valid: false,
|
|
366
|
+
errors: [{ field: "", message: "PERSONA frontmatter is not an object.", category: "FAIL_SCHEMA" }],
|
|
367
|
+
warnings: [],
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
const obj = data;
|
|
371
|
+
// Version dispatch: 1.x documents validate against the v1.0 schema; anything
|
|
372
|
+
// else uses the frozen 0.10 schema (read-compat window; migrate 0.10-to-1.0).
|
|
373
|
+
const structural = isV1Document(obj) ? validate : validateLegacy;
|
|
374
|
+
const schemaValid = structural(obj);
|
|
375
|
+
const errors = [];
|
|
376
|
+
const warnings = [];
|
|
377
|
+
if (!schemaValid) {
|
|
378
|
+
for (const e of structural.errors ?? []) {
|
|
379
|
+
errors.push({
|
|
380
|
+
field: e.instancePath || e.schemaPath,
|
|
381
|
+
message: e.message ?? "invalid",
|
|
382
|
+
category: "FAIL_SCHEMA",
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
return { status: "FAIL_SCHEMA", valid: false, errors, warnings };
|
|
386
|
+
}
|
|
387
|
+
checkConceptualUniversals(obj, errors);
|
|
388
|
+
if (errors.some((e) => e.category === "FAIL_CONCEPTUAL")) {
|
|
389
|
+
return { status: "FAIL_CONCEPTUAL", valid: false, errors, warnings };
|
|
390
|
+
}
|
|
391
|
+
checkPolicyUniversals(obj, errors);
|
|
392
|
+
if (errors.some((e) => e.category === "FAIL_POLICY")) {
|
|
393
|
+
return { status: "FAIL_POLICY", valid: false, errors, warnings };
|
|
394
|
+
}
|
|
395
|
+
collectWarnings(obj, warnings);
|
|
396
|
+
if (warnings.length > 0) {
|
|
397
|
+
return { status: "PASS_WITH_WARNINGS", valid: true, errors, warnings };
|
|
398
|
+
}
|
|
399
|
+
return { status: "PASS", valid: true, errors, warnings };
|
|
400
|
+
}
|
|
401
|
+
export function exitCodeFor(status) {
|
|
402
|
+
switch (status) {
|
|
403
|
+
case "PASS":
|
|
404
|
+
case "PASS_WITH_WARNINGS":
|
|
405
|
+
return 0;
|
|
406
|
+
case "FAIL_SCHEMA":
|
|
407
|
+
return 1;
|
|
408
|
+
case "FAIL_POLICY":
|
|
409
|
+
return 2;
|
|
410
|
+
case "FAIL_CONCEPTUAL":
|
|
411
|
+
return 3;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
// The canonical schemas themselves (embedded at build time; usable by any consumer
|
|
415
|
+
// that needs the raw JSON Schema — e.g. server-side validation in the SaaS).
|
|
416
|
+
export { personaSchema, personaSchemaLegacy, policySchema, stateSchema, memorySchema, } from "./generated/schemas.js";
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@personaxis/spec",
|
|
3
|
+
"version": "0.11.0",
|
|
4
|
+
"description": "The personaxis.md spec as a package: canonical JSON Schemas (v1.0 + frozen 0.10 legacy), the five-state validator with version dispatch, and the twelve universal invariants. Single source consumed by the CLI, MCP server, SDK, and SaaS — replaces the manual byte-identical schema mirror.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "dist/index.js",
|
|
8
|
+
"types": "dist/index.d.ts",
|
|
9
|
+
"files": [
|
|
10
|
+
"dist",
|
|
11
|
+
"schema"
|
|
12
|
+
],
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"ajv": "^8.17.1",
|
|
15
|
+
"ajv-formats": "^3.0.1"
|
|
16
|
+
},
|
|
17
|
+
"devDependencies": {
|
|
18
|
+
"typescript": "^5.7.3",
|
|
19
|
+
"vitest": "^3.0.5"
|
|
20
|
+
},
|
|
21
|
+
"publishConfig": {
|
|
22
|
+
"access": "public"
|
|
23
|
+
},
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "git+https://github.com/personaxis/cli.git",
|
|
27
|
+
"directory": "packages/spec"
|
|
28
|
+
},
|
|
29
|
+
"scripts": {
|
|
30
|
+
"build": "node ./scripts/gen-schemas.mjs && tsc",
|
|
31
|
+
"lint": "tsc --noEmit",
|
|
32
|
+
"test": "vitest run"
|
|
33
|
+
}
|
|
34
|
+
}
|