novel-writer-cli 0.0.1

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 (116) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +103 -0
  3. package/agents/chapter-writer.md +142 -0
  4. package/agents/character-weaver.md +117 -0
  5. package/agents/consistency-auditor.md +85 -0
  6. package/agents/plot-architect.md +128 -0
  7. package/agents/quality-judge.md +232 -0
  8. package/agents/style-analyzer.md +109 -0
  9. package/agents/style-refiner.md +97 -0
  10. package/agents/summarizer.md +128 -0
  11. package/agents/world-builder.md +161 -0
  12. package/dist/__tests__/character-voice.test.js +445 -0
  13. package/dist/__tests__/commit-prototype-pollution.test.js +45 -0
  14. package/dist/__tests__/engagement.test.js +382 -0
  15. package/dist/__tests__/foreshadow-visibility.test.js +131 -0
  16. package/dist/__tests__/hook-ledger.test.js +1028 -0
  17. package/dist/__tests__/naming-lint.test.js +132 -0
  18. package/dist/__tests__/narrative-health-injection.test.js +359 -0
  19. package/dist/__tests__/next-step-prejudge-guardrails.test.js +325 -0
  20. package/dist/__tests__/next-step-title-fix.test.js +153 -0
  21. package/dist/__tests__/platform-profile.test.js +274 -0
  22. package/dist/__tests__/promise-ledger.test.js +189 -0
  23. package/dist/__tests__/readability-lint.test.js +209 -0
  24. package/dist/__tests__/text-utils.test.js +39 -0
  25. package/dist/__tests__/title-policy.test.js +147 -0
  26. package/dist/advance.js +75 -0
  27. package/dist/character-voice.js +805 -0
  28. package/dist/checkpoint.js +126 -0
  29. package/dist/cli.js +563 -0
  30. package/dist/cliche-lint.js +515 -0
  31. package/dist/commit.js +1460 -0
  32. package/dist/consistency-auditor.js +684 -0
  33. package/dist/engagement.js +687 -0
  34. package/dist/errors.js +7 -0
  35. package/dist/fingerprint.js +16 -0
  36. package/dist/foreshadow-visibility.js +214 -0
  37. package/dist/fs-utils.js +68 -0
  38. package/dist/hook-ledger.js +721 -0
  39. package/dist/hook-policy.js +107 -0
  40. package/dist/instruction-gates.js +51 -0
  41. package/dist/instructions.js +406 -0
  42. package/dist/latest-summary-loader.js +29 -0
  43. package/dist/lock.js +121 -0
  44. package/dist/naming-lint.js +531 -0
  45. package/dist/ner.js +73 -0
  46. package/dist/next-step.js +408 -0
  47. package/dist/novel-ask.js +270 -0
  48. package/dist/output.js +9 -0
  49. package/dist/platform-constraints.js +518 -0
  50. package/dist/platform-profile.js +325 -0
  51. package/dist/prejudge-guardrails.js +370 -0
  52. package/dist/project.js +40 -0
  53. package/dist/promise-ledger.js +723 -0
  54. package/dist/readability-lint.js +555 -0
  55. package/dist/safe-parse.js +36 -0
  56. package/dist/safe-path.js +29 -0
  57. package/dist/scoring-weights.js +290 -0
  58. package/dist/steps.js +60 -0
  59. package/dist/text-utils.js +18 -0
  60. package/dist/title-policy.js +251 -0
  61. package/dist/type-guards.js +6 -0
  62. package/dist/validate.js +131 -0
  63. package/docs/user/README.md +17 -0
  64. package/docs/user/guardrails.md +179 -0
  65. package/docs/user/interactive-gates.md +124 -0
  66. package/docs/user/novel-cli.md +289 -0
  67. package/docs/user/ops.md +123 -0
  68. package/docs/user/quick-start.md +97 -0
  69. package/docs/user/spec-system.md +166 -0
  70. package/docs/user/storylines.md +144 -0
  71. package/package.json +48 -0
  72. package/schemas/README.md +18 -0
  73. package/schemas/character-voice-drift.schema.json +135 -0
  74. package/schemas/character-voice-profiles.schema.json +141 -0
  75. package/schemas/engagement-metrics.schema.json +38 -0
  76. package/schemas/hook-ledger.schema.json +108 -0
  77. package/schemas/platform-profile.schema.json +235 -0
  78. package/schemas/promise-ledger.schema.json +97 -0
  79. package/scripts/calibrate-quality-judge.sh +91 -0
  80. package/scripts/compare-regression-runs.sh +86 -0
  81. package/scripts/lib/_common.py +131 -0
  82. package/scripts/lib/calibrate_quality_judge.py +312 -0
  83. package/scripts/lib/compare_regression_runs.py +142 -0
  84. package/scripts/lib/run_regression.py +621 -0
  85. package/scripts/lint-blacklist.sh +201 -0
  86. package/scripts/lint-cliche.sh +370 -0
  87. package/scripts/lint-readability.sh +404 -0
  88. package/scripts/query-foreshadow.sh +252 -0
  89. package/scripts/run-ner.sh +669 -0
  90. package/scripts/run-regression.sh +122 -0
  91. package/skills/cli-step/SKILL.md +158 -0
  92. package/skills/continue/SKILL.md +348 -0
  93. package/skills/continue/references/context-contracts.md +169 -0
  94. package/skills/continue/references/continuity-checks.md +187 -0
  95. package/skills/continue/references/file-protocols.md +64 -0
  96. package/skills/continue/references/foreshadowing.md +130 -0
  97. package/skills/continue/references/gate-decision.md +53 -0
  98. package/skills/continue/references/periodic-maintenance.md +46 -0
  99. package/skills/novel-writing/SKILL.md +77 -0
  100. package/skills/novel-writing/references/quality-rubric.md +140 -0
  101. package/skills/novel-writing/references/style-guide.md +145 -0
  102. package/skills/start/SKILL.md +458 -0
  103. package/skills/start/references/quality-review.md +86 -0
  104. package/skills/start/references/setting-update.md +44 -0
  105. package/skills/start/references/vol-planning.md +61 -0
  106. package/skills/start/references/vol-review.md +58 -0
  107. package/skills/status/SKILL.md +116 -0
  108. package/skills/status/references/sample-output.md +60 -0
  109. package/templates/ai-blacklist.json +79 -0
  110. package/templates/brief-template.md +46 -0
  111. package/templates/genre-weight-profiles.json +90 -0
  112. package/templates/novel-ask/example.answer.json +12 -0
  113. package/templates/novel-ask/example.question.json +51 -0
  114. package/templates/platform-profile.json +148 -0
  115. package/templates/style-profile-template.json +58 -0
  116. package/templates/web-novel-cliche-lint.json +41 -0
@@ -0,0 +1,325 @@
1
+ import { join } from "node:path";
2
+ import { NovelCliError } from "./errors.js";
3
+ import { pathExists, readJsonFile } from "./fs-utils.js";
4
+ import { isPlainObject } from "./type-guards.js";
5
+ function requireIntField(obj, field, file) {
6
+ const v = obj[field];
7
+ if (typeof v !== "number" || !Number.isInteger(v))
8
+ throw new NovelCliError(`Invalid ${file}: '${field}' must be an int.`, 2);
9
+ return v;
10
+ }
11
+ function requirePositiveNumberField(obj, field, file) {
12
+ const v = obj[field];
13
+ if (typeof v !== "number" || !Number.isFinite(v) || v < 0)
14
+ throw new NovelCliError(`Invalid ${file}: '${field}' must be a non-negative number.`, 2);
15
+ return v;
16
+ }
17
+ function requireBoolField(obj, field, file) {
18
+ const v = obj[field];
19
+ if (typeof v !== "boolean")
20
+ throw new NovelCliError(`Invalid ${file}: '${field}' must be a boolean.`, 2);
21
+ return v;
22
+ }
23
+ function requireStringArrayField(obj, field, file) {
24
+ const v = obj[field];
25
+ if (!Array.isArray(v) || !v.every((s) => typeof s === "string"))
26
+ throw new NovelCliError(`Invalid ${file}: '${field}' must be a string array.`, 2);
27
+ return v;
28
+ }
29
+ function requireStringField(obj, field, file) {
30
+ const v = obj[field];
31
+ if (typeof v !== "string" || v.trim().length === 0)
32
+ throw new NovelCliError(`Invalid ${file}: '${field}' must be a non-empty string.`, 2);
33
+ return v;
34
+ }
35
+ function requirePlatformId(value, file) {
36
+ if (value === "qidian" || value === "tomato")
37
+ return value;
38
+ throw new NovelCliError(`Invalid ${file}: 'platform' must be one of: qidian, tomato.`, 2);
39
+ }
40
+ function requireSeverityPolicy(value, file, field) {
41
+ if (value === "warn" || value === "soft" || value === "hard")
42
+ return value;
43
+ throw new NovelCliError(`Invalid ${file}: '${field}' must be one of: warn, soft, hard.`, 2);
44
+ }
45
+ function parseWordCountPolicy(raw, file) {
46
+ if (!isPlainObject(raw))
47
+ throw new NovelCliError(`Invalid ${file}: 'word_count' must be an object.`, 2);
48
+ const obj = raw;
49
+ return {
50
+ target_min: requireIntField(obj, "target_min", file),
51
+ target_max: requireIntField(obj, "target_max", file),
52
+ hard_min: requireIntField(obj, "hard_min", file),
53
+ hard_max: requireIntField(obj, "hard_max", file)
54
+ };
55
+ }
56
+ function parseInfoLoadPolicy(raw, file) {
57
+ if (!isPlainObject(raw))
58
+ throw new NovelCliError(`Invalid ${file}: 'info_load' must be an object.`, 2);
59
+ const obj = raw;
60
+ return {
61
+ max_new_entities_per_chapter: requireIntField(obj, "max_new_entities_per_chapter", file),
62
+ max_unknown_entities_per_chapter: requireIntField(obj, "max_unknown_entities_per_chapter", file),
63
+ max_new_terms_per_1k_words: requirePositiveNumberField(obj, "max_new_terms_per_1k_words", file)
64
+ };
65
+ }
66
+ function parseCompliancePolicy(raw, file) {
67
+ if (!isPlainObject(raw))
68
+ throw new NovelCliError(`Invalid ${file}: 'compliance' must be an object.`, 2);
69
+ const obj = raw;
70
+ const bannedRaw = obj.banned_words;
71
+ if (!Array.isArray(bannedRaw) || !bannedRaw.every((w) => typeof w === "string" && w.trim().length > 0)) {
72
+ throw new NovelCliError(`Invalid ${file}: 'compliance.banned_words' must be a string array.`, 2);
73
+ }
74
+ const banned_words = Array.from(new Set(bannedRaw.map((w) => w.trim()))).filter((w) => w.length > 0);
75
+ const out = {
76
+ banned_words,
77
+ duplicate_name_policy: requireSeverityPolicy(obj.duplicate_name_policy, file, "compliance.duplicate_name_policy")
78
+ };
79
+ if (obj.script_paths !== undefined) {
80
+ if (!isPlainObject(obj.script_paths))
81
+ throw new NovelCliError(`Invalid ${file}: 'compliance.script_paths' must be an object.`, 2);
82
+ const sp = obj.script_paths;
83
+ const script_paths = {};
84
+ for (const [k, v] of Object.entries(sp)) {
85
+ if (typeof v !== "string" || v.trim().length === 0) {
86
+ throw new NovelCliError(`Invalid ${file}: 'compliance.script_paths.${k}' must be a non-empty string.`, 2);
87
+ }
88
+ script_paths[k] = v.trim();
89
+ }
90
+ out.script_paths = script_paths;
91
+ }
92
+ return out;
93
+ }
94
+ const VALID_FIX_STRATEGIES = ["hook-fix"];
95
+ function parseHookPolicy(raw, file) {
96
+ if (!isPlainObject(raw))
97
+ throw new NovelCliError(`Invalid ${file}: 'hook_policy' must be an object.`, 2);
98
+ const obj = raw;
99
+ const min_strength = requireIntField(obj, "min_strength", file);
100
+ if (min_strength < 1 || min_strength > 5)
101
+ throw new NovelCliError(`Invalid ${file}: 'hook_policy.min_strength' must be 1-5.`, 2);
102
+ const fix_strategy = requireStringField(obj, "fix_strategy", file);
103
+ if (!VALID_FIX_STRATEGIES.includes(fix_strategy)) {
104
+ throw new NovelCliError(`Invalid ${file}: 'hook_policy.fix_strategy' must be one of: ${VALID_FIX_STRATEGIES.join(", ")}.`, 2);
105
+ }
106
+ const allowed_types = Array.from(new Set(requireStringArrayField(obj, "allowed_types", file).map((s) => s.trim()))).filter((s) => s.length > 0);
107
+ if (allowed_types.length === 0) {
108
+ throw new NovelCliError(`Invalid ${file}: 'hook_policy.allowed_types' must be a non-empty string array.`, 2);
109
+ }
110
+ return {
111
+ required: requireBoolField(obj, "required", file),
112
+ min_strength,
113
+ allowed_types,
114
+ fix_strategy
115
+ };
116
+ }
117
+ function parseScoringPolicy(raw, file) {
118
+ if (!isPlainObject(raw))
119
+ throw new NovelCliError(`Invalid ${file}: 'scoring' must be an object.`, 2);
120
+ const obj = raw;
121
+ const out = {
122
+ genre_drive_type: requireStringField(obj, "genre_drive_type", file),
123
+ weight_profile_id: requireStringField(obj, "weight_profile_id", file)
124
+ };
125
+ if (obj.weight_overrides !== undefined) {
126
+ if (!isPlainObject(obj.weight_overrides))
127
+ throw new NovelCliError(`Invalid ${file}: 'scoring.weight_overrides' must be an object.`, 2);
128
+ const wo = obj.weight_overrides;
129
+ const overrides = {};
130
+ for (const [k, v] of Object.entries(wo)) {
131
+ if (typeof v !== "number" || !Number.isFinite(v) || v < 0) {
132
+ throw new NovelCliError(`Invalid ${file}: 'scoring.weight_overrides.${k}' must be a finite number >= 0.`, 2);
133
+ }
134
+ overrides[k] = v;
135
+ }
136
+ out.weight_overrides = overrides;
137
+ }
138
+ return out;
139
+ }
140
+ function requireBoolValue(value, file, field) {
141
+ if (typeof value !== "boolean")
142
+ throw new NovelCliError(`Invalid ${file}: '${field}' must be a boolean.`, 2);
143
+ return value;
144
+ }
145
+ function requireNonNegativeIntValue(value, file, field) {
146
+ if (typeof value !== "number" || !Number.isInteger(value) || value < 0) {
147
+ throw new NovelCliError(`Invalid ${file}: '${field}' must be an int >= 0.`, 2);
148
+ }
149
+ return value;
150
+ }
151
+ function requirePositiveIntValue(value, file, field) {
152
+ if (typeof value !== "number" || !Number.isInteger(value) || value < 1) {
153
+ throw new NovelCliError(`Invalid ${file}: '${field}' must be an int >= 1.`, 2);
154
+ }
155
+ return value;
156
+ }
157
+ function requireFiniteNonNegativeNumberValue(value, file, field) {
158
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
159
+ throw new NovelCliError(`Invalid ${file}: '${field}' must be a finite number >= 0.`, 2);
160
+ }
161
+ return value;
162
+ }
163
+ function requireStringArrayValue(value, file, field, opts) {
164
+ if (!Array.isArray(value) || !value.every((v) => typeof v === "string" && v.trim().length > 0)) {
165
+ throw new NovelCliError(`Invalid ${file}: '${field}' must be a string array (no empty items).`, 2);
166
+ }
167
+ const uniq = Array.from(new Set(value.map((v) => v.trim()))).filter((v) => v.length > 0);
168
+ if (!opts.allowEmpty && uniq.length === 0) {
169
+ throw new NovelCliError(`Invalid ${file}: '${field}' must be a non-empty string array.`, 2);
170
+ }
171
+ return uniq;
172
+ }
173
+ function assertValidRegexPattern(pattern, file, field, index) {
174
+ try {
175
+ // eslint-disable-next-line no-new
176
+ new RegExp(pattern);
177
+ }
178
+ catch (err) {
179
+ const message = err instanceof Error ? err.message : String(err);
180
+ throw new NovelCliError(`Invalid ${file}: '${field}[${index}]' must be a valid regex pattern. ${message}`, 2);
181
+ }
182
+ }
183
+ function parseRetentionTitlePolicy(raw, file) {
184
+ if (!isPlainObject(raw))
185
+ throw new NovelCliError(`Invalid ${file}: 'retention.title_policy' must be an object.`, 2);
186
+ const obj = raw;
187
+ const enabled = requireBoolValue(obj.enabled, file, "retention.title_policy.enabled");
188
+ const min_chars = requireNonNegativeIntValue(obj.min_chars, file, "retention.title_policy.min_chars");
189
+ const max_chars = requireNonNegativeIntValue(obj.max_chars, file, "retention.title_policy.max_chars");
190
+ if (min_chars > max_chars) {
191
+ throw new NovelCliError(`Invalid ${file}: 'retention.title_policy.min_chars' must be <= 'retention.title_policy.max_chars'.`, 2);
192
+ }
193
+ const forbidden_patterns = requireStringArrayValue(obj.forbidden_patterns, file, "retention.title_policy.forbidden_patterns", { allowEmpty: true });
194
+ forbidden_patterns.forEach((p, i) => assertValidRegexPattern(p, file, "retention.title_policy.forbidden_patterns", i));
195
+ const required_patterns = obj.required_patterns === undefined
196
+ ? undefined
197
+ : requireStringArrayValue(obj.required_patterns, file, "retention.title_policy.required_patterns", { allowEmpty: true });
198
+ if (required_patterns)
199
+ required_patterns.forEach((p, i) => assertValidRegexPattern(p, file, "retention.title_policy.required_patterns", i));
200
+ const auto_fix = requireBoolValue(obj.auto_fix, file, "retention.title_policy.auto_fix");
201
+ return {
202
+ enabled,
203
+ min_chars,
204
+ max_chars,
205
+ forbidden_patterns,
206
+ ...(required_patterns ? { required_patterns } : {}),
207
+ auto_fix
208
+ };
209
+ }
210
+ function parseHookLedgerPolicy(raw, file) {
211
+ if (!isPlainObject(raw))
212
+ throw new NovelCliError(`Invalid ${file}: 'retention.hook_ledger' must be an object.`, 2);
213
+ const obj = raw;
214
+ return {
215
+ enabled: requireBoolValue(obj.enabled, file, "retention.hook_ledger.enabled"),
216
+ fulfillment_window_chapters: requirePositiveIntValue(obj.fulfillment_window_chapters, file, "retention.hook_ledger.fulfillment_window_chapters"),
217
+ diversity_window_chapters: requirePositiveIntValue(obj.diversity_window_chapters, file, "retention.hook_ledger.diversity_window_chapters"),
218
+ max_same_type_streak: requirePositiveIntValue(obj.max_same_type_streak, file, "retention.hook_ledger.max_same_type_streak"),
219
+ min_distinct_types_in_window: requirePositiveIntValue(obj.min_distinct_types_in_window, file, "retention.hook_ledger.min_distinct_types_in_window"),
220
+ overdue_policy: requireSeverityPolicy(obj.overdue_policy, file, "retention.hook_ledger.overdue_policy")
221
+ };
222
+ }
223
+ function parseRetentionPolicy(raw, file) {
224
+ if (!isPlainObject(raw))
225
+ throw new NovelCliError(`Invalid ${file}: 'retention' must be an object.`, 2);
226
+ const obj = raw;
227
+ return {
228
+ title_policy: parseRetentionTitlePolicy(obj.title_policy, file),
229
+ hook_ledger: parseHookLedgerPolicy(obj.hook_ledger, file)
230
+ };
231
+ }
232
+ function parseMobileReadabilityPolicy(raw, file) {
233
+ if (!isPlainObject(raw))
234
+ throw new NovelCliError(`Invalid ${file}: 'readability.mobile' must be an object.`, 2);
235
+ const obj = raw;
236
+ const blocking = typeof obj.blocking_severity === "string" ? obj.blocking_severity : null;
237
+ if (blocking !== "hard_only" && blocking !== "soft_and_hard") {
238
+ throw new NovelCliError(`Invalid ${file}: 'readability.mobile.blocking_severity' must be 'hard_only' or 'soft_and_hard'.`, 2);
239
+ }
240
+ return {
241
+ enabled: requireBoolValue(obj.enabled, file, "readability.mobile.enabled"),
242
+ max_paragraph_chars: requirePositiveIntValue(obj.max_paragraph_chars, file, "readability.mobile.max_paragraph_chars"),
243
+ max_consecutive_exposition_paragraphs: requirePositiveIntValue(obj.max_consecutive_exposition_paragraphs, file, "readability.mobile.max_consecutive_exposition_paragraphs"),
244
+ blocking_severity: blocking
245
+ };
246
+ }
247
+ function parseReadabilityPolicy(raw, file) {
248
+ if (!isPlainObject(raw))
249
+ throw new NovelCliError(`Invalid ${file}: 'readability' must be an object.`, 2);
250
+ const obj = raw;
251
+ return { mobile: parseMobileReadabilityPolicy(obj.mobile, file) };
252
+ }
253
+ const VALID_NAMING_CONFLICT_TYPES = ["duplicate", "near_duplicate", "alias_collision"];
254
+ function parseNamingPolicy(raw, file) {
255
+ if (!isPlainObject(raw))
256
+ throw new NovelCliError(`Invalid ${file}: 'naming' must be an object.`, 2);
257
+ const obj = raw;
258
+ const enabled = requireBoolValue(obj.enabled, file, "naming.enabled");
259
+ const near_duplicate_threshold = requireFiniteNonNegativeNumberValue(obj.near_duplicate_threshold, file, "naming.near_duplicate_threshold");
260
+ if (near_duplicate_threshold > 1) {
261
+ throw new NovelCliError(`Invalid ${file}: 'naming.near_duplicate_threshold' must be <= 1.`, 2);
262
+ }
263
+ const rawTypes = requireStringArrayValue(obj.blocking_conflict_types, file, "naming.blocking_conflict_types", { allowEmpty: false });
264
+ const blocking_conflict_types = [];
265
+ for (const t of rawTypes) {
266
+ if (!VALID_NAMING_CONFLICT_TYPES.includes(t)) {
267
+ throw new NovelCliError(`Invalid ${file}: 'naming.blocking_conflict_types' contains unknown type '${t}' (allowed: ${VALID_NAMING_CONFLICT_TYPES.join(", ")}).`, 2);
268
+ }
269
+ blocking_conflict_types.push(t);
270
+ }
271
+ const out = {
272
+ enabled,
273
+ near_duplicate_threshold,
274
+ blocking_conflict_types: Array.from(new Set(blocking_conflict_types))
275
+ };
276
+ if (obj.exemptions !== undefined) {
277
+ if (!isPlainObject(obj.exemptions))
278
+ throw new NovelCliError(`Invalid ${file}: 'naming.exemptions' must be an object.`, 2);
279
+ out.exemptions = obj.exemptions;
280
+ }
281
+ return out;
282
+ }
283
+ export function parsePlatformProfile(raw, file) {
284
+ if (!isPlainObject(raw))
285
+ throw new NovelCliError(`Invalid ${file}: expected a JSON object.`, 2);
286
+ const obj = raw;
287
+ const schema_version = requireIntField(obj, "schema_version", file);
288
+ const platform = requirePlatformId(obj.platform, file);
289
+ const created_at = requireStringField(obj, "created_at", file);
290
+ const word_count = parseWordCountPolicy(obj.word_count, file);
291
+ const info_load = parseInfoLoadPolicy(obj.info_load, file);
292
+ const compliance = parseCompliancePolicy(obj.compliance, file);
293
+ // hook_policy is required in schemas/platform-profile.schema.json but optional here for backward compat with older files.
294
+ let hook_policy;
295
+ if (obj.hook_policy !== undefined) {
296
+ if (!isPlainObject(obj.hook_policy))
297
+ throw new NovelCliError(`Invalid ${file}: 'hook_policy' must be an object.`, 2);
298
+ hook_policy = parseHookPolicy(obj.hook_policy, file);
299
+ }
300
+ let scoring;
301
+ if (obj.scoring !== undefined) {
302
+ if (!isPlainObject(obj.scoring))
303
+ throw new NovelCliError(`Invalid ${file}: 'scoring' must be an object.`, 2);
304
+ scoring = parseScoringPolicy(obj.scoring, file);
305
+ }
306
+ const out = { schema_version, platform, created_at, word_count, info_load, compliance, hook_policy, scoring };
307
+ if (obj.retention !== undefined) {
308
+ out.retention = obj.retention === null ? null : parseRetentionPolicy(obj.retention, file);
309
+ }
310
+ if (obj.readability !== undefined) {
311
+ out.readability = obj.readability === null ? null : parseReadabilityPolicy(obj.readability, file);
312
+ }
313
+ if (obj.naming !== undefined) {
314
+ out.naming = obj.naming === null ? null : parseNamingPolicy(obj.naming, file);
315
+ }
316
+ return out;
317
+ }
318
+ export async function loadPlatformProfile(rootDir) {
319
+ const relPath = "platform-profile.json";
320
+ const absPath = join(rootDir, relPath);
321
+ if (!(await pathExists(absPath)))
322
+ return null;
323
+ const raw = await readJsonFile(absPath);
324
+ return { relPath, profile: parsePlatformProfile(raw, relPath) };
325
+ }