@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.
Files changed (70) hide show
  1. package/LICENSE +201 -0
  2. package/NOTICE +6 -0
  3. package/README.md +30 -0
  4. package/assets/domain-md.schema.json +1103 -0
  5. package/assets/oracle-capsule-jcs-vectors.json +57 -0
  6. package/assets/oracle-capsule-manifest-contract.md +72 -0
  7. package/assets/oracle-capsule-source-lock.schema.json +45 -0
  8. package/assets/oracle-capsule.schema.json +265 -0
  9. package/assets/rules.json +275 -0
  10. package/assets/spec.md +1434 -0
  11. package/assets/template-manifest.schema.json +57 -0
  12. package/dist/capsule/index.d.ts +75 -0
  13. package/dist/capsule/index.js +20 -0
  14. package/dist/capsule/index.js.map +1 -0
  15. package/dist/chunk-BYBC2TSS.js +597 -0
  16. package/dist/chunk-BYBC2TSS.js.map +1 -0
  17. package/dist/chunk-G7PZJYAJ.js +31826 -0
  18. package/dist/chunk-G7PZJYAJ.js.map +1 -0
  19. package/dist/chunk-R6QZRNEX.js +164 -0
  20. package/dist/chunk-R6QZRNEX.js.map +1 -0
  21. package/dist/chunk-T3STBWXP.js +46 -0
  22. package/dist/chunk-T3STBWXP.js.map +1 -0
  23. package/dist/chunk-W7MRQP7I.js +862 -0
  24. package/dist/chunk-W7MRQP7I.js.map +1 -0
  25. package/dist/chunk-XDIWMZVF.js +1150 -0
  26. package/dist/chunk-XDIWMZVF.js.map +1 -0
  27. package/dist/cli-KcUIQEiE.d.ts +171 -0
  28. package/dist/cli.d.ts +2 -0
  29. package/dist/cli.js +400 -0
  30. package/dist/cli.js.map +1 -0
  31. package/dist/index.d.ts +12 -0
  32. package/dist/index.js +83 -0
  33. package/dist/index.js.map +1 -0
  34. package/dist/resolver-B3mwnjJq.d.ts +96 -0
  35. package/dist/spec.d.ts +20 -0
  36. package/dist/spec.js +23 -0
  37. package/dist/spec.js.map +1 -0
  38. package/dist/templates/index.d.ts +7 -0
  39. package/dist/templates/index.js +24 -0
  40. package/dist/templates/index.js.map +1 -0
  41. package/dist/workers-Dr8B8a2O.d.ts +19 -0
  42. package/dist/workers.d.ts +3 -0
  43. package/dist/workers.js +31 -0
  44. package/dist/workers.js.map +1 -0
  45. package/package.json +97 -0
  46. package/src/capsule/canonical.ts +74 -0
  47. package/src/capsule/index.ts +10 -0
  48. package/src/capsule/json.ts +373 -0
  49. package/src/capsule/types.ts +63 -0
  50. package/src/capsule/validate.ts +551 -0
  51. package/src/cli.ts +405 -0
  52. package/src/compiled-validators.d.ts +7 -0
  53. package/src/compiled-validators.js +32512 -0
  54. package/src/constants.ts +250 -0
  55. package/src/diff.ts +111 -0
  56. package/src/export.ts +28 -0
  57. package/src/index.ts +36 -0
  58. package/src/lint.ts +42 -0
  59. package/src/parser.ts +174 -0
  60. package/src/sarif.ts +49 -0
  61. package/src/semantic.ts +1156 -0
  62. package/src/spec.ts +48 -0
  63. package/src/templates/errors.ts +3 -0
  64. package/src/templates/index.ts +11 -0
  65. package/src/templates/manifest.ts +94 -0
  66. package/src/templates/render.ts +435 -0
  67. package/src/templates/resolver.ts +200 -0
  68. package/src/templates/types.ts +98 -0
  69. package/src/types.ts +195 -0
  70. package/src/workers.ts +6 -0
@@ -0,0 +1,250 @@
1
+ import packageJson from '../package.json' with { type: 'json' };
2
+
3
+ /** The published package version; semantic-release sets it at release time. */
4
+ export const PACKAGE_VERSION: string = packageJson.version;
5
+ export const SPEC_VERSION = '1.0.0-rc.3';
6
+ export const SCHEMA_ID = `urn:ixo:domain-md:schema:${SPEC_VERSION}`;
7
+ export const TEMPLATE_SCHEMA_ID = `urn:ixo:domain-md:template-manifest-schema:${SPEC_VERSION}`;
8
+ export const ORACLE_CAPSULE_CONTRACT = 'ixo.earth/oracle-capsule/v0alpha1';
9
+ export const ORACLE_CAPSULE_SCHEMA_ID = 'urn:ixo:domain-md:x-oracle-capsule:manifest:0.1.0';
10
+ export const ORACLE_CAPSULE_SOURCE_LOCK_SCHEMA_ID =
11
+ 'urn:ixo:domain-md:x-oracle-capsule:source-lock:0.1.0';
12
+ export const ORACLE_CAPSULE_MEDIA_TYPE = 'application/vnd.ixo.oracle-capsule+json';
13
+ export const ORACLE_CAPSULE_SOURCE_LOCK_MEDIA_TYPE =
14
+ 'application/vnd.ixo.oracle-capsule.source-lock+json';
15
+ export const ORACLE_CAPSULE_LIMITS = {
16
+ maxBytes: 1_048_576,
17
+ maxDepth: 64,
18
+ maxNodes: 10_000,
19
+ maxScalarLength: 10_000,
20
+ maxLockedFiles: 10_000,
21
+ maxLockedBytes: 67_108_864,
22
+ } as const;
23
+ export const ORACLE_CAPSULE_EXTERNAL_CHECKS = [
24
+ 'oracle-identity-current',
25
+ 'subject-domain-current',
26
+ 'capability-current-and-unrevoked',
27
+ 'trusted-time-available',
28
+ 'private-resource-access-authorized',
29
+ 'human-review-authorized',
30
+ 'runtime-distribution-trusted',
31
+ 'receipt-signer-current-and-unrevoked',
32
+ ] as const;
33
+ export const MAX_DOMAIN_BYTES = 1024 * 1024;
34
+ export const MAX_LINKED_BYTES = 2 * 1024 * 1024;
35
+ export const MAX_YAML_DEPTH = 64;
36
+ export const MAX_YAML_NODES = 10_000;
37
+ export const MAX_SCALAR_LENGTH = 10_000;
38
+ export const EXTERNAL_CHECKS = [
39
+ {
40
+ code: 'cid-verification',
41
+ category: 'integrity',
42
+ reason: 'Resolve external bytes and verify their declared CIDs.',
43
+ },
44
+ {
45
+ code: 'did-iid-resolution',
46
+ category: 'identity',
47
+ reason: 'Resolve the current DID/IID state with the configured resolver.',
48
+ },
49
+ {
50
+ code: 'canonical-state-consistency',
51
+ category: 'identity',
52
+ reason: 'Compare the document with current fact-scoped canonical state.',
53
+ },
54
+ {
55
+ code: 'capability-revocation',
56
+ category: 'authorization',
57
+ reason: 'Verify live delegation and revocation state before acting.',
58
+ },
59
+ {
60
+ code: 'trusted-clock',
61
+ category: 'time',
62
+ reason: 'Evaluate time-bound grants against a trusted clock.',
63
+ },
64
+ {
65
+ code: 'chain-anchor',
66
+ category: 'chain',
67
+ reason: 'Verify the declared chain anchor and confirmation policy.',
68
+ },
69
+ {
70
+ code: 'constitutional-instrument-cid',
71
+ category: 'integrity',
72
+ reason: 'Verify every constitutional instrument against its declared CID and exact bytes.',
73
+ },
74
+ {
75
+ code: 'constitutional-authority-current',
76
+ category: 'authorization',
77
+ reason: 'Resolve current adoption, amendment, interpretation, and enforcement authority.',
78
+ },
79
+ {
80
+ code: 'constitutional-effective-status',
81
+ category: 'time',
82
+ reason:
83
+ 'Verify effective, suspended, and superseded status against a trusted clock and canonical state.',
84
+ },
85
+ {
86
+ code: 'constitutional-implementation-integrity',
87
+ category: 'integrity',
88
+ reason: 'Verify executable governance code and conformance-test identities.',
89
+ },
90
+ {
91
+ code: 'constitutional-enforcement-deployment',
92
+ category: 'authorization',
93
+ reason: 'Verify the declared enforcement point is deployed and authorized for this subject.',
94
+ },
95
+ {
96
+ code: 'constitutional-ai-profile-binding',
97
+ category: 'authorization',
98
+ reason:
99
+ 'Verify the active agent and model profile are bound to the declared principles and procedures.',
100
+ },
101
+ ] as const;
102
+
103
+ export const CANONICAL_SECTIONS = [
104
+ 'Overview',
105
+ 'Operating Model',
106
+ 'Authority & Control',
107
+ 'Constitutional Governance',
108
+ 'Services',
109
+ 'Resources',
110
+ 'Rights & Capabilities',
111
+ 'Claims, Evidence & Evaluation',
112
+ 'Linked Entities',
113
+ 'Accounts & Value',
114
+ 'POD, Flows & Agents',
115
+ 'Privacy & Source-of-Truth Boundaries',
116
+ 'Playbooks',
117
+ "Do's and Don'ts",
118
+ 'Changelog',
119
+ ] as const;
120
+
121
+ export const KNOWN_TOP_LEVEL_KEYS = new Set([
122
+ 'version',
123
+ 'kind',
124
+ 'conformance',
125
+ 'document_revision',
126
+ 'name',
127
+ 'description',
128
+ 'last_updated',
129
+ 'maintainers',
130
+ 'domain',
131
+ 'source_of_truth',
132
+ 'documents',
133
+ 'constitution',
134
+ 'agent_default_mode',
135
+ 'controllers',
136
+ 'services',
137
+ 'resources',
138
+ 'rights',
139
+ 'claims',
140
+ 'linked_entities',
141
+ 'accounts',
142
+ 'pods',
143
+ 'agents',
144
+ 'privacy',
145
+ 'graph_policy',
146
+ 'validation',
147
+ 'critical_do_not',
148
+ 'governance',
149
+ 'protocols',
150
+ 'asset',
151
+ 'deed',
152
+ 'protocol',
153
+ 'investment',
154
+ 'x-oracle-capsule',
155
+ ]);
156
+
157
+ export const CONSTITUTION_STATUSES = [
158
+ 'not_applicable',
159
+ 'draft',
160
+ 'adopted',
161
+ 'in_force',
162
+ 'suspended',
163
+ 'superseded',
164
+ ] as const;
165
+
166
+ export const CONSTITUTION_TYPES = [
167
+ 'con:OperationalConstitution',
168
+ 'con:PersonalConstitution',
169
+ 'con:AssetConstitution',
170
+ 'con:FinancialSubjectConstitution',
171
+ 'con:WorkConstitution',
172
+ 'con:ServiceConstitution',
173
+ 'con:OracleConstitution',
174
+ 'con:InformationSubjectConstitution',
175
+ 'con:PlaceConstitution',
176
+ 'con:BiologicalSubjectConstitution',
177
+ 'con:NetworkConstitution',
178
+ 'con:StateConstitution',
179
+ 'con:InternationalOrganizationConstitution',
180
+ 'con:OrganizationalConstitution',
181
+ 'con:CorporateConstitution',
182
+ 'con:TrustConstitution',
183
+ 'con:CooperativeConstitution',
184
+ 'con:PartnershipConstitution',
185
+ 'con:FoundationConstitution',
186
+ 'con:PublicBodyConstitution',
187
+ 'con:ProjectConstitution',
188
+ 'con:ProtocolConstitution',
189
+ 'con:DAOConstitution',
190
+ 'con:AgenticConstitution',
191
+ 'con:SchemeConstitution',
192
+ ] as const;
193
+
194
+ export const CONSTITUTIONAL_SUBJECT_TYPES = [
195
+ 'con:Person',
196
+ 'con:Organization',
197
+ 'con:Asset',
198
+ 'con:Commodity',
199
+ 'con:FinancialInstrument',
200
+ 'con:PropertyRight',
201
+ 'con:Agreement',
202
+ 'con:Deed',
203
+ 'con:Project',
204
+ 'con:Work',
205
+ 'con:Protocol',
206
+ 'con:Service',
207
+ 'con:Oracle',
208
+ 'con:Claim',
209
+ 'con:Credential',
210
+ 'con:Evidence',
211
+ 'con:Decision',
212
+ 'con:Outcome',
213
+ 'con:Place',
214
+ 'con:BiologicalEntity',
215
+ 'con:Network',
216
+ 'con:AgenticTwin',
217
+ ] as const;
218
+
219
+ export const CONSTITUTIONAL_ARCHETYPES = [
220
+ 'con:Stewarded',
221
+ 'con:Owned',
222
+ 'con:Managed',
223
+ 'con:Governed',
224
+ 'con:Regulated',
225
+ 'con:Verified',
226
+ 'con:Settled',
227
+ ] as const;
228
+
229
+ export const CONSTITUTION_INSTRUMENT_FUNCTIONS = [
230
+ 'constitutive',
231
+ 'governing',
232
+ 'amending',
233
+ 'interpretive',
234
+ 'executable',
235
+ ] as const;
236
+
237
+ export const CONSTITUTION_EXECUTION_MODES = [
238
+ 'human_interpreted',
239
+ 'machine_assisted',
240
+ 'machine_executable',
241
+ 'hybrid',
242
+ ] as const;
243
+
244
+ export const CONSTITUTION_AI_MODES = [
245
+ 'none',
246
+ 'context_only',
247
+ 'critique_and_revise',
248
+ 'policy_evaluate',
249
+ 'hybrid',
250
+ ] as const;
package/src/diff.ts ADDED
@@ -0,0 +1,111 @@
1
+ import { createHash } from 'node:crypto';
2
+
3
+ import { lint } from './lint.js';
4
+ import type {
5
+ ChangeClassification,
6
+ DiffOptions,
7
+ DiffReport,
8
+ DomainChange,
9
+ DomainRecord,
10
+ } from './types.js';
11
+
12
+ const SECURITY_PREFIXES = [
13
+ '/controllers',
14
+ '/rights',
15
+ '/accounts',
16
+ '/privacy',
17
+ '/source_of_truth',
18
+ '/constitution',
19
+ '/claims',
20
+ '/agents',
21
+ '/agent_default_mode',
22
+ '/critical_do_not',
23
+ ];
24
+ const OPERATIONAL_PREFIXES = [
25
+ '/services',
26
+ '/resources',
27
+ '/linked_entities',
28
+ '/pods',
29
+ '/graph_policy',
30
+ '/documents',
31
+ ];
32
+
33
+ function classify(path: string): ChangeClassification {
34
+ if (SECURITY_PREFIXES.some((prefix) => path.startsWith(prefix))) return 'security-sensitive';
35
+ if (OPERATIONAL_PREFIXES.some((prefix) => path.startsWith(prefix))) return 'operational';
36
+ return path.startsWith('/sections/') ? 'narrative' : 'operational';
37
+ }
38
+
39
+ function stable(value: unknown): string {
40
+ if (Array.isArray(value)) return `[${value.map(stable).join(',')}]`;
41
+ if (typeof value === 'object' && value !== null) {
42
+ return `{${Object.entries(value as DomainRecord)
43
+ .sort(([a], [b]) => a.localeCompare(b))
44
+ .map(([key, child]) => `${JSON.stringify(key)}:${stable(child)}`)
45
+ .join(',')}}`;
46
+ }
47
+ return JSON.stringify(value);
48
+ }
49
+
50
+ function compare(before: unknown, after: unknown, path: string, changes: DomainChange[]): void {
51
+ if (stable(before) === stable(after)) return;
52
+ if (
53
+ before &&
54
+ after &&
55
+ typeof before === 'object' &&
56
+ typeof after === 'object' &&
57
+ !Array.isArray(before) &&
58
+ !Array.isArray(after)
59
+ ) {
60
+ const keys = new Set([...Object.keys(before), ...Object.keys(after)]);
61
+ for (const key of [...keys].sort())
62
+ compare(
63
+ (before as DomainRecord)[key],
64
+ (after as DomainRecord)[key],
65
+ `${path}/${key}`,
66
+ changes,
67
+ );
68
+ return;
69
+ }
70
+ changes.push({ path: path || '/', classification: classify(path), before, after });
71
+ }
72
+
73
+ function sectionHashes(content: string): DomainRecord {
74
+ const report = lint(content);
75
+ return Object.fromEntries(
76
+ (report.document?.sections ?? []).map((section) => [
77
+ section.heading,
78
+ createHash('sha256').update(section.content).digest('hex'),
79
+ ]),
80
+ );
81
+ }
82
+
83
+ export function diffDomains(
84
+ beforeContent: string,
85
+ afterContent: string,
86
+ options: DiffOptions = {},
87
+ ): DiffReport {
88
+ const beforeReport = lint(beforeContent, { sourceName: 'before' });
89
+ const afterReport = lint(afterContent, { sourceName: 'after' });
90
+ const changes: DomainChange[] = [];
91
+ compare(
92
+ beforeReport.document?.frontmatter ?? {},
93
+ afterReport.document?.frontmatter ?? {},
94
+ '',
95
+ changes,
96
+ );
97
+ compare(sectionHashes(beforeContent), sectionHashes(afterContent), '/sections', changes);
98
+ const conformanceRegression =
99
+ afterReport.summary.errors > beforeReport.summary.errors ||
100
+ (beforeReport.ok && !afterReport.ok);
101
+ const requiresReview = changes.some((change) => change.classification === 'security-sensitive');
102
+ const regression = conformanceRegression || (options.failOn === 'review' && requiresReview);
103
+ return {
104
+ before: beforeReport.summary,
105
+ after: afterReport.summary,
106
+ changes,
107
+ conformanceRegression,
108
+ requiresReview,
109
+ regression,
110
+ };
111
+ }
package/src/export.ts ADDED
@@ -0,0 +1,28 @@
1
+ import { lint } from './lint.js';
2
+ import { toSarif } from './sarif.js';
3
+ import type { ExportOptions } from './types.js';
4
+
5
+ export function exportDomain(content: string, options: ExportOptions): string {
6
+ const report = lint(
7
+ content,
8
+ options.sourceName === undefined ? {} : { sourceName: options.sourceName },
9
+ );
10
+ if (options.format === 'sarif') return `${JSON.stringify(toSarif(report), null, 2)}\n`;
11
+ if (!report.ok || !report.document)
12
+ throw new Error('Cannot export a JSON model while conformance errors remain.');
13
+ return `${JSON.stringify(
14
+ {
15
+ specVersion: report.specVersion,
16
+ profile: report.profile,
17
+ frontmatter: report.document.frontmatter,
18
+ sections: report.document.sections.map(({ heading, content, location }) => ({
19
+ heading,
20
+ content,
21
+ location,
22
+ })),
23
+ note: 'This normalized model is not CID-canonical bytes.',
24
+ },
25
+ null,
26
+ 2,
27
+ )}\n`;
28
+ }
package/src/index.ts ADDED
@@ -0,0 +1,36 @@
1
+ export { diffDomains } from './diff.js';
2
+ export { exportDomain } from './export.js';
3
+ export { lint } from './lint.js';
4
+ export { parseDomain } from './parser.js';
5
+ export * from './capsule/index.js';
6
+ export {
7
+ getOracleCapsuleContract,
8
+ getOracleCapsuleJcsVectors,
9
+ getOracleCapsuleSchema,
10
+ getOracleCapsuleSourceLockSchema,
11
+ getRules,
12
+ getSchema,
13
+ getSchemaSource,
14
+ getSpecification,
15
+ getTemplateManifestSchema,
16
+ } from './spec.js';
17
+ export { renderTemplateBundle } from './templates/render.js';
18
+ export { TemplateValidationError } from './templates/errors.js';
19
+ export {
20
+ CONSTITUTIONAL_ARCHETYPES,
21
+ CONSTITUTIONAL_SUBJECT_TYPES,
22
+ CONSTITUTION_AI_MODES,
23
+ CONSTITUTION_EXECUTION_MODES,
24
+ CONSTITUTION_INSTRUMENT_FUNCTIONS,
25
+ CONSTITUTION_STATUSES,
26
+ CONSTITUTION_TYPES,
27
+ PACKAGE_VERSION,
28
+ SPEC_VERSION,
29
+ } from './constants.js';
30
+ export {
31
+ FileTemplateResolver,
32
+ HttpsTemplateResolver,
33
+ IpfsTemplateResolver,
34
+ } from './templates/resolver.js';
35
+ export type * from './types.js';
36
+ export type * from './templates/types.js';
package/src/lint.ts ADDED
@@ -0,0 +1,42 @@
1
+ import { EXTERNAL_CHECKS, PACKAGE_VERSION, SPEC_VERSION } from './constants.js';
2
+ import { parseDomain } from './parser.js';
3
+ import { getProfile, inferMode, validateSemantics } from './semantic.js';
4
+ import type { Finding, FindingSummary, LintOptions, LintReport } from './types.js';
5
+
6
+ function summarize(findings: Finding[]): FindingSummary {
7
+ return {
8
+ errors: findings.filter((item) => item.severity === 'error').length,
9
+ warnings: findings.filter((item) => item.severity === 'warning').length,
10
+ infos: findings.filter((item) => item.severity === 'info').length,
11
+ };
12
+ }
13
+
14
+ export function lint(content: string, options: LintOptions = {}): LintReport {
15
+ const parsed = parseDomain(content, options);
16
+ const findings = [...parsed.findings];
17
+ if (parsed.document)
18
+ findings.push(...validateSemantics(parsed.document, options.expectedProfile));
19
+ const summary = summarize(findings);
20
+ const profile = parsed.document ? getProfile(parsed.document.frontmatter) : null;
21
+ const externalChecksRequired =
22
+ profile === 'persisted_draft'
23
+ ? EXTERNAL_CHECKS.filter((check) => check.code === 'cid-verification')
24
+ : profile === 'anchored' || profile === 'runtime'
25
+ ? [...EXTERNAL_CHECKS]
26
+ : [];
27
+ const result: LintReport = {
28
+ tool: '@ixo/domain.md',
29
+ toolVersion: PACKAGE_VERSION,
30
+ specVersion: SPEC_VERSION,
31
+ sourceName: options.sourceName ?? '<memory>',
32
+ mode: parsed.document ? inferMode(parsed.document.frontmatter) : null,
33
+ profile,
34
+ status: summary.errors === 0 ? 'static-pass' : 'fail',
35
+ ok: summary.errors === 0,
36
+ summary,
37
+ findings,
38
+ externalChecksRequired,
39
+ };
40
+ if (parsed.document) result.document = parsed.document;
41
+ return result;
42
+ }
package/src/parser.ts ADDED
@@ -0,0 +1,174 @@
1
+ import type { Heading, Root, YAML as MdastYaml } from 'mdast';
2
+ import remarkFrontmatter from 'remark-frontmatter';
3
+ import remarkParse from 'remark-parse';
4
+ import { unified } from 'unified';
5
+ import { visit as visitTree } from 'unist-util-visit';
6
+ import { isAlias, parseDocument, visit as visitYaml } from 'yaml';
7
+
8
+ import {
9
+ MAX_DOMAIN_BYTES,
10
+ MAX_SCALAR_LENGTH,
11
+ MAX_YAML_DEPTH,
12
+ MAX_YAML_NODES,
13
+ } from './constants.js';
14
+ import type { DomainRecord, DomainSection, Finding, ParseOptions, ParseResult } from './types.js';
15
+
16
+ const FRONTMATTER = /^---[ \t]*\r?\n([\s\S]*?)\r?\n---[ \t]*(?:\r?\n|$)/;
17
+
18
+ function finding(code: string, message: string, path = '/'): Finding {
19
+ return { severity: 'error', code, message, path, location: { line: 1, column: 1 } };
20
+ }
21
+
22
+ function isRecord(value: unknown): value is DomainRecord {
23
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
24
+ }
25
+
26
+ function nodeText(value: unknown): string {
27
+ if (!isRecord(value)) return '';
28
+ if (typeof value.value === 'string') return value.value;
29
+ return Array.isArray(value.children) ? value.children.map(nodeText).join('') : '';
30
+ }
31
+
32
+ function parseFrontmatter(source: string, findings: Finding[]): DomainRecord | undefined {
33
+ const match = FRONTMATTER.exec(source);
34
+ if (!match?.[1]) {
35
+ findings.push(
36
+ finding(
37
+ 'missing-frontmatter',
38
+ 'A domain.md must begin with YAML frontmatter.',
39
+ '/frontmatter',
40
+ ),
41
+ );
42
+ return undefined;
43
+ }
44
+ const yaml = parseDocument(match[1], { prettyErrors: true, uniqueKeys: true, version: '1.2' });
45
+ if (yaml.errors.length > 0 || yaml.warnings.length > 0) {
46
+ findings.push(
47
+ finding(
48
+ 'unsafe-yaml',
49
+ [...yaml.errors, ...yaml.warnings].map((item) => item.message).join('; '),
50
+ '/frontmatter',
51
+ ),
52
+ );
53
+ return undefined;
54
+ }
55
+ const unsafe = new Set<string>();
56
+ let nodes = 0;
57
+ let depth = 0;
58
+ let scalarTooLong = false;
59
+ visitYaml(yaml, {
60
+ Node(_key, node, path) {
61
+ const candidate = node as { anchor?: string; tag?: string; value?: unknown };
62
+ nodes += 1;
63
+ depth = Math.max(depth, path.length + 1);
64
+ if (typeof candidate.value === 'string' && candidate.value.length > MAX_SCALAR_LENGTH)
65
+ scalarTooLong = true;
66
+ if (isAlias(node)) unsafe.add('alias');
67
+ if (candidate.anchor) unsafe.add('anchor');
68
+ if (candidate.tag?.startsWith('!')) unsafe.add('custom-tag');
69
+ },
70
+ Pair(_key, pair) {
71
+ const key = pair.key as { value?: unknown } | null;
72
+ if (key?.value === '<<') unsafe.add('merge-key');
73
+ },
74
+ });
75
+ if (nodes > MAX_YAML_NODES || depth > MAX_YAML_DEPTH || scalarTooLong) {
76
+ findings.push(
77
+ finding('unsafe-yaml', 'YAML exceeds the node, depth, or scalar limits.', '/frontmatter'),
78
+ );
79
+ return undefined;
80
+ }
81
+ if (unsafe.size > 0) {
82
+ findings.push(
83
+ finding(
84
+ 'unsafe-yaml',
85
+ `Forbidden YAML feature(s): ${[...unsafe].sort().join(', ')}.`,
86
+ '/frontmatter',
87
+ ),
88
+ );
89
+ return undefined;
90
+ }
91
+ let value: unknown;
92
+ try {
93
+ value = yaml.toJS({ maxAliasCount: 0 });
94
+ } catch (error) {
95
+ findings.push(
96
+ finding(
97
+ 'unsafe-yaml',
98
+ error instanceof Error ? error.message : String(error),
99
+ '/frontmatter',
100
+ ),
101
+ );
102
+ return undefined;
103
+ }
104
+ if (!isRecord(value)) {
105
+ findings.push(
106
+ finding('frontmatter-shape', 'YAML frontmatter must be a mapping.', '/frontmatter'),
107
+ );
108
+ return undefined;
109
+ }
110
+ return value;
111
+ }
112
+
113
+ function extractSections(tree: Root, source: string): DomainSection[] {
114
+ const headings: Heading[] = [];
115
+ visitTree(tree, 'heading', (node: Heading) => {
116
+ if (node.depth === 2) headings.push(node);
117
+ });
118
+ return headings.map((heading, index) => {
119
+ const next = headings[index + 1];
120
+ const start = heading.position?.start.offset ?? 0;
121
+ const end = next?.position?.start.offset ?? source.length;
122
+ const title = nodeText(heading);
123
+ return {
124
+ heading: title,
125
+ content: source.slice(start, end).trimEnd(),
126
+ location: {
127
+ line: heading.position?.start.line ?? 1,
128
+ column: heading.position?.start.column ?? 1,
129
+ ...(heading.position?.end.line === undefined ? {} : { endLine: heading.position.end.line }),
130
+ ...(heading.position?.end.column === undefined
131
+ ? {}
132
+ : { endColumn: heading.position.end.column }),
133
+ },
134
+ };
135
+ });
136
+ }
137
+
138
+ export function parseDomain(content: string, options: ParseOptions = {}): ParseResult {
139
+ const sourceName = options.sourceName ?? '<memory>';
140
+ const findings: Finding[] = [];
141
+ const maxBytes = options.maxBytes ?? MAX_DOMAIN_BYTES;
142
+ if (Buffer.byteLength(content, 'utf8') > maxBytes) {
143
+ return { ok: false, findings: [finding('file-too-large', `Input exceeds ${maxBytes} bytes.`)] };
144
+ }
145
+ if (content.charCodeAt(0) === 0xfeff) {
146
+ findings.push(finding('encoding', 'UTF-8 byte-order marks are forbidden.'));
147
+ }
148
+ const frontmatter = parseFrontmatter(content, findings);
149
+ if (!frontmatter) return { ok: false, findings };
150
+
151
+ const tree = unified().use(remarkParse).use(remarkFrontmatter, ['yaml']).parse(content);
152
+ const yamlNodes: MdastYaml[] = [];
153
+ visitTree(tree, 'yaml', (node: MdastYaml) => yamlNodes.push(node));
154
+ const frontmatterLength = FRONTMATTER.exec(content)?.[0].length ?? 0;
155
+ const additionalFence = /(?:^|\r?\n)---[ \t]*\r?\n[\s\S]*?\r?\n---[ \t]*(?:\r?\n|$)/.test(
156
+ content.slice(frontmatterLength),
157
+ );
158
+ if (yamlNodes.length !== 1 || tree.children[0]?.type !== 'yaml' || additionalFence) {
159
+ findings.push(
160
+ finding(
161
+ 'frontmatter-fence',
162
+ 'Exactly one YAML frontmatter block must appear at the start.',
163
+ '/frontmatter',
164
+ ),
165
+ );
166
+ }
167
+ const document = {
168
+ frontmatter,
169
+ sections: extractSections(tree, content),
170
+ sourceName,
171
+ raw: content,
172
+ };
173
+ return { ok: findings.every((item) => item.severity !== 'error'), document, findings };
174
+ }
package/src/sarif.ts ADDED
@@ -0,0 +1,49 @@
1
+ import type { Finding, LintReport } from './types.js';
2
+
3
+ function level(finding: Finding): 'error' | 'warning' | 'note' {
4
+ return finding.severity === 'info' ? 'note' : finding.severity;
5
+ }
6
+
7
+ export function toSarif(report: LintReport): Record<string, unknown> {
8
+ const ruleMap = new Map(report.findings.map((finding) => [finding.code, finding]));
9
+ return {
10
+ version: '2.1.0',
11
+ $schema: 'https://json.schemastore.org/sarif-2.1.0.json',
12
+ runs: [
13
+ {
14
+ tool: {
15
+ driver: {
16
+ name: report.tool,
17
+ version: report.toolVersion,
18
+ rules: [...ruleMap.entries()].map(([id, finding]) => ({
19
+ id,
20
+ shortDescription: { text: finding.message },
21
+ })),
22
+ },
23
+ },
24
+ results: report.findings.map((finding) => ({
25
+ ruleId: finding.code,
26
+ level: level(finding),
27
+ message: { text: finding.message },
28
+ locations: [
29
+ {
30
+ physicalLocation: {
31
+ artifactLocation: { uri: report.sourceName },
32
+ region: {
33
+ startLine: finding.location.line,
34
+ startColumn: finding.location.column,
35
+ ...(finding.location.endLine === undefined
36
+ ? {}
37
+ : { endLine: finding.location.endLine }),
38
+ ...(finding.location.endColumn === undefined
39
+ ? {}
40
+ : { endColumn: finding.location.endColumn }),
41
+ },
42
+ },
43
+ },
44
+ ],
45
+ })),
46
+ },
47
+ ],
48
+ };
49
+ }