@ixo/domain.md 0.3.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 +201 -0
- package/NOTICE +6 -0
- package/README.md +30 -0
- package/assets/domain-md.schema.json +1103 -0
- package/assets/oracle-capsule-jcs-vectors.json +57 -0
- package/assets/oracle-capsule-manifest-contract.md +72 -0
- package/assets/oracle-capsule-source-lock.schema.json +45 -0
- package/assets/oracle-capsule.schema.json +265 -0
- package/assets/rules.json +275 -0
- package/assets/spec.md +1434 -0
- package/assets/template-manifest.schema.json +57 -0
- package/dist/capsule/index.d.ts +75 -0
- package/dist/capsule/index.js +20 -0
- package/dist/capsule/index.js.map +1 -0
- package/dist/chunk-BYBC2TSS.js +597 -0
- package/dist/chunk-BYBC2TSS.js.map +1 -0
- package/dist/chunk-G7PZJYAJ.js +31826 -0
- package/dist/chunk-G7PZJYAJ.js.map +1 -0
- package/dist/chunk-R6QZRNEX.js +164 -0
- package/dist/chunk-R6QZRNEX.js.map +1 -0
- package/dist/chunk-T3STBWXP.js +46 -0
- package/dist/chunk-T3STBWXP.js.map +1 -0
- package/dist/chunk-W7MRQP7I.js +862 -0
- package/dist/chunk-W7MRQP7I.js.map +1 -0
- package/dist/chunk-XDIWMZVF.js +1150 -0
- package/dist/chunk-XDIWMZVF.js.map +1 -0
- package/dist/cli-KcUIQEiE.d.ts +171 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +400 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +83 -0
- package/dist/index.js.map +1 -0
- package/dist/resolver-B3mwnjJq.d.ts +96 -0
- package/dist/spec.d.ts +20 -0
- package/dist/spec.js +23 -0
- package/dist/spec.js.map +1 -0
- package/dist/templates/index.d.ts +7 -0
- package/dist/templates/index.js +24 -0
- package/dist/templates/index.js.map +1 -0
- package/dist/workers-Dr8B8a2O.d.ts +19 -0
- package/dist/workers.d.ts +3 -0
- package/dist/workers.js +31 -0
- package/dist/workers.js.map +1 -0
- package/package.json +97 -0
- package/src/capsule/canonical.ts +74 -0
- package/src/capsule/index.ts +10 -0
- package/src/capsule/json.ts +373 -0
- package/src/capsule/types.ts +63 -0
- package/src/capsule/validate.ts +551 -0
- package/src/cli.ts +405 -0
- package/src/compiled-validators.d.ts +7 -0
- package/src/compiled-validators.js +32512 -0
- package/src/constants.ts +250 -0
- package/src/diff.ts +111 -0
- package/src/export.ts +28 -0
- package/src/index.ts +36 -0
- package/src/lint.ts +42 -0
- package/src/parser.ts +174 -0
- package/src/sarif.ts +49 -0
- package/src/semantic.ts +1156 -0
- package/src/spec.ts +48 -0
- package/src/templates/errors.ts +3 -0
- package/src/templates/index.ts +11 -0
- package/src/templates/manifest.ts +94 -0
- package/src/templates/render.ts +435 -0
- package/src/templates/resolver.ts +200 -0
- package/src/templates/types.ts +98 -0
- package/src/types.ts +195 -0
- package/src/workers.ts +6 -0
package/src/semantic.ts
ADDED
|
@@ -0,0 +1,1156 @@
|
|
|
1
|
+
import { CANONICAL_SECTIONS, KNOWN_TOP_LEVEL_KEYS } from './constants.js';
|
|
2
|
+
import validators from './compiled-validators.js';
|
|
3
|
+
import type {
|
|
4
|
+
ConformanceProfile,
|
|
5
|
+
DomainDocument,
|
|
6
|
+
DomainMode,
|
|
7
|
+
DomainRecord,
|
|
8
|
+
Finding,
|
|
9
|
+
} from './types.js';
|
|
10
|
+
|
|
11
|
+
const AUTHOR_PLACEHOLDER = /\{\{[^{}\n]+\}\}/;
|
|
12
|
+
const PUBLISH_PLACEHOLDER = /<<FILL_AT_PUBLISH:[A-Za-z0-9_.-]+>>/;
|
|
13
|
+
const validateSchema = validators.domain;
|
|
14
|
+
|
|
15
|
+
function isRecord(value: unknown): value is DomainRecord {
|
|
16
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function records(value: unknown, key: string): DomainRecord[] {
|
|
20
|
+
return isRecord(value) && Array.isArray(value[key]) ? value[key].filter(isRecord) : [];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function add(
|
|
24
|
+
findings: Finding[],
|
|
25
|
+
severity: Finding['severity'],
|
|
26
|
+
code: string,
|
|
27
|
+
message: string,
|
|
28
|
+
path?: string,
|
|
29
|
+
): void {
|
|
30
|
+
findings.push({
|
|
31
|
+
severity,
|
|
32
|
+
code,
|
|
33
|
+
message,
|
|
34
|
+
path: path && path.length > 0 ? path : '/',
|
|
35
|
+
location: { line: 1, column: 1 },
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function locate(document: DomainDocument, finding: Finding): Finding {
|
|
40
|
+
const section = document.sections.find((candidate) => candidate.heading === finding.path);
|
|
41
|
+
if (section) return { ...finding, location: section.location };
|
|
42
|
+
const segment = finding.path.split('/').filter(Boolean).at(-1);
|
|
43
|
+
if (!segment) return finding;
|
|
44
|
+
const key = segment.replace(/~1/g, '/').replace(/~0/g, '~');
|
|
45
|
+
const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
46
|
+
const lines = document.raw.split(/\r?\n/);
|
|
47
|
+
const line = lines.findIndex((value) => new RegExp(`^\\s*(?:-\\s*)?${escaped}:`).test(value));
|
|
48
|
+
if (line < 0) return finding;
|
|
49
|
+
const column = (lines[line]?.search(/\S/) ?? 0) + 1;
|
|
50
|
+
return { ...finding, location: { line: line + 1, column } };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function uniqueIds(entries: DomainRecord[], label: string, findings: Finding[]): Set<string> {
|
|
54
|
+
const result = new Set<string>();
|
|
55
|
+
for (const entry of entries) {
|
|
56
|
+
if (typeof entry.id !== 'string') continue;
|
|
57
|
+
const id = entry.id.normalize('NFC');
|
|
58
|
+
if (result.has(id))
|
|
59
|
+
add(findings, 'error', 'duplicate-entry-id', `Duplicate ${label} id ${JSON.stringify(id)}.`);
|
|
60
|
+
result.add(id);
|
|
61
|
+
}
|
|
62
|
+
return result;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function externalReference(value: string): boolean {
|
|
66
|
+
return /^[A-Za-z][A-Za-z0-9+.-]*:/.test(value) || value.startsWith('baf');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function inspectSecrets(value: unknown, findings: Finding[], path = ''): void {
|
|
70
|
+
if (Array.isArray(value)) {
|
|
71
|
+
value.forEach((child, index) => inspectSecrets(child, findings, `${path}/${index}`));
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
if (!isRecord(value)) {
|
|
75
|
+
if (
|
|
76
|
+
typeof value === 'string' &&
|
|
77
|
+
/-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/.test(value)
|
|
78
|
+
) {
|
|
79
|
+
add(findings, 'error', 'secret-in-index', 'Private key material is forbidden.', path);
|
|
80
|
+
}
|
|
81
|
+
if (
|
|
82
|
+
typeof value === 'string' &&
|
|
83
|
+
(/:\/\/[^\s/@:]+:[^\s/@]+@/.test(value) ||
|
|
84
|
+
/[?&](?:access_token|api_key|bearer_token|secret|signature)=[^&#\s]+/i.test(value))
|
|
85
|
+
) {
|
|
86
|
+
add(
|
|
87
|
+
findings,
|
|
88
|
+
'error',
|
|
89
|
+
'secret-in-index',
|
|
90
|
+
'Credential-bearing URI content is forbidden.',
|
|
91
|
+
path,
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
for (const [key, child] of Object.entries(value)) {
|
|
97
|
+
const childPath = `${path}/${key.replace(/~/g, '~0').replace(/\//g, '~1')}`;
|
|
98
|
+
if (
|
|
99
|
+
/(^|_)(secret|private_key|seed_phrase|mnemonic|access_token|api_key|bearer_token)($|_)/i.test(
|
|
100
|
+
key,
|
|
101
|
+
)
|
|
102
|
+
) {
|
|
103
|
+
add(
|
|
104
|
+
findings,
|
|
105
|
+
'error',
|
|
106
|
+
'secret-in-index',
|
|
107
|
+
`Secret-bearing key ${JSON.stringify(key)} is forbidden.`,
|
|
108
|
+
childPath,
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
inspectSecrets(child, findings, childPath);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function validateSections(document: DomainDocument, findings: Finding[]): void {
|
|
116
|
+
const seen = new Set<string>();
|
|
117
|
+
let previous = -1;
|
|
118
|
+
for (const section of document.sections) {
|
|
119
|
+
if (seen.has(section.heading) && CANONICAL_SECTIONS.includes(section.heading as never)) {
|
|
120
|
+
add(
|
|
121
|
+
findings,
|
|
122
|
+
'error',
|
|
123
|
+
'duplicate-section',
|
|
124
|
+
`Duplicate section ${JSON.stringify(section.heading)}.`,
|
|
125
|
+
section.heading,
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
seen.add(section.heading);
|
|
129
|
+
const index = CANONICAL_SECTIONS.indexOf(section.heading as never);
|
|
130
|
+
if (index < 0)
|
|
131
|
+
add(
|
|
132
|
+
findings,
|
|
133
|
+
'info',
|
|
134
|
+
'unknown-section',
|
|
135
|
+
`Unknown section ${JSON.stringify(section.heading)} is preserved.`,
|
|
136
|
+
section.heading,
|
|
137
|
+
);
|
|
138
|
+
else if (index < previous)
|
|
139
|
+
add(
|
|
140
|
+
findings,
|
|
141
|
+
'warning',
|
|
142
|
+
'section-order',
|
|
143
|
+
`Section ${JSON.stringify(section.heading)} is out of canonical order.`,
|
|
144
|
+
section.heading,
|
|
145
|
+
);
|
|
146
|
+
else previous = index;
|
|
147
|
+
}
|
|
148
|
+
const validation = document.frontmatter.validation;
|
|
149
|
+
const required =
|
|
150
|
+
isRecord(validation) && Array.isArray(validation.required_sections)
|
|
151
|
+
? validation.required_sections.filter((item): item is string => typeof item === 'string')
|
|
152
|
+
: [];
|
|
153
|
+
for (const section of required) {
|
|
154
|
+
if (!seen.has(section))
|
|
155
|
+
add(
|
|
156
|
+
findings,
|
|
157
|
+
'error',
|
|
158
|
+
'missing-required-section',
|
|
159
|
+
`Missing required section ${JSON.stringify(section)}.`,
|
|
160
|
+
section,
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function documentEntries(value: unknown): DomainRecord[] {
|
|
166
|
+
if (Array.isArray(value)) return value.filter(isRecord);
|
|
167
|
+
return records(value, 'entries');
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function validateDocuments(frontmatter: DomainRecord, findings: Finding[]): Set<string> {
|
|
171
|
+
const entries = documentEntries(frontmatter.documents);
|
|
172
|
+
const ids = uniqueIds(entries, 'document', findings);
|
|
173
|
+
const roles = entries
|
|
174
|
+
.map((entry) => entry.role)
|
|
175
|
+
.filter((role): role is string => typeof role === 'string');
|
|
176
|
+
for (const required of ['description', 'changelog']) {
|
|
177
|
+
if (!roles.includes(required))
|
|
178
|
+
add(
|
|
179
|
+
findings,
|
|
180
|
+
'error',
|
|
181
|
+
'document-contract',
|
|
182
|
+
`Missing universal document role ${required}.`,
|
|
183
|
+
'/documents/entries',
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
const duplicates = roles.filter((role, index) => roles.indexOf(role) !== index);
|
|
187
|
+
for (const role of new Set(duplicates))
|
|
188
|
+
add(
|
|
189
|
+
findings,
|
|
190
|
+
'error',
|
|
191
|
+
'document-contract',
|
|
192
|
+
`Duplicate document role ${role}.`,
|
|
193
|
+
'/documents/entries',
|
|
194
|
+
);
|
|
195
|
+
for (const entry of entries) {
|
|
196
|
+
if (
|
|
197
|
+
entry.role === 'manifest' &&
|
|
198
|
+
(entry.category !== 'manifest' ||
|
|
199
|
+
entry.authority !== 'defining' ||
|
|
200
|
+
entry.disclosure_pass !== 3)
|
|
201
|
+
) {
|
|
202
|
+
add(
|
|
203
|
+
findings,
|
|
204
|
+
'error',
|
|
205
|
+
'document-contract',
|
|
206
|
+
'Manifest documents must be defining category manifest at disclosure pass 3.',
|
|
207
|
+
'/documents/entries',
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
if (entry.sensitivity !== 'public' && entry.access_policy === 'public') {
|
|
211
|
+
add(
|
|
212
|
+
findings,
|
|
213
|
+
'error',
|
|
214
|
+
'document-contract',
|
|
215
|
+
`Sensitive document role ${String(entry.role)} cannot be public.`,
|
|
216
|
+
'/documents/entries',
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return ids;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function validateAuthority(
|
|
224
|
+
frontmatter: DomainRecord,
|
|
225
|
+
findings: Finding[],
|
|
226
|
+
): {
|
|
227
|
+
controllers: Set<string>;
|
|
228
|
+
rights: Set<string>;
|
|
229
|
+
resources: Set<string>;
|
|
230
|
+
services: Set<string>;
|
|
231
|
+
agents: Set<string>;
|
|
232
|
+
agentControllers: Set<string>;
|
|
233
|
+
linkedEntities: Set<string>;
|
|
234
|
+
claims: Set<string>;
|
|
235
|
+
wallets: Set<string>;
|
|
236
|
+
} {
|
|
237
|
+
const controllerEntries = records(frontmatter.controllers, 'entries');
|
|
238
|
+
const controllerIds = uniqueIds(controllerEntries, 'controller', findings);
|
|
239
|
+
const summary =
|
|
240
|
+
isRecord(frontmatter.controllers) && isRecord(frontmatter.controllers.summary)
|
|
241
|
+
? frontmatter.controllers.summary
|
|
242
|
+
: undefined;
|
|
243
|
+
if (
|
|
244
|
+
typeof summary?.primary_controller === 'string' &&
|
|
245
|
+
!controllerIds.has(summary.primary_controller)
|
|
246
|
+
) {
|
|
247
|
+
add(
|
|
248
|
+
findings,
|
|
249
|
+
'error',
|
|
250
|
+
'broken-local-reference',
|
|
251
|
+
'Primary controller does not resolve.',
|
|
252
|
+
'/controllers/summary/primary_controller',
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
const rightEntries = records(frontmatter.rights, 'entries');
|
|
256
|
+
const rights = uniqueIds(rightEntries, 'right', findings);
|
|
257
|
+
const resources = uniqueIds(records(frontmatter.resources, 'entries'), 'resource', findings);
|
|
258
|
+
const services = uniqueIds(records(frontmatter.services, 'entries'), 'service', findings);
|
|
259
|
+
const linkedEntities = uniqueIds(
|
|
260
|
+
records(frontmatter.linked_entities, 'entries'),
|
|
261
|
+
'linked entity',
|
|
262
|
+
findings,
|
|
263
|
+
);
|
|
264
|
+
const claimCollections = records(frontmatter.claims, 'collections');
|
|
265
|
+
const claims = new Set(
|
|
266
|
+
[
|
|
267
|
+
...claimCollections,
|
|
268
|
+
...claimCollections.flatMap((collection) => records(collection, 'claim_types')),
|
|
269
|
+
...records(frontmatter.claims, 'linked_claims'),
|
|
270
|
+
]
|
|
271
|
+
.map((entry) => entry.id)
|
|
272
|
+
.filter((id): id is string => typeof id === 'string'),
|
|
273
|
+
);
|
|
274
|
+
const wallets = new Set(
|
|
275
|
+
records(frontmatter.accounts, 'entries').flatMap((entry) =>
|
|
276
|
+
[entry.name, entry.address].filter((value): value is string => typeof value === 'string'),
|
|
277
|
+
),
|
|
278
|
+
);
|
|
279
|
+
uniqueIds(records(frontmatter.pods, 'entries'), 'POD', findings);
|
|
280
|
+
const agents = uniqueIds(records(frontmatter.agents, 'entries'), 'agent', findings);
|
|
281
|
+
const agentControllers = new Set(
|
|
282
|
+
controllerEntries
|
|
283
|
+
.filter((entry) => entry.type === 'agent')
|
|
284
|
+
.map((entry) => entry.id)
|
|
285
|
+
.filter((id): id is string => typeof id === 'string'),
|
|
286
|
+
);
|
|
287
|
+
|
|
288
|
+
if (isRecord(frontmatter.source_of_truth)) {
|
|
289
|
+
const order = new Set(
|
|
290
|
+
Array.isArray(frontmatter.source_of_truth.conflict_resolution_order)
|
|
291
|
+
? frontmatter.source_of_truth.conflict_resolution_order.filter(
|
|
292
|
+
(item): item is string => typeof item === 'string',
|
|
293
|
+
)
|
|
294
|
+
: [],
|
|
295
|
+
);
|
|
296
|
+
for (const scope of records(frontmatter.source_of_truth, 'authority_scopes')) {
|
|
297
|
+
if (!Array.isArray(scope.sources)) continue;
|
|
298
|
+
for (const source of scope.sources) {
|
|
299
|
+
if (typeof source === 'string' && !order.has(source)) {
|
|
300
|
+
add(
|
|
301
|
+
findings,
|
|
302
|
+
'error',
|
|
303
|
+
'source-authority',
|
|
304
|
+
`Authority source ${JSON.stringify(source)} is absent from conflict_resolution_order.`,
|
|
305
|
+
'/source_of_truth/authority_scopes',
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
return {
|
|
312
|
+
controllers: controllerIds,
|
|
313
|
+
rights,
|
|
314
|
+
resources,
|
|
315
|
+
services,
|
|
316
|
+
agents,
|
|
317
|
+
agentControllers,
|
|
318
|
+
linkedEntities,
|
|
319
|
+
claims,
|
|
320
|
+
wallets,
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function stringArray(value: unknown): string[] {
|
|
325
|
+
return Array.isArray(value)
|
|
326
|
+
? value.filter((item): item is string => typeof item === 'string')
|
|
327
|
+
: [];
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function resolvesReference(value: string, ...sets: Set<string>[]): boolean {
|
|
331
|
+
return externalReference(value) || sets.some((set) => set.has(value));
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function validateConstitution(
|
|
335
|
+
frontmatter: DomainRecord,
|
|
336
|
+
documents: Set<string>,
|
|
337
|
+
controllers: Set<string>,
|
|
338
|
+
rights: Set<string>,
|
|
339
|
+
resources: Set<string>,
|
|
340
|
+
services: Set<string>,
|
|
341
|
+
agents: Set<string>,
|
|
342
|
+
agentControllers: Set<string>,
|
|
343
|
+
linkedEntities: Set<string>,
|
|
344
|
+
claims: Set<string>,
|
|
345
|
+
wallets: Set<string>,
|
|
346
|
+
findings: Finding[],
|
|
347
|
+
): void {
|
|
348
|
+
const constitution = isRecord(frontmatter.constitution) ? frontmatter.constitution : undefined;
|
|
349
|
+
if (!constitution) {
|
|
350
|
+
add(
|
|
351
|
+
findings,
|
|
352
|
+
'error',
|
|
353
|
+
'constitution-required',
|
|
354
|
+
'Every domain.md 1.0.0-rc.3 document must declare constitutional status.',
|
|
355
|
+
'/constitution',
|
|
356
|
+
);
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const domain = isRecord(frontmatter.domain) ? frontmatter.domain : undefined;
|
|
361
|
+
const subject = typeof domain?.id === 'string' ? domain.id : undefined;
|
|
362
|
+
if (subject !== undefined && constitution.subject !== subject) {
|
|
363
|
+
add(
|
|
364
|
+
findings,
|
|
365
|
+
'error',
|
|
366
|
+
'constitution-required',
|
|
367
|
+
'constitution.subject must exactly equal domain.id.',
|
|
368
|
+
'/constitution/subject',
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const subjectProfile = isRecord(constitution.subject_profile)
|
|
373
|
+
? constitution.subject_profile
|
|
374
|
+
: undefined;
|
|
375
|
+
const subjectTypes = stringArray(subjectProfile?.subject_types);
|
|
376
|
+
const archetypes = stringArray(subjectProfile?.archetypes);
|
|
377
|
+
const subjectIdentity = stringArray(subjectProfile?.identity);
|
|
378
|
+
const agenticTwins = stringArray(subjectProfile?.agentic_twins);
|
|
379
|
+
if (!subjectProfile || subjectTypes.length === 0 || subjectIdentity.length === 0) {
|
|
380
|
+
add(
|
|
381
|
+
findings,
|
|
382
|
+
'error',
|
|
383
|
+
'constitutional-subject-profile-unresolved',
|
|
384
|
+
'Every domain must classify its constitutional subject and identify its canonical identity through constitution.subject_profile.',
|
|
385
|
+
'/constitution/subject_profile',
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
if (subject !== undefined && !subjectIdentity.includes(subject)) {
|
|
389
|
+
add(
|
|
390
|
+
findings,
|
|
391
|
+
'error',
|
|
392
|
+
'constitutional-subject-profile-unresolved',
|
|
393
|
+
'constitution.subject_profile.identity must include constitution.subject.',
|
|
394
|
+
'/constitution/subject_profile/identity',
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
for (const reference of [...subjectTypes, ...archetypes]) {
|
|
398
|
+
if (!externalReference(reference)) {
|
|
399
|
+
add(
|
|
400
|
+
findings,
|
|
401
|
+
'error',
|
|
402
|
+
'constitutional-subject-profile-unresolved',
|
|
403
|
+
`Constitutional subject taxonomy reference ${reference} must be an IRI.`,
|
|
404
|
+
'/constitution/subject_profile',
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
const profileReferenceSets: Array<[string, Set<string>[]]> = [
|
|
410
|
+
['identity', [controllers, linkedEntities, resources]],
|
|
411
|
+
['purposes', [documents, resources]],
|
|
412
|
+
['interests', [documents, resources]],
|
|
413
|
+
['values', [documents, resources]],
|
|
414
|
+
['rights', [rights]],
|
|
415
|
+
['obligations', [documents, resources, rights]],
|
|
416
|
+
['capabilities', [rights, resources, services]],
|
|
417
|
+
['claims', [claims, resources]],
|
|
418
|
+
['wallets', [wallets, resources]],
|
|
419
|
+
['authorities', [controllers, linkedEntities]],
|
|
420
|
+
['memory', [resources]],
|
|
421
|
+
['evidence_policies', [documents, resources]],
|
|
422
|
+
['evaluation_policies', [documents, resources]],
|
|
423
|
+
['decision_policies', [documents, resources]],
|
|
424
|
+
['settlement_policies', [documents, resources]],
|
|
425
|
+
['governance', [documents, resources]],
|
|
426
|
+
['custodians', [controllers, linkedEntities]],
|
|
427
|
+
['stewards', [controllers, linkedEntities]],
|
|
428
|
+
['owners', [controllers, linkedEntities]],
|
|
429
|
+
['beneficiaries', [controllers, linkedEntities]],
|
|
430
|
+
['oracles', [agents, services, linkedEntities]],
|
|
431
|
+
['agentic_twins', [agents, linkedEntities]],
|
|
432
|
+
];
|
|
433
|
+
for (const [field, sets] of profileReferenceSets) {
|
|
434
|
+
for (const reference of stringArray(subjectProfile?.[field])) {
|
|
435
|
+
if (!resolvesReference(reference, ...sets)) {
|
|
436
|
+
add(
|
|
437
|
+
findings,
|
|
438
|
+
'error',
|
|
439
|
+
'constitutional-subject-profile-unresolved',
|
|
440
|
+
`Constitutional subject ${field} reference ${reference} does not resolve.`,
|
|
441
|
+
`/constitution/subject_profile/${field}`,
|
|
442
|
+
);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
const controllerSummary =
|
|
448
|
+
isRecord(frontmatter.controllers) && isRecord(frontmatter.controllers.summary)
|
|
449
|
+
? frontmatter.controllers.summary
|
|
450
|
+
: undefined;
|
|
451
|
+
const agentMode =
|
|
452
|
+
isRecord(frontmatter.agent_default_mode) &&
|
|
453
|
+
typeof frontmatter.agent_default_mode.mode === 'string'
|
|
454
|
+
? frontmatter.agent_default_mode.mode
|
|
455
|
+
: undefined;
|
|
456
|
+
const governedTypes = new Set([
|
|
457
|
+
'dao',
|
|
458
|
+
'organisation',
|
|
459
|
+
'project',
|
|
460
|
+
'protocol',
|
|
461
|
+
'marketplace',
|
|
462
|
+
'pod',
|
|
463
|
+
]);
|
|
464
|
+
const agentic =
|
|
465
|
+
agents.size > 0 ||
|
|
466
|
+
agentControllers.size > 0 ||
|
|
467
|
+
agenticTwins.length > 0 ||
|
|
468
|
+
controllerSummary?.agent_controllers_allowed === true ||
|
|
469
|
+
agentMode === 'bounded_evaluate' ||
|
|
470
|
+
agentMode === 'bounded_execute';
|
|
471
|
+
const constitutionRequired =
|
|
472
|
+
(typeof domain?.type === 'string' && governedTypes.has(domain.type)) || agentic;
|
|
473
|
+
|
|
474
|
+
if (constitution.status === 'not_applicable') {
|
|
475
|
+
const declaresConstitutionalPackage = [
|
|
476
|
+
'legal_effect',
|
|
477
|
+
'norms',
|
|
478
|
+
'instruments',
|
|
479
|
+
'governance',
|
|
480
|
+
'execution',
|
|
481
|
+
'constitutional_ai',
|
|
482
|
+
].some((field) => field in constitution);
|
|
483
|
+
if (constitutionRequired || declaresConstitutionalPackage) {
|
|
484
|
+
add(
|
|
485
|
+
findings,
|
|
486
|
+
'error',
|
|
487
|
+
'constitution-not-applicable-invalid',
|
|
488
|
+
'Only passive domains without a constitutional package, agents, agent controllers, bounded agency, or executable governance may declare constitution.status as not_applicable.',
|
|
489
|
+
'/constitution/status',
|
|
490
|
+
);
|
|
491
|
+
}
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
const legalEffect = isRecord(constitution.legal_effect) ? constitution.legal_effect : undefined;
|
|
496
|
+
const governance = isRecord(constitution.governance) ? constitution.governance : undefined;
|
|
497
|
+
const execution = isRecord(constitution.execution) ? constitution.execution : undefined;
|
|
498
|
+
const constitutionalAI = isRecord(constitution.constitutional_ai)
|
|
499
|
+
? constitution.constitutional_ai
|
|
500
|
+
: undefined;
|
|
501
|
+
const instruments = Array.isArray(constitution.instruments)
|
|
502
|
+
? constitution.instruments.filter(isRecord)
|
|
503
|
+
: [];
|
|
504
|
+
if (
|
|
505
|
+
!legalEffect ||
|
|
506
|
+
!governance ||
|
|
507
|
+
!execution ||
|
|
508
|
+
!constitutionalAI ||
|
|
509
|
+
instruments.length === 0 ||
|
|
510
|
+
stringArray(constitution.norms).length === 0
|
|
511
|
+
) {
|
|
512
|
+
add(
|
|
513
|
+
findings,
|
|
514
|
+
'error',
|
|
515
|
+
'constitution-required',
|
|
516
|
+
'A constitutional package requires legal effect, norms, instruments, governance, execution, and constitutional-AI declarations.',
|
|
517
|
+
'/constitution',
|
|
518
|
+
);
|
|
519
|
+
}
|
|
520
|
+
for (const norm of stringArray(constitution.norms)) {
|
|
521
|
+
if (!resolvesReference(norm, resources)) {
|
|
522
|
+
add(
|
|
523
|
+
findings,
|
|
524
|
+
'error',
|
|
525
|
+
'constitution-required',
|
|
526
|
+
`Constitutional norm ${norm} does not resolve.`,
|
|
527
|
+
'/constitution/norms',
|
|
528
|
+
);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
const instrumentByDocument = new Map<string, DomainRecord>();
|
|
533
|
+
for (const instrument of instruments) {
|
|
534
|
+
if (typeof instrument.document_ref !== 'string' || !documents.has(instrument.document_ref)) {
|
|
535
|
+
add(
|
|
536
|
+
findings,
|
|
537
|
+
'error',
|
|
538
|
+
'constitutional-instrument-unresolved',
|
|
539
|
+
`Constitutional instrument ${String(instrument.document_ref)} does not resolve to documents.entries[].id.`,
|
|
540
|
+
'/constitution/instruments',
|
|
541
|
+
);
|
|
542
|
+
continue;
|
|
543
|
+
}
|
|
544
|
+
instrumentByDocument.set(instrument.document_ref, instrument);
|
|
545
|
+
if (
|
|
546
|
+
typeof instrument.effective_from === 'string' &&
|
|
547
|
+
typeof instrument.effective_until === 'string' &&
|
|
548
|
+
Date.parse(instrument.effective_from) > Date.parse(instrument.effective_until)
|
|
549
|
+
) {
|
|
550
|
+
add(
|
|
551
|
+
findings,
|
|
552
|
+
'error',
|
|
553
|
+
'constitution-conflicts-canonical',
|
|
554
|
+
`Instrument ${instrument.document_ref} ends before it becomes effective.`,
|
|
555
|
+
'/constitution/instruments',
|
|
556
|
+
);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
if (
|
|
561
|
+
constitution.status === 'superseded' &&
|
|
562
|
+
instruments.some((instrument) => instrument.canonical === true)
|
|
563
|
+
) {
|
|
564
|
+
add(
|
|
565
|
+
findings,
|
|
566
|
+
'error',
|
|
567
|
+
'constitution-conflicts-canonical',
|
|
568
|
+
'A superseded constitution cannot retain a canonical instrument.',
|
|
569
|
+
'/constitution/instruments',
|
|
570
|
+
);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
for (const entry of documentEntries(frontmatter.documents)) {
|
|
574
|
+
if (typeof entry.id !== 'string' || typeof entry.supersedes !== 'string') continue;
|
|
575
|
+
const current = instrumentByDocument.get(entry.id);
|
|
576
|
+
const previous = instrumentByDocument.get(entry.supersedes);
|
|
577
|
+
if (current?.canonical === true && previous?.canonical === true) {
|
|
578
|
+
add(
|
|
579
|
+
findings,
|
|
580
|
+
'error',
|
|
581
|
+
'constitution-conflicts-canonical',
|
|
582
|
+
`Constitutional instruments ${entry.id} and ${entry.supersedes} cannot both be canonical when one supersedes the other.`,
|
|
583
|
+
'/constitution/instruments',
|
|
584
|
+
);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
if (
|
|
589
|
+
legalEffect?.status === 'verified' &&
|
|
590
|
+
(typeof legalEffect.jurisdiction !== 'string' ||
|
|
591
|
+
stringArray(legalEffect.authority_evidence).length === 0)
|
|
592
|
+
) {
|
|
593
|
+
add(
|
|
594
|
+
findings,
|
|
595
|
+
'error',
|
|
596
|
+
'constitutional-authority-unverified',
|
|
597
|
+
'Verified legal effect requires a jurisdiction and at least one authority-evidence reference.',
|
|
598
|
+
'/constitution/legal_effect',
|
|
599
|
+
);
|
|
600
|
+
}
|
|
601
|
+
for (const reference of stringArray(legalEffect?.authority_evidence)) {
|
|
602
|
+
if (!resolvesReference(reference, resources)) {
|
|
603
|
+
add(
|
|
604
|
+
findings,
|
|
605
|
+
'error',
|
|
606
|
+
'constitutional-authority-unverified',
|
|
607
|
+
`Legal authority evidence ${reference} does not resolve.`,
|
|
608
|
+
'/constitution/legal_effect/authority_evidence',
|
|
609
|
+
);
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
for (const reference of stringArray(governance?.authority_sources)) {
|
|
614
|
+
if (!resolvesReference(reference, documents, resources)) {
|
|
615
|
+
add(
|
|
616
|
+
findings,
|
|
617
|
+
'error',
|
|
618
|
+
'constitutional-authority-unverified',
|
|
619
|
+
`Constitutional authority source ${reference} does not resolve.`,
|
|
620
|
+
'/constitution/governance/authority_sources',
|
|
621
|
+
);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
for (const field of [
|
|
625
|
+
'decision_procedure',
|
|
626
|
+
'amendment_procedure',
|
|
627
|
+
'interpretation_procedure',
|
|
628
|
+
'dispute_resolution_procedure',
|
|
629
|
+
'suspension_procedure',
|
|
630
|
+
'dissolution_procedure',
|
|
631
|
+
]) {
|
|
632
|
+
const reference = governance?.[field];
|
|
633
|
+
if (typeof reference === 'string' && !resolvesReference(reference, documents, resources)) {
|
|
634
|
+
add(
|
|
635
|
+
findings,
|
|
636
|
+
'error',
|
|
637
|
+
'constitutional-authority-unverified',
|
|
638
|
+
`Constitutional ${field} ${reference} does not resolve.`,
|
|
639
|
+
`/constitution/governance/${field}`,
|
|
640
|
+
);
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
const amends = instruments.some((instrument) =>
|
|
645
|
+
stringArray(instrument.functions).includes('amending'),
|
|
646
|
+
);
|
|
647
|
+
if (
|
|
648
|
+
amends &&
|
|
649
|
+
(typeof governance?.amendment_procedure !== 'string' ||
|
|
650
|
+
stringArray(governance?.authority_sources).length === 0)
|
|
651
|
+
) {
|
|
652
|
+
add(
|
|
653
|
+
findings,
|
|
654
|
+
'error',
|
|
655
|
+
'constitutional-amendment-unapproved',
|
|
656
|
+
'An amending instrument requires an amendment procedure and at least one authority source.',
|
|
657
|
+
'/constitution/governance/amendment_procedure',
|
|
658
|
+
);
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
const executable =
|
|
662
|
+
execution?.mode === 'machine_executable' ||
|
|
663
|
+
execution?.mode === 'hybrid' ||
|
|
664
|
+
instruments.some((instrument) => stringArray(instrument.functions).includes('executable'));
|
|
665
|
+
const implementations = stringArray(execution?.implementations);
|
|
666
|
+
const conformanceTests = stringArray(execution?.conformance_tests);
|
|
667
|
+
const enforcementPoints = stringArray(execution?.enforcement_points);
|
|
668
|
+
const humanReviewGates = stringArray(execution?.human_review_required_for);
|
|
669
|
+
if (
|
|
670
|
+
executable &&
|
|
671
|
+
(implementations.length === 0 ||
|
|
672
|
+
conformanceTests.length === 0 ||
|
|
673
|
+
enforcementPoints.length === 0 ||
|
|
674
|
+
humanReviewGates.length === 0 ||
|
|
675
|
+
!['deny', 'pause_and_escalate'].includes(String(execution?.failure_policy)))
|
|
676
|
+
) {
|
|
677
|
+
add(
|
|
678
|
+
findings,
|
|
679
|
+
'error',
|
|
680
|
+
'constitutional-execution-incomplete',
|
|
681
|
+
'Executable constitutional governance requires implementations, conformance tests, enforcement points, human-review gates, and a fail-closed policy.',
|
|
682
|
+
'/constitution/execution',
|
|
683
|
+
);
|
|
684
|
+
}
|
|
685
|
+
for (const reference of [...implementations, ...conformanceTests]) {
|
|
686
|
+
if (!resolvesReference(reference, resources)) {
|
|
687
|
+
add(
|
|
688
|
+
findings,
|
|
689
|
+
'error',
|
|
690
|
+
'constitutional-execution-incomplete',
|
|
691
|
+
`Constitutional execution resource ${reference} does not resolve.`,
|
|
692
|
+
'/constitution/execution',
|
|
693
|
+
);
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
for (const reference of enforcementPoints) {
|
|
697
|
+
if (!resolvesReference(reference, services)) {
|
|
698
|
+
add(
|
|
699
|
+
findings,
|
|
700
|
+
'error',
|
|
701
|
+
'constitutional-execution-incomplete',
|
|
702
|
+
`Constitutional enforcement point ${reference} does not resolve.`,
|
|
703
|
+
'/constitution/execution/enforcement_points',
|
|
704
|
+
);
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
const aiMode = typeof constitutionalAI?.mode === 'string' ? constitutionalAI.mode : undefined;
|
|
709
|
+
if (agentic && (!aiMode || aiMode === 'none')) {
|
|
710
|
+
add(
|
|
711
|
+
findings,
|
|
712
|
+
'error',
|
|
713
|
+
'constitutional-ai-incomplete',
|
|
714
|
+
'Agentic domains require an active constitutional-AI mode.',
|
|
715
|
+
'/constitution/constitutional_ai/mode',
|
|
716
|
+
);
|
|
717
|
+
}
|
|
718
|
+
if (aiMode && aiMode !== 'none') {
|
|
719
|
+
const principles = stringArray(constitutionalAI?.principles);
|
|
720
|
+
const appliesToAgents = stringArray(constitutionalAI?.applies_to_agents);
|
|
721
|
+
const constitutionalAgentIds = new Set([...agents, ...agentControllers, ...agenticTwins]);
|
|
722
|
+
const auditRecord = constitutionalAI?.audit_record;
|
|
723
|
+
const critiqueRequired = aiMode === 'critique_and_revise' || aiMode === 'hybrid';
|
|
724
|
+
const decisionRequired = aiMode === 'policy_evaluate' || aiMode === 'hybrid';
|
|
725
|
+
if (
|
|
726
|
+
principles.length === 0 ||
|
|
727
|
+
typeof auditRecord !== 'string' ||
|
|
728
|
+
constitutionalAI?.conflict_policy !== 'canonical_authority_prevails' ||
|
|
729
|
+
(critiqueRequired &&
|
|
730
|
+
(typeof constitutionalAI.critique_procedure !== 'string' ||
|
|
731
|
+
typeof constitutionalAI.revision_procedure !== 'string')) ||
|
|
732
|
+
(decisionRequired && typeof constitutionalAI.decision_procedure !== 'string')
|
|
733
|
+
) {
|
|
734
|
+
add(
|
|
735
|
+
findings,
|
|
736
|
+
'error',
|
|
737
|
+
'constitutional-ai-incomplete',
|
|
738
|
+
'Active constitutional AI requires principles, mode-specific procedures, canonical-authority conflict policy, and an audit-record definition.',
|
|
739
|
+
'/constitution/constitutional_ai',
|
|
740
|
+
);
|
|
741
|
+
}
|
|
742
|
+
if (constitutionalAgentIds.size > 0 && appliesToAgents.length === 0) {
|
|
743
|
+
add(
|
|
744
|
+
findings,
|
|
745
|
+
'error',
|
|
746
|
+
'constitutional-ai-incomplete',
|
|
747
|
+
'Constitutional AI must identify the declared agents and agent controllers to which it applies.',
|
|
748
|
+
'/constitution/constitutional_ai/applies_to_agents',
|
|
749
|
+
);
|
|
750
|
+
}
|
|
751
|
+
for (const controller of agentControllers) {
|
|
752
|
+
if (!appliesToAgents.includes(controller)) {
|
|
753
|
+
add(
|
|
754
|
+
findings,
|
|
755
|
+
'error',
|
|
756
|
+
'constitutional-ai-incomplete',
|
|
757
|
+
`Agent controller ${controller} must be bound to the constitutional-AI principles and procedures.`,
|
|
758
|
+
'/constitution/constitutional_ai/applies_to_agents',
|
|
759
|
+
);
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
for (const twin of agenticTwins) {
|
|
763
|
+
if (!appliesToAgents.includes(twin)) {
|
|
764
|
+
add(
|
|
765
|
+
findings,
|
|
766
|
+
'error',
|
|
767
|
+
'constitutional-ai-incomplete',
|
|
768
|
+
`Agentic twin ${twin} must be bound to the subject's constitutional-AI principles and procedures.`,
|
|
769
|
+
'/constitution/constitutional_ai/applies_to_agents',
|
|
770
|
+
);
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
for (const agent of appliesToAgents) {
|
|
774
|
+
if (!constitutionalAgentIds.has(agent)) {
|
|
775
|
+
add(
|
|
776
|
+
findings,
|
|
777
|
+
'error',
|
|
778
|
+
'constitutional-ai-incomplete',
|
|
779
|
+
`Constitutional-AI subject ${agent} is not declared as an agent or agent controller.`,
|
|
780
|
+
'/constitution/constitutional_ai/applies_to_agents',
|
|
781
|
+
);
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
for (const reference of [
|
|
785
|
+
...principles,
|
|
786
|
+
constitutionalAI?.critique_procedure,
|
|
787
|
+
constitutionalAI?.revision_procedure,
|
|
788
|
+
constitutionalAI?.decision_procedure,
|
|
789
|
+
constitutionalAI?.model_profile,
|
|
790
|
+
auditRecord,
|
|
791
|
+
]) {
|
|
792
|
+
if (typeof reference === 'string' && !resolvesReference(reference, resources)) {
|
|
793
|
+
add(
|
|
794
|
+
findings,
|
|
795
|
+
'error',
|
|
796
|
+
'constitutional-ai-incomplete',
|
|
797
|
+
`Constitutional-AI resource ${reference} does not resolve.`,
|
|
798
|
+
'/constitution/constitutional_ai',
|
|
799
|
+
);
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
interface FlowIndexEntry {
|
|
806
|
+
transitions: Set<string>;
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
function validateFlows(
|
|
810
|
+
frontmatter: DomainRecord,
|
|
811
|
+
rights: Set<string>,
|
|
812
|
+
findings: Finding[],
|
|
813
|
+
): Map<string, FlowIndexEntry> {
|
|
814
|
+
const index = new Map<string, FlowIndexEntry>();
|
|
815
|
+
for (const pod of records(frontmatter.pods, 'entries')) {
|
|
816
|
+
for (const role of records(pod, 'roles')) {
|
|
817
|
+
if (!Array.isArray(role.rights)) continue;
|
|
818
|
+
for (const right of role.rights) {
|
|
819
|
+
if (typeof right === 'string' && !rights.has(right)) {
|
|
820
|
+
add(
|
|
821
|
+
findings,
|
|
822
|
+
'error',
|
|
823
|
+
'broken-local-reference',
|
|
824
|
+
`POD role ${String(role.id)} references missing right ${right}.`,
|
|
825
|
+
'/pods/entries/roles',
|
|
826
|
+
);
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
for (const flow of records(pod, 'flows')) {
|
|
831
|
+
if (typeof flow.id !== 'string') continue;
|
|
832
|
+
if (index.has(flow.id))
|
|
833
|
+
add(
|
|
834
|
+
findings,
|
|
835
|
+
'error',
|
|
836
|
+
'duplicate-entry-id',
|
|
837
|
+
`Duplicate flow id ${flow.id}.`,
|
|
838
|
+
'/pods/entries/flows',
|
|
839
|
+
);
|
|
840
|
+
const states = new Set(
|
|
841
|
+
Array.isArray(flow.states)
|
|
842
|
+
? flow.states.filter((item): item is string => typeof item === 'string')
|
|
843
|
+
: [],
|
|
844
|
+
);
|
|
845
|
+
const transitions = records(flow, 'transitions');
|
|
846
|
+
const transitionIds = uniqueIds(transitions, `transition in ${flow.id}`, findings);
|
|
847
|
+
index.set(flow.id, { transitions: transitionIds });
|
|
848
|
+
const initial = typeof flow.initial_state === 'string' ? flow.initial_state : '';
|
|
849
|
+
if (!states.has(initial))
|
|
850
|
+
add(
|
|
851
|
+
findings,
|
|
852
|
+
'error',
|
|
853
|
+
'invalid-flow',
|
|
854
|
+
`Flow ${flow.id} initial state is not declared.`,
|
|
855
|
+
'/pods/entries/flows',
|
|
856
|
+
);
|
|
857
|
+
const reachable = new Set(initial ? [initial] : []);
|
|
858
|
+
let changed = true;
|
|
859
|
+
while (changed) {
|
|
860
|
+
changed = false;
|
|
861
|
+
for (const transition of transitions) {
|
|
862
|
+
if (
|
|
863
|
+
typeof transition.from === 'string' &&
|
|
864
|
+
typeof transition.to === 'string' &&
|
|
865
|
+
reachable.has(transition.from) &&
|
|
866
|
+
!reachable.has(transition.to)
|
|
867
|
+
) {
|
|
868
|
+
reachable.add(transition.to);
|
|
869
|
+
changed = true;
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
for (const transition of transitions) {
|
|
874
|
+
if (
|
|
875
|
+
(typeof transition.from === 'string' && !states.has(transition.from)) ||
|
|
876
|
+
(typeof transition.to === 'string' && !states.has(transition.to))
|
|
877
|
+
) {
|
|
878
|
+
add(
|
|
879
|
+
findings,
|
|
880
|
+
'error',
|
|
881
|
+
'invalid-flow',
|
|
882
|
+
`Flow ${flow.id} transition ${String(transition.id)} references an unknown state.`,
|
|
883
|
+
'/pods/entries/flows',
|
|
884
|
+
);
|
|
885
|
+
}
|
|
886
|
+
if (Array.isArray(transition.actor_rights)) {
|
|
887
|
+
for (const right of transition.actor_rights) {
|
|
888
|
+
if (typeof right === 'string' && !rights.has(right))
|
|
889
|
+
add(
|
|
890
|
+
findings,
|
|
891
|
+
'error',
|
|
892
|
+
'broken-local-reference',
|
|
893
|
+
`Flow ${flow.id} references missing right ${right}.`,
|
|
894
|
+
'/pods/entries/flows',
|
|
895
|
+
);
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
const effects = Array.isArray(transition.effects) ? transition.effects.map(String) : [];
|
|
899
|
+
if (
|
|
900
|
+
effects.some((effect) =>
|
|
901
|
+
['credential', 'payment', 'mint', 'burn', 'transfer'].includes(effect),
|
|
902
|
+
) &&
|
|
903
|
+
transition.human_review !== true
|
|
904
|
+
) {
|
|
905
|
+
add(
|
|
906
|
+
findings,
|
|
907
|
+
'error',
|
|
908
|
+
'invalid-flow',
|
|
909
|
+
`Consequential transition ${flow.id}/${String(transition.id)} requires human review.`,
|
|
910
|
+
'/pods/entries/flows',
|
|
911
|
+
);
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
for (const state of states) {
|
|
915
|
+
if (!reachable.has(state))
|
|
916
|
+
add(
|
|
917
|
+
findings,
|
|
918
|
+
'error',
|
|
919
|
+
'invalid-flow',
|
|
920
|
+
`Flow ${flow.id} state ${state} is unreachable.`,
|
|
921
|
+
'/pods/entries/flows',
|
|
922
|
+
);
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
return index;
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
function validateClaims(
|
|
930
|
+
frontmatter: DomainRecord,
|
|
931
|
+
rights: Set<string>,
|
|
932
|
+
resources: Set<string>,
|
|
933
|
+
flows: Map<string, FlowIndexEntry>,
|
|
934
|
+
findings: Finding[],
|
|
935
|
+
): void {
|
|
936
|
+
const collections = records(frontmatter.claims, 'collections');
|
|
937
|
+
uniqueIds(collections, 'claim collection', findings);
|
|
938
|
+
for (const collection of collections) {
|
|
939
|
+
const claimTypes = records(collection, 'claim_types');
|
|
940
|
+
uniqueIds(claimTypes, 'claim type', findings);
|
|
941
|
+
for (const claim of claimTypes) {
|
|
942
|
+
for (const field of ['evaluator_right', 'determiner_right']) {
|
|
943
|
+
const right = claim[field];
|
|
944
|
+
if (typeof right === 'string' && !rights.has(right))
|
|
945
|
+
add(
|
|
946
|
+
findings,
|
|
947
|
+
'error',
|
|
948
|
+
'broken-local-reference',
|
|
949
|
+
`Claim ${String(claim.id)} references missing ${field} ${right}.`,
|
|
950
|
+
'/claims/collections',
|
|
951
|
+
);
|
|
952
|
+
}
|
|
953
|
+
const review = isRecord(claim.human_review_policy) ? claim.human_review_policy : undefined;
|
|
954
|
+
if (typeof review?.reviewer_right === 'string' && !rights.has(review.reviewer_right)) {
|
|
955
|
+
add(
|
|
956
|
+
findings,
|
|
957
|
+
'error',
|
|
958
|
+
'broken-local-reference',
|
|
959
|
+
`Claim ${String(claim.id)} reviewer right does not resolve.`,
|
|
960
|
+
'/claims/collections',
|
|
961
|
+
);
|
|
962
|
+
}
|
|
963
|
+
const rubric = isRecord(claim.rubric) ? claim.rubric : undefined;
|
|
964
|
+
if (
|
|
965
|
+
typeof rubric?.resource_id === 'string' &&
|
|
966
|
+
!externalReference(rubric.resource_id) &&
|
|
967
|
+
!resources.has(rubric.resource_id)
|
|
968
|
+
) {
|
|
969
|
+
add(
|
|
970
|
+
findings,
|
|
971
|
+
'error',
|
|
972
|
+
'broken-local-reference',
|
|
973
|
+
`Claim ${String(claim.id)} rubric does not resolve.`,
|
|
974
|
+
'/claims/collections',
|
|
975
|
+
);
|
|
976
|
+
}
|
|
977
|
+
for (const requirement of records(claim, 'evidence_requirements')) {
|
|
978
|
+
if (
|
|
979
|
+
typeof requirement.resource_id === 'string' &&
|
|
980
|
+
!externalReference(requirement.resource_id) &&
|
|
981
|
+
!resources.has(requirement.resource_id)
|
|
982
|
+
) {
|
|
983
|
+
add(
|
|
984
|
+
findings,
|
|
985
|
+
'error',
|
|
986
|
+
'broken-local-reference',
|
|
987
|
+
`Claim ${String(claim.id)} evidence resource does not resolve.`,
|
|
988
|
+
'/claims/collections',
|
|
989
|
+
);
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
const outcomes = new Set(Array.isArray(claim.allowed_outcomes) ? claim.allowed_outcomes : []);
|
|
993
|
+
for (const action of records(claim, 'next_actions')) {
|
|
994
|
+
if (!outcomes.has(action.outcome))
|
|
995
|
+
add(
|
|
996
|
+
findings,
|
|
997
|
+
'error',
|
|
998
|
+
'incomplete-claim-contract',
|
|
999
|
+
`Claim ${String(claim.id)} next action uses an undeclared outcome.`,
|
|
1000
|
+
'/claims/collections',
|
|
1001
|
+
);
|
|
1002
|
+
const flow = typeof action.flow_id === 'string' ? flows.get(action.flow_id) : undefined;
|
|
1003
|
+
if (!flow)
|
|
1004
|
+
add(
|
|
1005
|
+
findings,
|
|
1006
|
+
'error',
|
|
1007
|
+
'broken-local-reference',
|
|
1008
|
+
`Claim ${String(claim.id)} next-action flow does not resolve.`,
|
|
1009
|
+
'/claims/collections',
|
|
1010
|
+
);
|
|
1011
|
+
else if (typeof action.transition !== 'string' || !flow.transitions.has(action.transition))
|
|
1012
|
+
add(
|
|
1013
|
+
findings,
|
|
1014
|
+
'error',
|
|
1015
|
+
'broken-local-reference',
|
|
1016
|
+
`Claim ${String(claim.id)} next-action transition does not resolve.`,
|
|
1017
|
+
'/claims/collections',
|
|
1018
|
+
);
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
function validatePrivacy(frontmatter: DomainRecord, findings: Finding[]): void {
|
|
1025
|
+
for (const entry of [
|
|
1026
|
+
...records(frontmatter.documents, 'entries'),
|
|
1027
|
+
...records(frontmatter.resources, 'entries'),
|
|
1028
|
+
]) {
|
|
1029
|
+
if (entry.sensitivity !== 'public' && entry.access_policy === 'public') {
|
|
1030
|
+
const label =
|
|
1031
|
+
typeof entry.id === 'string'
|
|
1032
|
+
? entry.id
|
|
1033
|
+
: typeof entry.role === 'string'
|
|
1034
|
+
? entry.role
|
|
1035
|
+
: 'entry';
|
|
1036
|
+
add(
|
|
1037
|
+
findings,
|
|
1038
|
+
'error',
|
|
1039
|
+
'privacy-public-sensitive',
|
|
1040
|
+
`Sensitive ${label} cannot use public access.`,
|
|
1041
|
+
'/privacy',
|
|
1042
|
+
);
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
for (const service of records(frontmatter.services, 'entries')) {
|
|
1046
|
+
const auth = isRecord(service.auth) ? service.auth : undefined;
|
|
1047
|
+
if (service.data_classification !== 'public' && auth?.method === 'none') {
|
|
1048
|
+
add(
|
|
1049
|
+
findings,
|
|
1050
|
+
'error',
|
|
1051
|
+
'privacy-public-sensitive',
|
|
1052
|
+
`Non-public service ${String(service.id)} requires an authentication boundary.`,
|
|
1053
|
+
'/services/entries',
|
|
1054
|
+
);
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
export function inferMode(frontmatter: DomainRecord): DomainMode | null {
|
|
1060
|
+
if (!isRecord(frontmatter.domain)) return null;
|
|
1061
|
+
if (frontmatter.domain.type === 'protocol') return 'protocol';
|
|
1062
|
+
if (typeof frontmatter.domain.class === 'string') return 'derived';
|
|
1063
|
+
return 'standalone';
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
export function getProfile(frontmatter: DomainRecord): ConformanceProfile | null {
|
|
1067
|
+
return isRecord(frontmatter.conformance) && typeof frontmatter.conformance.profile === 'string'
|
|
1068
|
+
? (frontmatter.conformance.profile as ConformanceProfile)
|
|
1069
|
+
: null;
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
export function validateSemantics(
|
|
1073
|
+
document: DomainDocument,
|
|
1074
|
+
expectedProfile?: ConformanceProfile,
|
|
1075
|
+
): Finding[] {
|
|
1076
|
+
const findings: Finding[] = [];
|
|
1077
|
+
if (!validateSchema(document.frontmatter)) {
|
|
1078
|
+
for (const error of validateSchema.errors ?? []) {
|
|
1079
|
+
add(
|
|
1080
|
+
findings,
|
|
1081
|
+
'error',
|
|
1082
|
+
'schema',
|
|
1083
|
+
`${error.instancePath || '$'} ${error.message ?? 'is invalid'}.`,
|
|
1084
|
+
error.instancePath || '/',
|
|
1085
|
+
);
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
const profile = getProfile(document.frontmatter);
|
|
1089
|
+
if (expectedProfile !== undefined && profile !== expectedProfile) {
|
|
1090
|
+
add(
|
|
1091
|
+
findings,
|
|
1092
|
+
'error',
|
|
1093
|
+
'profile-mismatch',
|
|
1094
|
+
`Expected ${expectedProfile}, found ${String(profile)}.`,
|
|
1095
|
+
'/conformance/profile',
|
|
1096
|
+
);
|
|
1097
|
+
}
|
|
1098
|
+
if (AUTHOR_PLACEHOLDER.test(document.raw) || PUBLISH_PLACEHOLDER.test(document.raw)) {
|
|
1099
|
+
add(
|
|
1100
|
+
findings,
|
|
1101
|
+
'error',
|
|
1102
|
+
'template-placeholder',
|
|
1103
|
+
'Conforming domain.md output contains an unresolved placeholder.',
|
|
1104
|
+
);
|
|
1105
|
+
}
|
|
1106
|
+
for (const key of Object.keys(document.frontmatter)) {
|
|
1107
|
+
if (!KNOWN_TOP_LEVEL_KEYS.has(key) && !key.startsWith('x-'))
|
|
1108
|
+
add(
|
|
1109
|
+
findings,
|
|
1110
|
+
'warning',
|
|
1111
|
+
'unknown-top-level-key',
|
|
1112
|
+
`Unknown top-level key ${JSON.stringify(key)}.`,
|
|
1113
|
+
`/${key}`,
|
|
1114
|
+
);
|
|
1115
|
+
}
|
|
1116
|
+
inspectSecrets(document.frontmatter, findings);
|
|
1117
|
+
validateSections(document, findings);
|
|
1118
|
+
const documents = validateDocuments(document.frontmatter, findings);
|
|
1119
|
+
const {
|
|
1120
|
+
controllers,
|
|
1121
|
+
rights,
|
|
1122
|
+
resources,
|
|
1123
|
+
services,
|
|
1124
|
+
agents,
|
|
1125
|
+
agentControllers,
|
|
1126
|
+
linkedEntities,
|
|
1127
|
+
claims,
|
|
1128
|
+
wallets,
|
|
1129
|
+
} = validateAuthority(document.frontmatter, findings);
|
|
1130
|
+
validateConstitution(
|
|
1131
|
+
document.frontmatter,
|
|
1132
|
+
documents,
|
|
1133
|
+
controllers,
|
|
1134
|
+
rights,
|
|
1135
|
+
resources,
|
|
1136
|
+
services,
|
|
1137
|
+
agents,
|
|
1138
|
+
agentControllers,
|
|
1139
|
+
linkedEntities,
|
|
1140
|
+
claims,
|
|
1141
|
+
wallets,
|
|
1142
|
+
findings,
|
|
1143
|
+
);
|
|
1144
|
+
const flows = validateFlows(document.frontmatter, rights, findings);
|
|
1145
|
+
validateClaims(document.frontmatter, rights, resources, flows, findings);
|
|
1146
|
+
validatePrivacy(document.frontmatter, findings);
|
|
1147
|
+
if (profile === 'anchored' || profile === 'runtime') {
|
|
1148
|
+
add(
|
|
1149
|
+
findings,
|
|
1150
|
+
'info',
|
|
1151
|
+
'runtime-external-checks-required',
|
|
1152
|
+
'Static validation cannot prove all anchored or runtime checks.',
|
|
1153
|
+
);
|
|
1154
|
+
}
|
|
1155
|
+
return findings.map((finding) => locate(document, finding));
|
|
1156
|
+
}
|