@sorane/okf 0.2.1 → 0.2.7

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sorane/okf",
3
- "version": "0.2.1",
3
+ "version": "0.2.7",
4
4
  "description": "Open Knowledge Format parsing, validation, and serialization for sorane",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -0,0 +1,171 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "https://sorane.dev/profile/sorane-okf/0.3",
4
+ "title": "sorane-okf/0.3",
5
+ "description": "OKF v0.1 profile for sorane SSG: extended types, open-data metadata, AI disclosure.",
6
+ "oneOf": [
7
+ { "$ref": "#/$defs/article" },
8
+ { "$ref": "#/$defs/index" },
9
+ { "$ref": "#/$defs/dataset" },
10
+ { "$ref": "#/$defs/reference" },
11
+ { "$ref": "#/$defs/glossary" },
12
+ { "$ref": "#/$defs/faq" }
13
+ ],
14
+ "$defs": {
15
+ "publisher": {
16
+ "type": "object",
17
+ "required": ["name"],
18
+ "properties": {
19
+ "name": { "type": "string", "minLength": 1 },
20
+ "url": { "type": "string", "minLength": 1 },
21
+ "email": { "type": "string", "minLength": 1 }
22
+ },
23
+ "additionalProperties": false
24
+ },
25
+ "distribution": {
26
+ "type": "object",
27
+ "required": ["title", "format", "accessURL"],
28
+ "properties": {
29
+ "title": { "type": "string", "minLength": 1 },
30
+ "format": { "type": "string", "minLength": 1 },
31
+ "accessURL": { "type": "string", "minLength": 1 },
32
+ "downloadURL": { "type": "string", "minLength": 1 },
33
+ "byteSize": { "type": "integer", "minimum": 0 },
34
+ "checksum": { "type": "string", "minLength": 1 }
35
+ },
36
+ "additionalProperties": false
37
+ },
38
+ "temporal": {
39
+ "type": "object",
40
+ "properties": {
41
+ "start": { "type": "string", "minLength": 1 },
42
+ "end": { "type": "string", "minLength": 1 }
43
+ },
44
+ "additionalProperties": false
45
+ },
46
+ "okfBase": {
47
+ "type": "object",
48
+ "required": ["type"],
49
+ "properties": {
50
+ "type": { "type": "string", "minLength": 1 },
51
+ "title": { "type": "string", "minLength": 1 },
52
+ "description": { "type": "string" },
53
+ "resource": { "type": "string" },
54
+ "tags": {
55
+ "type": "array",
56
+ "items": { "type": "string" }
57
+ },
58
+ "timestamp": { "type": "string", "format": "date-time" },
59
+ "profile": { "type": "string" },
60
+ "identifier": { "type": "string", "minLength": 1 },
61
+ "language": { "type": "string", "minLength": 2 },
62
+ "license": { "type": "string", "minLength": 1 },
63
+ "publisher": { "$ref": "#/$defs/publisher" },
64
+ "theme": { "type": "string", "minLength": 1 },
65
+ "temporal": { "$ref": "#/$defs/temporal" },
66
+ "spatial": { "type": "string", "minLength": 1 },
67
+ "distributions": {
68
+ "type": "array",
69
+ "items": { "$ref": "#/$defs/distribution" }
70
+ },
71
+ "creativeWorkType": {
72
+ "type": "string",
73
+ "enum": ["BlogPosting", "TechArticle"]
74
+ },
75
+ "digitalSourceType": {
76
+ "type": "string",
77
+ "minLength": 1
78
+ },
79
+ "euAiLabel": {
80
+ "type": "string",
81
+ "enum": ["basic", "fully-generated", "partially-modified"]
82
+ },
83
+ "aiDisclosureNote": { "type": "string", "maxLength": 500 },
84
+ "aiSystems": {
85
+ "type": "array",
86
+ "maxItems": 10,
87
+ "items": {
88
+ "type": "object",
89
+ "required": ["name"],
90
+ "properties": {
91
+ "name": { "type": "string", "minLength": 1 },
92
+ "version": { "type": "string" },
93
+ "provider": { "type": "string" }
94
+ },
95
+ "additionalProperties": false
96
+ }
97
+ }
98
+ },
99
+ "additionalProperties": true
100
+ },
101
+ "article": {
102
+ "allOf": [
103
+ { "$ref": "#/$defs/okfBase" },
104
+ {
105
+ "properties": { "type": { "const": "article" } },
106
+ "required": ["type", "title"]
107
+ }
108
+ ]
109
+ },
110
+ "index": {
111
+ "allOf": [
112
+ { "$ref": "#/$defs/okfBase" },
113
+ {
114
+ "properties": { "type": { "const": "index" } },
115
+ "required": ["type", "title"]
116
+ }
117
+ ]
118
+ },
119
+ "dataset": {
120
+ "allOf": [
121
+ { "$ref": "#/$defs/okfBase" },
122
+ {
123
+ "properties": {
124
+ "type": { "const": "dataset" },
125
+ "distributions": {
126
+ "type": "array",
127
+ "minItems": 1,
128
+ "items": { "$ref": "#/$defs/distribution" }
129
+ }
130
+ },
131
+ "required": [
132
+ "type",
133
+ "title",
134
+ "description",
135
+ "resource",
136
+ "license",
137
+ "publisher",
138
+ "distributions"
139
+ ]
140
+ }
141
+ ]
142
+ },
143
+ "reference": {
144
+ "allOf": [
145
+ { "$ref": "#/$defs/okfBase" },
146
+ {
147
+ "properties": { "type": { "const": "reference" } },
148
+ "required": ["type", "title"]
149
+ }
150
+ ]
151
+ },
152
+ "glossary": {
153
+ "allOf": [
154
+ { "$ref": "#/$defs/okfBase" },
155
+ {
156
+ "properties": { "type": { "const": "glossary" } },
157
+ "required": ["type", "title"]
158
+ }
159
+ ]
160
+ },
161
+ "faq": {
162
+ "allOf": [
163
+ { "$ref": "#/$defs/okfBase" },
164
+ {
165
+ "properties": { "type": { "const": "faq" } },
166
+ "required": ["type", "title"]
167
+ }
168
+ ]
169
+ }
170
+ }
171
+ }
package/src/index.ts CHANGED
@@ -12,6 +12,17 @@ export {
12
12
  type ValidationResult,
13
13
  type ValidationIssue,
14
14
  } from "./validate.ts";
15
+ export {
16
+ DEFAULT_PROFILE,
17
+ SUPPORTED_PROFILE_RE,
18
+ TYPES_03,
19
+ TYPES_01_02,
20
+ BUILDABLE_CONTENT_TYPES,
21
+ isProfile03,
22
+ resolveEffectiveType,
23
+ isBuildableContentType,
24
+ resolveProfileForValidation,
25
+ } from "./profile.ts";
15
26
  export {
16
27
  IPTC_BASE,
17
28
  resolveDigitalSourceType,
package/src/profile.ts ADDED
@@ -0,0 +1,62 @@
1
+ export const SUPPORTED_PROFILE_RE = /^sorane-okf\/(0\.[123])$/;
2
+ export const DEFAULT_PROFILE = "sorane-okf/0.1";
3
+
4
+ export const TYPES_01_02 = new Set(["article", "index"]);
5
+
6
+ export const TYPES_03 = new Set([
7
+ "article",
8
+ "index",
9
+ "dataset",
10
+ "reference",
11
+ "glossary",
12
+ "faq",
13
+ ]);
14
+
15
+ export const BUILDABLE_CONTENT_TYPES = new Set([
16
+ "article",
17
+ "dataset",
18
+ "reference",
19
+ "glossary",
20
+ "faq",
21
+ ]);
22
+
23
+ export function profileMajor(profile: string | undefined): "0.1" | "0.2" | "0.3" | null {
24
+ if (profile === undefined) return null;
25
+ const m = profile.match(/^sorane-okf\/(0\.[123])$/);
26
+ if (!m) return null;
27
+ return m[1] as "0.1" | "0.2" | "0.3";
28
+ }
29
+
30
+ export function isProfile03(profile: string | undefined): boolean {
31
+ return profile === "sorane-okf/0.3";
32
+ }
33
+
34
+ export function resolveProfileForValidation(
35
+ profile: string | undefined,
36
+ ): string {
37
+ if (profile !== undefined && SUPPORTED_PROFILE_RE.test(profile)) {
38
+ return profile;
39
+ }
40
+ return DEFAULT_PROFILE;
41
+ }
42
+
43
+ /** Build/validate effective type (0.3 unknown types → article). */
44
+ export function resolveEffectiveType(
45
+ type: string,
46
+ profile: string | undefined,
47
+ ): string {
48
+ if (!type) return "";
49
+ if (isProfile03(profile)) {
50
+ if (TYPES_03.has(type)) return type;
51
+ return "article";
52
+ }
53
+ return type;
54
+ }
55
+
56
+ export function isBuildableContentType(
57
+ type: string,
58
+ profile: string | undefined,
59
+ ): boolean {
60
+ const effective = resolveEffectiveType(type, profile);
61
+ return BUILDABLE_CONTENT_TYPES.has(effective);
62
+ }
package/src/validate.ts CHANGED
@@ -6,16 +6,19 @@ import { fileURLToPath } from "node:url";
6
6
  import { validateDisclosureFields } from "./digital-source-type.ts";
7
7
  import { extract } from "./extract.ts";
8
8
  import { normalizeConcept } from "./normalize.ts";
9
+ import {
10
+ DEFAULT_PROFILE,
11
+ isProfile03,
12
+ resolveProfileForValidation,
13
+ SUPPORTED_PROFILE_RE,
14
+ TYPES_01_02,
15
+ TYPES_03,
16
+ } from "./profile.ts";
9
17
  import { parseYaml } from "./yaml.ts";
10
18
 
11
19
  const __dirname = dirname(fileURLToPath(import.meta.url));
12
20
  const PROFILE_SCHEMA_DIR = join(__dirname, "../profile");
13
21
 
14
- const SUPPORTED_PROFILE_RE = /^sorane-okf\/(0\.[12])$/;
15
- const DEFAULT_PROFILE = "sorane-okf/0.1";
16
-
17
- const SUPPORTED_TYPES = new Set(["article", "index"]);
18
-
19
22
  export interface ValidationIssue {
20
23
  readonly where: "structure" | "frontmatter" | "type" | "profile";
21
24
  readonly message: string;
@@ -39,7 +42,7 @@ export function validateProfileFormat(
39
42
  if (!SUPPORTED_PROFILE_RE.test(profile)) {
40
43
  return {
41
44
  where: "profile",
42
- message: `Unsupported profile "${profile}"; supported: sorane-okf/0.1, sorane-okf/0.2`,
45
+ message: `Unsupported profile "${profile}"; supported: sorane-okf/0.1, sorane-okf/0.2, sorane-okf/0.3`,
43
46
  };
44
47
  }
45
48
  return null;
@@ -70,6 +73,37 @@ function slugFromPath(filePath: string): string {
70
73
  return base.replace(/\.md$/i, "");
71
74
  }
72
75
 
76
+ function validateTypeForProfile(
77
+ type: string,
78
+ profile: string,
79
+ issues: ValidationIssue[],
80
+ warnings: string[],
81
+ ): void {
82
+ if (!type) {
83
+ issues.push({
84
+ where: "type",
85
+ message: "OKF 必須フィールド `type` がありません",
86
+ });
87
+ return;
88
+ }
89
+
90
+ if (isProfile03(profile)) {
91
+ if (!TYPES_03.has(type)) {
92
+ warnings.push(
93
+ `unknown type "${type}" (sorane-okf/0.3); build treats as article`,
94
+ );
95
+ }
96
+ return;
97
+ }
98
+
99
+ if (!TYPES_01_02.has(type)) {
100
+ issues.push({
101
+ where: "type",
102
+ message: `未サポートの type: ${type}(${profile} は article / index のみ)`,
103
+ });
104
+ }
105
+ }
106
+
73
107
  /** 1 つの markdown ソースを sorane-okf プロファイルで検証する。 */
74
108
  export function validateSource(file: string, source: string): ValidationResult {
75
109
  const { frontmatter, body } = extract(source);
@@ -126,29 +160,24 @@ export function validateSource(file: string, source: string): ValidationResult {
126
160
  const concept = normalizeConcept(raw, body, slugFromPath(file));
127
161
  warnings.push(...concept.warnings);
128
162
 
129
- if (!concept.type) {
130
- issues.push({
131
- where: "type",
132
- message: "OKF 必須フィールド `type` がありません",
133
- });
134
- } else if (!SUPPORTED_TYPES.has(concept.type)) {
135
- issues.push({
136
- where: "type",
137
- message: `未サポートの type: ${concept.type}(v0.1 は article / index のみ)`,
138
- });
139
- }
140
-
141
- const profile =
163
+ const profile = resolveProfileForValidation(
142
164
  typeof concept.profile === "string" && SUPPORTED_PROFILE_RE.test(concept.profile)
143
165
  ? concept.profile
144
- : DEFAULT_PROFILE;
166
+ : undefined,
167
+ );
168
+
169
+ validateTypeForProfile(concept.type, profile, issues, warnings);
145
170
 
146
171
  if (issues.every((i) => i.where !== "profile")) {
147
172
  const validate = getValidatorForProfile(profile);
173
+ const schemaType =
174
+ isProfile03(profile) && concept.type && !TYPES_03.has(concept.type)
175
+ ? "article"
176
+ : concept.type || "article";
148
177
  const fmForSchema: Record<string, unknown> = {
149
- type: concept.type || "article",
150
178
  title: concept.title,
151
179
  ...concept.frontmatter,
180
+ type: schemaType,
152
181
  };
153
182
  if (concept.timestamp) fmForSchema.timestamp = concept.timestamp;
154
183
  if (concept.description) fmForSchema.description = concept.description;
@@ -167,7 +196,7 @@ export function validateSource(file: string, source: string): ValidationResult {
167
196
  }
168
197
  }
169
198
 
170
- const strictDisclosure = profile === "sorane-okf/0.2";
199
+ const strictDisclosure = profile === "sorane-okf/0.2" || profile === "sorane-okf/0.3";
171
200
  const disclosure = validateDisclosureFields(concept.frontmatter, strictDisclosure);
172
201
  warnings.push(...disclosure.warnings);
173
202
  for (const d of disclosure.issues) {