@unbrained/pm-cli 2026.3.12 → 2026.5.1-2

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 (285) hide show
  1. package/.agents/pm/extensions/.managed-extensions.json +42 -0
  2. package/.agents/pm/extensions/beads/index.js +109 -0
  3. package/.agents/pm/extensions/beads/manifest.json +7 -0
  4. package/{dist/cli/commands/beads.js → .agents/pm/extensions/beads/runtime.js} +31 -21
  5. package/.agents/pm/extensions/beads/runtime.ts +702 -0
  6. package/.agents/pm/extensions/todos/index.js +126 -0
  7. package/.agents/pm/extensions/todos/manifest.json +7 -0
  8. package/{dist/extensions/builtins/todos/import-export.js → .agents/pm/extensions/todos/runtime.js} +39 -29
  9. package/.agents/pm/extensions/todos/runtime.ts +568 -0
  10. package/AGENTS.md +196 -92
  11. package/CHANGELOG.md +404 -0
  12. package/CODE_OF_CONDUCT.md +42 -0
  13. package/CONTRIBUTING.md +144 -0
  14. package/PRD.md +512 -164
  15. package/README.md +1053 -2
  16. package/SECURITY.md +51 -0
  17. package/dist/cli/commands/activity.d.ts +5 -0
  18. package/dist/cli/commands/activity.js +66 -3
  19. package/dist/cli/commands/activity.js.map +1 -1
  20. package/dist/cli/commands/aggregate.d.ts +54 -0
  21. package/dist/cli/commands/aggregate.js +181 -0
  22. package/dist/cli/commands/aggregate.js.map +1 -0
  23. package/dist/cli/commands/append.js +4 -1
  24. package/dist/cli/commands/append.js.map +1 -1
  25. package/dist/cli/commands/calendar.d.ts +109 -0
  26. package/dist/cli/commands/calendar.js +797 -0
  27. package/dist/cli/commands/calendar.js.map +1 -0
  28. package/dist/cli/commands/claim.d.ts +5 -1
  29. package/dist/cli/commands/claim.js +42 -21
  30. package/dist/cli/commands/claim.js.map +1 -1
  31. package/dist/cli/commands/close.d.ts +1 -0
  32. package/dist/cli/commands/close.js +54 -5
  33. package/dist/cli/commands/close.js.map +1 -1
  34. package/dist/cli/commands/comments-audit.d.ts +91 -0
  35. package/dist/cli/commands/comments-audit.js +195 -0
  36. package/dist/cli/commands/comments-audit.js.map +1 -0
  37. package/dist/cli/commands/comments.d.ts +1 -0
  38. package/dist/cli/commands/comments.js +70 -21
  39. package/dist/cli/commands/comments.js.map +1 -1
  40. package/dist/cli/commands/completion.d.ts +10 -4
  41. package/dist/cli/commands/completion.js +1184 -137
  42. package/dist/cli/commands/completion.js.map +1 -1
  43. package/dist/cli/commands/config.d.ts +35 -3
  44. package/dist/cli/commands/config.js +968 -13
  45. package/dist/cli/commands/config.js.map +1 -1
  46. package/dist/cli/commands/context.d.ts +86 -0
  47. package/dist/cli/commands/context.js +299 -0
  48. package/dist/cli/commands/context.js.map +1 -0
  49. package/dist/cli/commands/contracts.d.ts +78 -0
  50. package/dist/cli/commands/contracts.js +920 -0
  51. package/dist/cli/commands/contracts.js.map +1 -0
  52. package/dist/cli/commands/create.d.ts +48 -14
  53. package/dist/cli/commands/create.js +1331 -160
  54. package/dist/cli/commands/create.js.map +1 -1
  55. package/dist/cli/commands/dedupe-audit.d.ts +81 -0
  56. package/dist/cli/commands/dedupe-audit.js +330 -0
  57. package/dist/cli/commands/dedupe-audit.js.map +1 -0
  58. package/dist/cli/commands/deps.d.ts +52 -0
  59. package/dist/cli/commands/deps.js +204 -0
  60. package/dist/cli/commands/deps.js.map +1 -0
  61. package/dist/cli/commands/docs.d.ts +19 -0
  62. package/dist/cli/commands/docs.js +212 -13
  63. package/dist/cli/commands/docs.js.map +1 -1
  64. package/dist/cli/commands/extension.d.ts +122 -0
  65. package/dist/cli/commands/extension.js +1850 -0
  66. package/dist/cli/commands/extension.js.map +1 -0
  67. package/dist/cli/commands/files.d.ts +52 -1
  68. package/dist/cli/commands/files.js +455 -13
  69. package/dist/cli/commands/files.js.map +1 -1
  70. package/dist/cli/commands/gc.d.ts +11 -1
  71. package/dist/cli/commands/gc.js +89 -11
  72. package/dist/cli/commands/gc.js.map +1 -1
  73. package/dist/cli/commands/get.d.ts +13 -0
  74. package/dist/cli/commands/get.js +35 -3
  75. package/dist/cli/commands/get.js.map +1 -1
  76. package/dist/cli/commands/health.d.ts +10 -2
  77. package/dist/cli/commands/health.js +774 -23
  78. package/dist/cli/commands/health.js.map +1 -1
  79. package/dist/cli/commands/history.d.ts +20 -0
  80. package/dist/cli/commands/history.js +152 -6
  81. package/dist/cli/commands/history.js.map +1 -1
  82. package/dist/cli/commands/index.d.ts +16 -3
  83. package/dist/cli/commands/index.js +16 -3
  84. package/dist/cli/commands/index.js.map +1 -1
  85. package/dist/cli/commands/init.d.ts +7 -2
  86. package/dist/cli/commands/init.js +137 -5
  87. package/dist/cli/commands/init.js.map +1 -1
  88. package/dist/cli/commands/learnings.d.ts +17 -0
  89. package/dist/cli/commands/learnings.js +129 -0
  90. package/dist/cli/commands/learnings.js.map +1 -0
  91. package/dist/cli/commands/list.d.ts +29 -1
  92. package/dist/cli/commands/list.js +289 -53
  93. package/dist/cli/commands/list.js.map +1 -1
  94. package/dist/cli/commands/normalize.d.ts +51 -0
  95. package/dist/cli/commands/normalize.js +298 -0
  96. package/dist/cli/commands/normalize.js.map +1 -0
  97. package/dist/cli/commands/notes.d.ts +17 -0
  98. package/dist/cli/commands/notes.js +129 -0
  99. package/dist/cli/commands/notes.js.map +1 -0
  100. package/dist/cli/commands/reindex.d.ts +1 -0
  101. package/dist/cli/commands/reindex.js +208 -32
  102. package/dist/cli/commands/reindex.js.map +1 -1
  103. package/dist/cli/commands/restore.js +164 -30
  104. package/dist/cli/commands/restore.js.map +1 -1
  105. package/dist/cli/commands/search.d.ts +14 -1
  106. package/dist/cli/commands/search.js +475 -81
  107. package/dist/cli/commands/search.js.map +1 -1
  108. package/dist/cli/commands/stats.js +26 -10
  109. package/dist/cli/commands/stats.js.map +1 -1
  110. package/dist/cli/commands/templates.d.ts +26 -0
  111. package/dist/cli/commands/templates.js +179 -0
  112. package/dist/cli/commands/templates.js.map +1 -0
  113. package/dist/cli/commands/test-all.d.ts +19 -1
  114. package/dist/cli/commands/test-all.js +161 -13
  115. package/dist/cli/commands/test-all.js.map +1 -1
  116. package/dist/cli/commands/test-runs.d.ts +63 -0
  117. package/dist/cli/commands/test-runs.js +179 -0
  118. package/dist/cli/commands/test-runs.js.map +1 -0
  119. package/dist/cli/commands/test.d.ts +75 -1
  120. package/dist/cli/commands/test.js +1360 -41
  121. package/dist/cli/commands/test.js.map +1 -1
  122. package/dist/cli/commands/update-many.d.ts +57 -0
  123. package/dist/cli/commands/update-many.js +631 -0
  124. package/dist/cli/commands/update-many.js.map +1 -0
  125. package/dist/cli/commands/update.d.ts +30 -0
  126. package/dist/cli/commands/update.js +1393 -84
  127. package/dist/cli/commands/update.js.map +1 -1
  128. package/dist/cli/commands/validate.d.ts +30 -0
  129. package/dist/cli/commands/validate.js +1151 -0
  130. package/dist/cli/commands/validate.js.map +1 -0
  131. package/dist/cli/error-guidance.d.ts +33 -0
  132. package/dist/cli/error-guidance.js +337 -0
  133. package/dist/cli/error-guidance.js.map +1 -0
  134. package/dist/cli/extension-command-options.d.ts +1 -0
  135. package/dist/cli/extension-command-options.js +92 -0
  136. package/dist/cli/extension-command-options.js.map +1 -1
  137. package/dist/cli/help-content.d.ts +20 -0
  138. package/dist/cli/help-content.js +543 -0
  139. package/dist/cli/help-content.js.map +1 -0
  140. package/dist/cli/main.js +3625 -445
  141. package/dist/cli/main.js.map +1 -1
  142. package/dist/core/extensions/index.d.ts +13 -1
  143. package/dist/core/extensions/index.js +108 -1
  144. package/dist/core/extensions/index.js.map +1 -1
  145. package/dist/core/extensions/item-fields.d.ts +2 -0
  146. package/dist/core/extensions/item-fields.js +79 -0
  147. package/dist/core/extensions/item-fields.js.map +1 -0
  148. package/dist/core/extensions/loader.d.ts +322 -9
  149. package/dist/core/extensions/loader.js +911 -20
  150. package/dist/core/extensions/loader.js.map +1 -1
  151. package/dist/core/extensions/runtime-registrations.d.ts +5 -0
  152. package/dist/core/extensions/runtime-registrations.js +51 -0
  153. package/dist/core/extensions/runtime-registrations.js.map +1 -0
  154. package/dist/core/history/history-stream-policy.d.ts +20 -0
  155. package/dist/core/history/history-stream-policy.js +53 -0
  156. package/dist/core/history/history-stream-policy.js.map +1 -0
  157. package/dist/core/history/history.js +90 -1
  158. package/dist/core/history/history.js.map +1 -1
  159. package/dist/core/item/id.js +4 -1
  160. package/dist/core/item/id.js.map +1 -1
  161. package/dist/core/item/index.d.ts +1 -0
  162. package/dist/core/item/index.js +1 -0
  163. package/dist/core/item/index.js.map +1 -1
  164. package/dist/core/item/item-format.d.ts +11 -5
  165. package/dist/core/item/item-format.js +507 -24
  166. package/dist/core/item/item-format.js.map +1 -1
  167. package/dist/core/item/parent-reference-policy.d.ts +6 -0
  168. package/dist/core/item/parent-reference-policy.js +32 -0
  169. package/dist/core/item/parent-reference-policy.js.map +1 -0
  170. package/dist/core/item/parse.d.ts +5 -0
  171. package/dist/core/item/parse.js +216 -19
  172. package/dist/core/item/parse.js.map +1 -1
  173. package/dist/core/item/sprint-release-format.d.ts +6 -0
  174. package/dist/core/item/sprint-release-format.js +33 -0
  175. package/dist/core/item/sprint-release-format.js.map +1 -0
  176. package/dist/core/item/status.d.ts +3 -0
  177. package/dist/core/item/status.js +24 -0
  178. package/dist/core/item/status.js.map +1 -0
  179. package/dist/core/item/type-registry.d.ts +37 -0
  180. package/dist/core/item/type-registry.js +706 -0
  181. package/dist/core/item/type-registry.js.map +1 -0
  182. package/dist/core/lock/lock.d.ts +1 -1
  183. package/dist/core/lock/lock.js +101 -12
  184. package/dist/core/lock/lock.js.map +1 -1
  185. package/dist/core/output/command-aware.d.ts +1 -0
  186. package/dist/core/output/command-aware.js +394 -0
  187. package/dist/core/output/command-aware.js.map +1 -0
  188. package/dist/core/output/output.d.ts +3 -0
  189. package/dist/core/output/output.js +124 -6
  190. package/dist/core/output/output.js.map +1 -1
  191. package/dist/core/schema/runtime-field-filters.d.ts +3 -0
  192. package/dist/core/schema/runtime-field-filters.js +39 -0
  193. package/dist/core/schema/runtime-field-filters.js.map +1 -0
  194. package/dist/core/schema/runtime-field-values.d.ts +8 -0
  195. package/dist/core/schema/runtime-field-values.js +154 -0
  196. package/dist/core/schema/runtime-field-values.js.map +1 -0
  197. package/dist/core/schema/runtime-schema.d.ts +68 -0
  198. package/dist/core/schema/runtime-schema.js +554 -0
  199. package/dist/core/schema/runtime-schema.js.map +1 -0
  200. package/dist/core/search/cache.d.ts +13 -1
  201. package/dist/core/search/cache.js +123 -14
  202. package/dist/core/search/cache.js.map +1 -1
  203. package/dist/core/search/semantic-defaults.d.ts +6 -0
  204. package/dist/core/search/semantic-defaults.js +120 -0
  205. package/dist/core/search/semantic-defaults.js.map +1 -0
  206. package/dist/core/search/vector-stores.js +3 -1
  207. package/dist/core/search/vector-stores.js.map +1 -1
  208. package/dist/core/shared/command-types.d.ts +2 -0
  209. package/dist/core/shared/conflict-markers.d.ts +7 -0
  210. package/dist/core/shared/conflict-markers.js +27 -0
  211. package/dist/core/shared/conflict-markers.js.map +1 -0
  212. package/dist/core/shared/constants.d.ts +15 -4
  213. package/dist/core/shared/constants.js +141 -1
  214. package/dist/core/shared/constants.js.map +1 -1
  215. package/dist/core/shared/errors.d.ts +10 -1
  216. package/dist/core/shared/errors.js +3 -1
  217. package/dist/core/shared/errors.js.map +1 -1
  218. package/dist/core/shared/text-normalization.d.ts +4 -0
  219. package/dist/core/shared/text-normalization.js +33 -0
  220. package/dist/core/shared/text-normalization.js.map +1 -0
  221. package/dist/core/shared/time.d.ts +1 -2
  222. package/dist/core/shared/time.js +98 -11
  223. package/dist/core/shared/time.js.map +1 -1
  224. package/dist/core/store/index.d.ts +1 -0
  225. package/dist/core/store/index.js +1 -0
  226. package/dist/core/store/index.js.map +1 -1
  227. package/dist/core/store/item-format-migration.d.ts +9 -0
  228. package/dist/core/store/item-format-migration.js +87 -0
  229. package/dist/core/store/item-format-migration.js.map +1 -0
  230. package/dist/core/store/item-store.d.ts +13 -4
  231. package/dist/core/store/item-store.js +238 -51
  232. package/dist/core/store/item-store.js.map +1 -1
  233. package/dist/core/store/paths.d.ts +21 -3
  234. package/dist/core/store/paths.js +59 -4
  235. package/dist/core/store/paths.js.map +1 -1
  236. package/dist/core/store/settings.d.ts +14 -1
  237. package/dist/core/store/settings.js +463 -7
  238. package/dist/core/store/settings.js.map +1 -1
  239. package/dist/core/telemetry/consent.d.ts +2 -0
  240. package/dist/core/telemetry/consent.js +79 -0
  241. package/dist/core/telemetry/consent.js.map +1 -0
  242. package/dist/core/telemetry/runtime.d.ts +38 -0
  243. package/dist/core/telemetry/runtime.js +733 -0
  244. package/dist/core/telemetry/runtime.js.map +1 -0
  245. package/dist/core/test/background-runs.d.ts +117 -0
  246. package/dist/core/test/background-runs.js +760 -0
  247. package/dist/core/test/background-runs.js.map +1 -0
  248. package/dist/core/test/item-test-run-tracking.d.ts +9 -0
  249. package/dist/core/test/item-test-run-tracking.js +50 -0
  250. package/dist/core/test/item-test-run-tracking.js.map +1 -0
  251. package/dist/sdk/cli-contracts.d.ts +92 -0
  252. package/dist/sdk/cli-contracts.js +2357 -0
  253. package/dist/sdk/cli-contracts.js.map +1 -0
  254. package/dist/sdk/index.d.ts +34 -0
  255. package/dist/sdk/index.js +23 -0
  256. package/dist/sdk/index.js.map +1 -0
  257. package/dist/types.d.ts +197 -3
  258. package/dist/types.js +48 -1
  259. package/dist/types.js.map +1 -1
  260. package/docs/ARCHITECTURE.md +368 -39
  261. package/docs/EXTENSIONS.md +454 -49
  262. package/docs/RELEASING.md +70 -19
  263. package/docs/SDK.md +123 -0
  264. package/docs/examples/starter-extension/README.md +48 -0
  265. package/docs/examples/starter-extension/index.js +191 -0
  266. package/docs/examples/starter-extension/manifest.json +17 -0
  267. package/docs/examples/starter-extension/package.json +10 -0
  268. package/package.json +41 -14
  269. package/.pi/extensions/pm-cli/index.ts +0 -778
  270. package/dist/cli/commands/beads.d.ts +0 -16
  271. package/dist/cli/commands/beads.js.map +0 -1
  272. package/dist/cli/commands/install.d.ts +0 -18
  273. package/dist/cli/commands/install.js +0 -87
  274. package/dist/cli/commands/install.js.map +0 -1
  275. package/dist/core/extensions/builtins.d.ts +0 -3
  276. package/dist/core/extensions/builtins.js +0 -47
  277. package/dist/core/extensions/builtins.js.map +0 -1
  278. package/dist/extensions/builtins/beads/index.d.ts +0 -8
  279. package/dist/extensions/builtins/beads/index.js +0 -33
  280. package/dist/extensions/builtins/beads/index.js.map +0 -1
  281. package/dist/extensions/builtins/todos/import-export.d.ts +0 -26
  282. package/dist/extensions/builtins/todos/import-export.js.map +0 -1
  283. package/dist/extensions/builtins/todos/index.d.ts +0 -8
  284. package/dist/extensions/builtins/todos/index.js +0 -38
  285. package/dist/extensions/builtins/todos/index.js.map +0 -1
@@ -1,8 +1,14 @@
1
- import { CONFIDENCE_TEXT_VALUES, ISSUE_SEVERITY_VALUES, ITEM_TYPE_VALUES, STATUS_VALUES } from "../../types/index.js";
1
+ import { decode as decodeToon, encode as encodeToon } from "@toon-format/toon";
2
+ import { CONFIDENCE_TEXT_VALUES, ISSUE_SEVERITY_VALUES, RECURRENCE_FREQUENCY_VALUES, RECURRENCE_WEEKDAY_VALUES, STATUS_VALUES, } from "../../types/index.js";
3
+ import { coerceRuntimeFieldValue } from "../schema/runtime-field-values.js";
4
+ import { resolveRuntimeFieldRegistry, resolveRuntimeStatusRegistry, } from "../schema/runtime-schema.js";
5
+ import { normalizeStatusInput } from "./status.js";
2
6
  import { EXIT_CODE, FRONT_MATTER_KEY_ORDER } from "../shared/constants.js";
7
+ import { findFirstMergeConflictMarker } from "../shared/conflict-markers.js";
3
8
  import { PmCliError } from "../shared/errors.js";
4
9
  import { orderObject } from "../shared/serialization.js";
5
10
  import { compareTimestampStrings, isTimestampLiteral } from "../shared/time.js";
11
+ const LINKED_TEST_PM_CONTEXT_MODE_VALUES = new Set(["schema", "tracker", "auto"]);
6
12
  function normalizePathValue(value) {
7
13
  return value.replaceAll("\\", "/");
8
14
  }
@@ -13,6 +19,30 @@ const REQUIRED_STRING_FIELDS = [
13
19
  "created_at",
14
20
  "updated_at",
15
21
  ];
22
+ const STATIC_FRONT_MATTER_FIELD_SET = new Set(FRONT_MATTER_KEY_ORDER);
23
+ function resolveRuntimeSchemaValidationContext(options) {
24
+ if (!options?.schema) {
25
+ return undefined;
26
+ }
27
+ return {
28
+ statusRegistry: resolveRuntimeStatusRegistry(options.schema),
29
+ fieldRegistry: resolveRuntimeFieldRegistry(options.schema),
30
+ unknownFieldPolicy: options.schema.unknown_field_policy ?? "allow",
31
+ onWarning: options.onWarning,
32
+ };
33
+ }
34
+ function runtimeFieldRequiredForType(definition, typeName) {
35
+ if (!definition.required) {
36
+ return false;
37
+ }
38
+ if (definition.required_types.length === 0) {
39
+ return true;
40
+ }
41
+ return definition.required_types.map((value) => value.toLowerCase()).includes(typeName.trim().toLowerCase());
42
+ }
43
+ function weekdayOrderIndex(value) {
44
+ return RECURRENCE_WEEKDAY_VALUES.indexOf(value);
45
+ }
16
46
  function validationError(message) {
17
47
  throw new PmCliError(`Invalid item front matter: ${message}`, EXIT_CODE.GENERIC_FAILURE);
18
48
  }
@@ -27,16 +57,62 @@ function assertTimestampField(record, fieldName) {
27
57
  const timestamp = rawValue;
28
58
  assertFrontMatterCondition(isTimestampLiteral(timestamp), `${fieldName} must be a valid ISO timestamp`);
29
59
  }
30
- function assertValidFrontMatter(frontMatter) {
60
+ function assertValidRecurrenceRule(recurrence) {
61
+ assertFrontMatterCondition(typeof recurrence === "object" && recurrence !== null && !Array.isArray(recurrence), "event.recurrence must be an object");
62
+ const recurrenceRecord = recurrence;
63
+ assertFrontMatterCondition(typeof recurrenceRecord.freq === "string", "event.recurrence.freq must be a string");
64
+ const frequency = recurrenceRecord.freq.trim().toLowerCase();
65
+ assertFrontMatterCondition(RECURRENCE_FREQUENCY_VALUES.includes(frequency), `event.recurrence.freq must be one of: ${RECURRENCE_FREQUENCY_VALUES.join(", ")}`);
66
+ if (recurrenceRecord.interval !== undefined) {
67
+ assertFrontMatterCondition(typeof recurrenceRecord.interval === "number" &&
68
+ Number.isInteger(recurrenceRecord.interval) &&
69
+ recurrenceRecord.interval >= 1, "event.recurrence.interval must be an integer >= 1");
70
+ }
71
+ if (recurrenceRecord.count !== undefined) {
72
+ assertFrontMatterCondition(typeof recurrenceRecord.count === "number" && Number.isInteger(recurrenceRecord.count) && recurrenceRecord.count >= 1, "event.recurrence.count must be an integer >= 1");
73
+ }
74
+ if (recurrenceRecord.until !== undefined) {
75
+ assertFrontMatterCondition(typeof recurrenceRecord.until === "string", "event.recurrence.until must be a string");
76
+ assertFrontMatterCondition(isTimestampLiteral(recurrenceRecord.until), "event.recurrence.until must be a valid ISO timestamp");
77
+ }
78
+ if (recurrenceRecord.by_weekday !== undefined) {
79
+ assertFrontMatterCondition(Array.isArray(recurrenceRecord.by_weekday), "event.recurrence.by_weekday must be an array");
80
+ for (const weekday of recurrenceRecord.by_weekday) {
81
+ assertFrontMatterCondition(typeof weekday === "string", "event.recurrence.by_weekday entries must be strings");
82
+ const normalizedWeekday = weekday.trim().toLowerCase();
83
+ assertFrontMatterCondition(RECURRENCE_WEEKDAY_VALUES.includes(normalizedWeekday), `event.recurrence.by_weekday entries must be one of: ${RECURRENCE_WEEKDAY_VALUES.join(", ")}`);
84
+ }
85
+ }
86
+ if (recurrenceRecord.by_month_day !== undefined) {
87
+ assertFrontMatterCondition(Array.isArray(recurrenceRecord.by_month_day), "event.recurrence.by_month_day must be an array");
88
+ for (const day of recurrenceRecord.by_month_day) {
89
+ assertFrontMatterCondition(typeof day === "number" && Number.isInteger(day) && day >= 1 && day <= 31, "event.recurrence.by_month_day entries must be integers 1..31");
90
+ }
91
+ }
92
+ if (recurrenceRecord.exdates !== undefined) {
93
+ assertFrontMatterCondition(Array.isArray(recurrenceRecord.exdates), "event.recurrence.exdates must be an array");
94
+ for (const exdate of recurrenceRecord.exdates) {
95
+ assertFrontMatterCondition(typeof exdate === "string", "event.recurrence.exdates entries must be strings");
96
+ assertFrontMatterCondition(isTimestampLiteral(exdate), "event.recurrence.exdates entries must be valid ISO timestamps");
97
+ }
98
+ }
99
+ }
100
+ function assertValidFrontMatter(frontMatter, runtimeContext) {
31
101
  assertFrontMatterCondition(typeof frontMatter === "object" && frontMatter !== null && !Array.isArray(frontMatter), "front matter must be an object");
32
102
  const record = frontMatter;
33
103
  for (const fieldName of REQUIRED_STRING_FIELDS) {
34
104
  assertFrontMatterCondition(typeof record[fieldName] === "string", `${fieldName} is required and must be a string`);
35
105
  }
36
106
  const itemType = record.type;
37
- assertFrontMatterCondition(typeof itemType === "string" && ITEM_TYPE_VALUES.includes(itemType), `type must be one of: ${ITEM_TYPE_VALUES.join(", ")}`);
107
+ assertFrontMatterCondition(typeof itemType === "string" && itemType.trim().length > 0, "type must be a non-empty string");
38
108
  const status = record.status;
39
- assertFrontMatterCondition(typeof status === "string" && STATUS_VALUES.includes(status), `status must be one of: ${STATUS_VALUES.join(", ")}`);
109
+ assertFrontMatterCondition(typeof status === "string" && status.trim().length > 0, "status must be a non-empty string");
110
+ const statusRegistry = runtimeContext?.statusRegistry;
111
+ const normalizedStatus = normalizeStatusInput(status, statusRegistry);
112
+ const statusDomain = statusRegistry
113
+ ? statusRegistry.definitions.map((definition) => definition.id)
114
+ : [...STATUS_VALUES];
115
+ assertFrontMatterCondition(normalizedStatus !== undefined, `status must be one of: ${statusDomain.join(", ")}`);
40
116
  const priority = record.priority;
41
117
  assertFrontMatterCondition(typeof priority === "number" && Number.isInteger(priority) && [0, 1, 2, 3, 4].includes(priority), "priority must be an integer 0..4");
42
118
  const tags = record.tags;
@@ -78,6 +154,48 @@ function assertValidFrontMatter(frontMatter) {
78
154
  if (record.deadline !== undefined) {
79
155
  assertTimestampField(record, "deadline");
80
156
  }
157
+ if (record.reminders !== undefined) {
158
+ const reminders = record.reminders;
159
+ assertFrontMatterCondition(Array.isArray(reminders), "reminders must be an array");
160
+ for (const reminder of reminders) {
161
+ assertFrontMatterCondition(typeof reminder === "object" && reminder !== null && !Array.isArray(reminder), "reminders entries must be objects");
162
+ const reminderRecord = reminder;
163
+ assertFrontMatterCondition(typeof reminderRecord.at === "string", "reminder.at must be a string");
164
+ assertFrontMatterCondition(isTimestampLiteral(reminderRecord.at), "reminder.at must be a valid ISO timestamp");
165
+ assertFrontMatterCondition(typeof reminderRecord.text === "string", "reminder.text must be a string");
166
+ assertFrontMatterCondition(reminderRecord.text.trim().length > 0, "reminder.text must not be empty");
167
+ }
168
+ }
169
+ if (record.events !== undefined) {
170
+ const events = record.events;
171
+ assertFrontMatterCondition(Array.isArray(events), "events must be an array");
172
+ for (const event of events) {
173
+ assertFrontMatterCondition(typeof event === "object" && event !== null && !Array.isArray(event), "events entries must be objects");
174
+ const eventRecord = event;
175
+ assertFrontMatterCondition(typeof eventRecord.start_at === "string", "event.start_at must be a string");
176
+ assertFrontMatterCondition(isTimestampLiteral(eventRecord.start_at), "event.start_at must be a valid ISO timestamp");
177
+ if (eventRecord.end_at !== undefined) {
178
+ assertFrontMatterCondition(typeof eventRecord.end_at === "string", "event.end_at must be a string");
179
+ assertFrontMatterCondition(isTimestampLiteral(eventRecord.end_at), "event.end_at must be a valid ISO timestamp");
180
+ assertFrontMatterCondition(compareTimestampStrings(eventRecord.end_at, eventRecord.start_at) > 0, "event.end_at must be after event.start_at");
181
+ }
182
+ for (const stringField of ["title", "description", "location", "timezone"]) {
183
+ if (eventRecord[stringField] !== undefined) {
184
+ assertFrontMatterCondition(typeof eventRecord[stringField] === "string", `event.${stringField} must be a string`);
185
+ assertFrontMatterCondition(eventRecord[stringField].trim().length > 0, `event.${stringField} must not be empty`);
186
+ }
187
+ }
188
+ if (eventRecord.all_day !== undefined) {
189
+ assertFrontMatterCondition(typeof eventRecord.all_day === "boolean", "event.all_day must be a boolean");
190
+ }
191
+ if (eventRecord.recurrence !== undefined) {
192
+ assertValidRecurrenceRule(eventRecord.recurrence);
193
+ if (eventRecord.recurrence.until !== undefined) {
194
+ assertFrontMatterCondition(compareTimestampStrings(eventRecord.recurrence.until, eventRecord.start_at) >= 0, "event.recurrence.until must be at or after event.start_at");
195
+ }
196
+ }
197
+ }
198
+ }
81
199
  if (record.closed_at !== undefined) {
82
200
  const closedAt = record.closed_at;
83
201
  assertFrontMatterCondition(typeof closedAt === "string", "closed_at must be a string");
@@ -89,6 +207,48 @@ function assertValidFrontMatter(frontMatter) {
89
207
  assertFrontMatterCondition(typeof value === "string", `${fieldName} must be a string`);
90
208
  }
91
209
  }
210
+ const typeOptions = record.type_options;
211
+ if (typeOptions !== undefined) {
212
+ assertFrontMatterCondition(typeof typeOptions === "object" && typeOptions !== null && !Array.isArray(typeOptions), "type_options must be an object");
213
+ for (const [optionKey, optionValue] of Object.entries(typeOptions)) {
214
+ assertFrontMatterCondition(optionKey.trim().length > 0, "type_options keys must be non-empty");
215
+ assertFrontMatterCondition(typeof optionValue === "string", "type_options values must be strings");
216
+ const optionText = optionValue;
217
+ assertFrontMatterCondition(optionText.trim().length > 0, "type_options values must be non-empty strings");
218
+ }
219
+ }
220
+ if (runtimeContext?.fieldRegistry) {
221
+ for (const definition of runtimeContext.fieldRegistry.definitions) {
222
+ const fieldValue = record[definition.front_matter_key];
223
+ if (fieldValue === undefined) {
224
+ if (runtimeFieldRequiredForType(definition, itemType)) {
225
+ validationError(`missing required schema field: ${definition.front_matter_key}`);
226
+ }
227
+ continue;
228
+ }
229
+ try {
230
+ record[definition.front_matter_key] = coerceRuntimeFieldValue(definition, fieldValue, `metadata field "${definition.front_matter_key}"`);
231
+ }
232
+ catch (error) {
233
+ validationError(error instanceof Error ? error.message.replace(/^Invalid\s+/u, "") : `invalid ${definition.front_matter_key} value`);
234
+ }
235
+ }
236
+ }
237
+ if (runtimeContext && runtimeContext.unknownFieldPolicy !== "allow") {
238
+ const knownKeys = new Set(STATIC_FRONT_MATTER_FIELD_SET);
239
+ for (const definition of runtimeContext.fieldRegistry?.definitions ?? []) {
240
+ knownKeys.add(definition.front_matter_key);
241
+ }
242
+ const unknownKeys = Object.keys(record).filter((key) => !knownKeys.has(key)).sort((left, right) => left.localeCompare(right));
243
+ if (unknownKeys.length > 0) {
244
+ if (runtimeContext.unknownFieldPolicy === "reject") {
245
+ validationError(`unknown schema fields are not allowed: ${unknownKeys.join(", ")}`);
246
+ }
247
+ else {
248
+ runtimeContext.onWarning?.(`item_unknown_schema_fields:${unknownKeys.join(",")}`);
249
+ }
250
+ }
251
+ }
92
252
  }
93
253
  function sortDependencies(values) {
94
254
  if (!values || values.length === 0)
@@ -127,24 +287,166 @@ function sortLogValues(values) {
127
287
  return a.author.localeCompare(b.author);
128
288
  });
129
289
  }
130
- function sortFiles(values) {
290
+ function sortReminders(values) {
131
291
  if (!values || values.length === 0)
132
292
  return undefined;
133
- return [...values]
293
+ const normalized = [...values]
294
+ .map((value) => ({
295
+ at: value.at,
296
+ text: value.text.trim(),
297
+ }))
298
+ .filter((value) => value.text.length > 0)
299
+ .sort((a, b) => {
300
+ const byAt = compareTimestampStrings(a.at, b.at);
301
+ if (byAt !== 0)
302
+ return byAt;
303
+ return a.text.localeCompare(b.text);
304
+ });
305
+ return normalized.length === 0 ? undefined : normalized;
306
+ }
307
+ function normalizeRecurrenceRule(value) {
308
+ if (!value) {
309
+ return undefined;
310
+ }
311
+ const normalizedFrequency = value.freq.trim().toLowerCase();
312
+ if (!RECURRENCE_FREQUENCY_VALUES.includes(normalizedFrequency)) {
313
+ return undefined;
314
+ }
315
+ const byWeekday = Array.from(new Set((value.by_weekday ?? [])
316
+ .map((weekday) => weekday.trim().toLowerCase())
317
+ .filter((weekday) => RECURRENCE_WEEKDAY_VALUES.includes(weekday)))).sort((a, b) => weekdayOrderIndex(a) -
318
+ weekdayOrderIndex(b));
319
+ const byMonthDay = Array.from(new Set((value.by_month_day ?? [])
320
+ .filter((day) => Number.isInteger(day) && day >= 1 && day <= 31)
321
+ .map((day) => day))).sort((a, b) => a - b);
322
+ const exdates = Array.from(new Set((value.exdates ?? [])
323
+ .map((timestamp) => timestamp.trim())
324
+ .filter((timestamp) => isTimestampLiteral(timestamp)))).sort((a, b) => compareTimestampStrings(a, b));
325
+ const normalized = {
326
+ freq: normalizedFrequency,
327
+ interval: value.interval !== undefined && value.interval > 1 ? value.interval : undefined,
328
+ count: value.count,
329
+ until: value.until?.trim() || undefined,
330
+ by_weekday: byWeekday.length > 0 ? byWeekday : undefined,
331
+ by_month_day: byMonthDay.length > 0 ? byMonthDay : undefined,
332
+ exdates: exdates.length > 0 ? exdates : undefined,
333
+ };
334
+ for (const [key, fieldValue] of Object.entries(normalized)) {
335
+ if (fieldValue === undefined) {
336
+ delete normalized[key];
337
+ }
338
+ }
339
+ return normalized;
340
+ }
341
+ function sortEvents(values) {
342
+ if (!values || values.length === 0) {
343
+ return undefined;
344
+ }
345
+ const normalized = [...values]
346
+ .map((value) => {
347
+ const event = {
348
+ start_at: value.start_at,
349
+ end_at: value.end_at || undefined,
350
+ title: value.title?.trim() || undefined,
351
+ description: value.description?.trim() || undefined,
352
+ location: value.location?.trim() || undefined,
353
+ all_day: value.all_day,
354
+ timezone: value.timezone?.trim() || undefined,
355
+ recurrence: normalizeRecurrenceRule(value.recurrence),
356
+ };
357
+ for (const [key, fieldValue] of Object.entries(event)) {
358
+ if (fieldValue === undefined) {
359
+ delete event[key];
360
+ }
361
+ }
362
+ return event;
363
+ })
364
+ .sort((a, b) => {
365
+ const byStart = compareTimestampStrings(a.start_at, b.start_at);
366
+ if (byStart !== 0)
367
+ return byStart;
368
+ const byEnd = (a.end_at ?? "").localeCompare(b.end_at ?? "");
369
+ if (byEnd !== 0)
370
+ return byEnd;
371
+ const byTitle = (a.title ?? "").localeCompare(b.title ?? "");
372
+ if (byTitle !== 0)
373
+ return byTitle;
374
+ const byAllDay = Number(Boolean(a.all_day)) - Number(Boolean(b.all_day));
375
+ if (byAllDay !== 0)
376
+ return byAllDay;
377
+ const byTimezone = (a.timezone ?? "").localeCompare(b.timezone ?? "");
378
+ if (byTimezone !== 0)
379
+ return byTimezone;
380
+ const byLocation = (a.location ?? "").localeCompare(b.location ?? "");
381
+ if (byLocation !== 0)
382
+ return byLocation;
383
+ const byDescription = (a.description ?? "").localeCompare(b.description ?? "");
384
+ if (byDescription !== 0)
385
+ return byDescription;
386
+ return JSON.stringify(a.recurrence ?? {}).localeCompare(JSON.stringify(b.recurrence ?? {}));
387
+ });
388
+ return normalized;
389
+ }
390
+ function normalizeFiles(values) {
391
+ if (!values || values.length === 0)
392
+ return undefined;
393
+ return values
134
394
  .map((value) => ({
135
395
  path: normalizePathValue(value.path),
136
396
  scope: value.scope,
137
397
  note: value.note?.trim() || undefined,
138
- }))
398
+ }));
399
+ }
400
+ function normalizeTestRunSummaries(values) {
401
+ if (!values || values.length === 0)
402
+ return undefined;
403
+ const normalized = values
404
+ .map((value) => {
405
+ const runId = typeof value.run_id === "string" ? value.run_id.trim() : "";
406
+ const kind = value.kind === "test" || value.kind === "test-all" ? value.kind : "test";
407
+ const status = value.status === "passed" || value.status === "failed" || value.status === "stopped" || value.status === "canceled"
408
+ ? value.status
409
+ : "failed";
410
+ const startedAt = typeof value.started_at === "string" ? value.started_at : "";
411
+ const finishedAt = typeof value.finished_at === "string" ? value.finished_at : "";
412
+ const recordedAt = typeof value.recorded_at === "string" ? value.recorded_at : "";
413
+ const passed = typeof value.passed === "number" && Number.isFinite(value.passed) ? Math.max(0, Math.floor(value.passed)) : 0;
414
+ const failed = typeof value.failed === "number" && Number.isFinite(value.failed) ? Math.max(0, Math.floor(value.failed)) : 0;
415
+ const skipped = typeof value.skipped === "number" && Number.isFinite(value.skipped) ? Math.max(0, Math.floor(value.skipped)) : 0;
416
+ return {
417
+ run_id: runId,
418
+ kind,
419
+ status,
420
+ started_at: startedAt,
421
+ finished_at: finishedAt,
422
+ recorded_at: recordedAt,
423
+ attempt: typeof value.attempt === "number" && Number.isFinite(value.attempt) && value.attempt >= 1
424
+ ? Math.floor(value.attempt)
425
+ : undefined,
426
+ resumed_from: value.resumed_from?.trim() || undefined,
427
+ passed,
428
+ failed,
429
+ skipped,
430
+ items: typeof value.items === "number" && Number.isFinite(value.items) && value.items >= 0
431
+ ? Math.floor(value.items)
432
+ : undefined,
433
+ linked_tests: typeof value.linked_tests === "number" && Number.isFinite(value.linked_tests) && value.linked_tests >= 0
434
+ ? Math.floor(value.linked_tests)
435
+ : undefined,
436
+ fail_on_skipped_triggered: value.fail_on_skipped_triggered === true ? true : undefined,
437
+ };
438
+ })
439
+ .filter((value) => value.run_id.length > 0 && value.started_at.length > 0 && value.finished_at.length > 0 && value.recorded_at.length > 0)
139
440
  .sort((a, b) => {
140
- const byScope = a.scope.localeCompare(b.scope);
141
- if (byScope !== 0)
142
- return byScope;
143
- const byPath = a.path.localeCompare(b.path);
144
- if (byPath !== 0)
145
- return byPath;
146
- return (a.note ?? "").localeCompare(b.note ?? "");
441
+ const byRecorded = compareTimestampStrings(a.recorded_at, b.recorded_at);
442
+ if (byRecorded !== 0)
443
+ return byRecorded;
444
+ const byRunId = a.run_id.localeCompare(b.run_id);
445
+ if (byRunId !== 0)
446
+ return byRunId;
447
+ return a.kind.localeCompare(b.kind);
147
448
  });
449
+ return normalized.length > 0 ? normalized : undefined;
148
450
  }
149
451
  function sortTests(values) {
150
452
  if (!values || values.length === 0)
@@ -155,6 +457,52 @@ function sortTests(values) {
155
457
  path: value.path ? normalizePathValue(value.path) : undefined,
156
458
  scope: value.scope,
157
459
  timeout_seconds: value.timeout_seconds,
460
+ pm_context_mode: (() => {
461
+ const normalized = value.pm_context_mode?.trim().toLowerCase();
462
+ if (!normalized || !LINKED_TEST_PM_CONTEXT_MODE_VALUES.has(normalized)) {
463
+ return undefined;
464
+ }
465
+ return normalized;
466
+ })(),
467
+ env_set: value.env_set
468
+ ? Object.fromEntries(Object.entries(value.env_set)
469
+ .map(([key, envValue]) => [key.trim(), String(envValue).trim()])
470
+ .filter(([key, envValue]) => key.length > 0 && envValue.length > 0)
471
+ .sort(([left], [right]) => left.localeCompare(right)))
472
+ : undefined,
473
+ env_clear: value.env_clear
474
+ ? [...new Set(value.env_clear.map((entry) => entry.trim()).filter((entry) => entry.length > 0))].sort((a, b) => a.localeCompare(b))
475
+ : undefined,
476
+ shared_host_safe: value.shared_host_safe === true ? true : undefined,
477
+ assert_stdout_contains: value.assert_stdout_contains
478
+ ? [...new Set(value.assert_stdout_contains.map((entry) => entry.trim()).filter((entry) => entry.length > 0))].sort((a, b) => a.localeCompare(b))
479
+ : undefined,
480
+ assert_stdout_regex: value.assert_stdout_regex
481
+ ? [...new Set(value.assert_stdout_regex.map((entry) => entry.trim()).filter((entry) => entry.length > 0))].sort((a, b) => a.localeCompare(b))
482
+ : undefined,
483
+ assert_stderr_contains: value.assert_stderr_contains
484
+ ? [...new Set(value.assert_stderr_contains.map((entry) => entry.trim()).filter((entry) => entry.length > 0))].sort((a, b) => a.localeCompare(b))
485
+ : undefined,
486
+ assert_stderr_regex: value.assert_stderr_regex
487
+ ? [...new Set(value.assert_stderr_regex.map((entry) => entry.trim()).filter((entry) => entry.length > 0))].sort((a, b) => a.localeCompare(b))
488
+ : undefined,
489
+ assert_stdout_min_lines: typeof value.assert_stdout_min_lines === "number" &&
490
+ Number.isFinite(value.assert_stdout_min_lines) &&
491
+ value.assert_stdout_min_lines >= 0
492
+ ? Math.floor(value.assert_stdout_min_lines)
493
+ : undefined,
494
+ assert_json_field_equals: value.assert_json_field_equals
495
+ ? Object.fromEntries(Object.entries(value.assert_json_field_equals)
496
+ .map(([key, expectedValue]) => [key.trim(), String(expectedValue).trim()])
497
+ .filter(([key, expectedValue]) => key.length > 0 && expectedValue.length > 0)
498
+ .sort(([left], [right]) => left.localeCompare(right)))
499
+ : undefined,
500
+ assert_json_field_gte: value.assert_json_field_gte
501
+ ? Object.fromEntries(Object.entries(value.assert_json_field_gte)
502
+ .map(([key, expectedValue]) => [key.trim(), Number(expectedValue)])
503
+ .filter(([key, expectedValue]) => key.length > 0 && Number.isFinite(expectedValue))
504
+ .sort(([left], [right]) => left.localeCompare(right)))
505
+ : undefined,
158
506
  note: value.note?.trim() || undefined,
159
507
  }))
160
508
  .sort((a, b) => {
@@ -170,6 +518,39 @@ function sortTests(values) {
170
518
  const byTimeout = (a.timeout_seconds ?? 0) - (b.timeout_seconds ?? 0);
171
519
  if (byTimeout !== 0)
172
520
  return byTimeout;
521
+ const byPmContext = (a.pm_context_mode ?? "").localeCompare(b.pm_context_mode ?? "");
522
+ if (byPmContext !== 0)
523
+ return byPmContext;
524
+ const bySharedHostSafe = Number(Boolean(a.shared_host_safe)) - Number(Boolean(b.shared_host_safe));
525
+ if (bySharedHostSafe !== 0)
526
+ return bySharedHostSafe;
527
+ const byEnvClear = JSON.stringify(a.env_clear ?? []).localeCompare(JSON.stringify(b.env_clear ?? []));
528
+ if (byEnvClear !== 0)
529
+ return byEnvClear;
530
+ const byEnvSet = JSON.stringify(a.env_set ?? {}).localeCompare(JSON.stringify(b.env_set ?? {}));
531
+ if (byEnvSet !== 0)
532
+ return byEnvSet;
533
+ const byStdoutContains = JSON.stringify(a.assert_stdout_contains ?? []).localeCompare(JSON.stringify(b.assert_stdout_contains ?? []));
534
+ if (byStdoutContains !== 0)
535
+ return byStdoutContains;
536
+ const byStdoutRegex = JSON.stringify(a.assert_stdout_regex ?? []).localeCompare(JSON.stringify(b.assert_stdout_regex ?? []));
537
+ if (byStdoutRegex !== 0)
538
+ return byStdoutRegex;
539
+ const byStderrContains = JSON.stringify(a.assert_stderr_contains ?? []).localeCompare(JSON.stringify(b.assert_stderr_contains ?? []));
540
+ if (byStderrContains !== 0)
541
+ return byStderrContains;
542
+ const byStderrRegex = JSON.stringify(a.assert_stderr_regex ?? []).localeCompare(JSON.stringify(b.assert_stderr_regex ?? []));
543
+ if (byStderrRegex !== 0)
544
+ return byStderrRegex;
545
+ const byStdoutMinLines = (a.assert_stdout_min_lines ?? 0) - (b.assert_stdout_min_lines ?? 0);
546
+ if (byStdoutMinLines !== 0)
547
+ return byStdoutMinLines;
548
+ const byJsonFieldEquals = JSON.stringify(a.assert_json_field_equals ?? {}).localeCompare(JSON.stringify(b.assert_json_field_equals ?? {}));
549
+ if (byJsonFieldEquals !== 0)
550
+ return byJsonFieldEquals;
551
+ const byJsonFieldGte = JSON.stringify(a.assert_json_field_gte ?? {}).localeCompare(JSON.stringify(b.assert_json_field_gte ?? {}));
552
+ if (byJsonFieldGte !== 0)
553
+ return byJsonFieldGte;
173
554
  return (a.note ?? "").localeCompare(b.note ?? "");
174
555
  });
175
556
  }
@@ -192,6 +573,19 @@ function sortDocs(values) {
192
573
  return (a.note ?? "").localeCompare(b.note ?? "");
193
574
  });
194
575
  }
576
+ function normalizeTypeOptions(values) {
577
+ if (!values) {
578
+ return undefined;
579
+ }
580
+ const normalizedEntries = Object.entries(values)
581
+ .map(([key, value]) => [key.trim(), value.trim()])
582
+ .filter(([key, value]) => key.length > 0 && value.length > 0)
583
+ .sort((left, right) => left[0].localeCompare(right[0]));
584
+ if (normalizedEntries.length === 0) {
585
+ return undefined;
586
+ }
587
+ return Object.fromEntries(normalizedEntries);
588
+ }
195
589
  function normalizeBody(body) {
196
590
  return body.replace(/^\n+/, "").replace(/\s+$/, "");
197
591
  }
@@ -224,7 +618,9 @@ function normalizeSeverityValue(value) {
224
618
  }
225
619
  return undefined;
226
620
  }
227
- export function normalizeFrontMatter(frontMatter) {
621
+ export function normalizeFrontMatter(frontMatter, options = {}) {
622
+ const runtimeContext = resolveRuntimeSchemaValidationContext(options);
623
+ const normalizedStatus = normalizeStatusInput(frontMatter.status, runtimeContext?.statusRegistry) ?? frontMatter.status;
228
624
  const tags = Array.from(new Set(frontMatter.tags.map((tag) => tag.trim().toLowerCase()).filter(Boolean))).sort((a, b) => a.localeCompare(b));
229
625
  const normalized = {
230
626
  id: frontMatter.id,
@@ -232,7 +628,8 @@ export function normalizeFrontMatter(frontMatter) {
232
628
  description: frontMatter.description,
233
629
  type: frontMatter.type,
234
630
  source_type: frontMatter.source_type?.trim() || undefined,
235
- status: frontMatter.status,
631
+ type_options: normalizeTypeOptions(frontMatter.type_options),
632
+ status: normalizedStatus,
236
633
  priority: frontMatter.priority,
237
634
  tags,
238
635
  created_at: frontMatter.created_at,
@@ -241,10 +638,13 @@ export function normalizeFrontMatter(frontMatter) {
241
638
  comments: sortLogValues(frontMatter.comments),
242
639
  notes: sortLogValues(frontMatter.notes),
243
640
  learnings: sortLogValues(frontMatter.learnings),
244
- files: sortFiles(frontMatter.files),
641
+ files: normalizeFiles(frontMatter.files),
245
642
  tests: sortTests(frontMatter.tests),
643
+ test_runs: normalizeTestRunSummaries(frontMatter.test_runs),
246
644
  docs: sortDocs(frontMatter.docs),
247
645
  deadline: frontMatter.deadline || undefined,
646
+ reminders: sortReminders(frontMatter.reminders),
647
+ events: sortEvents(frontMatter.events),
248
648
  closed_at: frontMatter.closed_at || undefined,
249
649
  assignee: frontMatter.assignee?.trim() || undefined,
250
650
  source_owner: frontMatter.source_owner?.trim() || undefined,
@@ -284,6 +684,36 @@ export function normalizeFrontMatter(frontMatter) {
284
684
  customer_impact: frontMatter.customer_impact?.trim() || undefined,
285
685
  close_reason: frontMatter.close_reason || undefined,
286
686
  };
687
+ const sourceRecord = frontMatter;
688
+ for (const [key, value] of Object.entries(sourceRecord)) {
689
+ if (Object.prototype.hasOwnProperty.call(normalized, key) || value === undefined) {
690
+ continue;
691
+ }
692
+ normalized[key] = value;
693
+ }
694
+ if (runtimeContext?.fieldRegistry) {
695
+ for (const definition of runtimeContext.fieldRegistry.definitions) {
696
+ const currentValue = normalized[definition.front_matter_key];
697
+ if (currentValue === undefined) {
698
+ continue;
699
+ }
700
+ normalized[definition.front_matter_key] = coerceRuntimeFieldValue(definition, currentValue, `metadata field "${definition.front_matter_key}"`);
701
+ }
702
+ }
703
+ if (runtimeContext && runtimeContext.unknownFieldPolicy !== "allow") {
704
+ const knownKeys = new Set(STATIC_FRONT_MATTER_FIELD_SET);
705
+ for (const definition of runtimeContext.fieldRegistry?.definitions ?? []) {
706
+ knownKeys.add(definition.front_matter_key);
707
+ }
708
+ const unknownKeys = Object.keys(normalized)
709
+ .filter((key) => !knownKeys.has(key))
710
+ .sort((left, right) => left.localeCompare(right));
711
+ if (unknownKeys.length > 0) {
712
+ if (runtimeContext.unknownFieldPolicy === "reject") {
713
+ validationError(`unknown schema fields are not allowed: ${unknownKeys.join(", ")}`);
714
+ }
715
+ }
716
+ }
287
717
  for (const [key, value] of Object.entries(normalized)) {
288
718
  if (value === undefined) {
289
719
  delete normalized[key];
@@ -343,7 +773,7 @@ export function splitFrontMatter(content) {
343
773
  const body = content.slice(end + 1).replace(/^\r?\n+/, "");
344
774
  return { frontMatter, body };
345
775
  }
346
- export function parseItemDocument(content) {
776
+ function parseJsonMarkdownItemDocument(content, runtimeContext, options = {}) {
347
777
  const { frontMatter, body } = splitFrontMatter(content);
348
778
  if (!frontMatter) {
349
779
  const trimmed = content.trimStart();
@@ -359,14 +789,40 @@ export function parseItemDocument(content) {
359
789
  catch {
360
790
  validationError("JSON front matter is not valid JSON");
361
791
  }
362
- assertValidFrontMatter(parsed);
792
+ assertValidFrontMatter(parsed, runtimeContext);
363
793
  return {
364
- front_matter: normalizeFrontMatter(parsed),
794
+ front_matter: normalizeFrontMatter(parsed, options),
365
795
  body: normalizeBody(body),
366
796
  };
367
797
  }
368
- export function serializeItemDocument(document) {
369
- const normalizedFrontMatter = normalizeFrontMatter(document.front_matter);
798
+ function parseToonItemDocument(content, runtimeContext, options = {}) {
799
+ let parsed;
800
+ try {
801
+ parsed = decodeToon(content);
802
+ }
803
+ catch {
804
+ validationError("TOON item document is not valid TOON");
805
+ }
806
+ assertFrontMatterCondition(typeof parsed === "object" && parsed !== null && !Array.isArray(parsed), "TOON item document must be an object");
807
+ const record = parsed;
808
+ if (Object.prototype.hasOwnProperty.call(record, "front_matter")) {
809
+ assertValidFrontMatter(record.front_matter, runtimeContext);
810
+ assertFrontMatterCondition(record.body === undefined || typeof record.body === "string", "TOON item document body must be a string");
811
+ return {
812
+ front_matter: normalizeFrontMatter(record.front_matter, options),
813
+ body: normalizeBody(typeof record.body === "string" ? record.body : ""),
814
+ };
815
+ }
816
+ const { body, ...frontMatterRecord } = record;
817
+ assertFrontMatterCondition(body === undefined || typeof body === "string", "TOON item document body must be a string");
818
+ assertValidFrontMatter(frontMatterRecord, runtimeContext);
819
+ return {
820
+ front_matter: normalizeFrontMatter(frontMatterRecord, options),
821
+ body: normalizeBody(typeof body === "string" ? body : ""),
822
+ };
823
+ }
824
+ function serializeJsonMarkdownItemDocument(document, options = {}) {
825
+ const normalizedFrontMatter = normalizeFrontMatter(document.front_matter, options);
370
826
  const orderedFrontMatter = orderFrontMatter(normalizedFrontMatter);
371
827
  const serializedFrontMatter = JSON.stringify(orderedFrontMatter, null, 2);
372
828
  const normalizedBody = normalizeBody(document.body ?? "");
@@ -375,9 +831,36 @@ export function serializeItemDocument(document) {
375
831
  }
376
832
  return `${serializedFrontMatter}\n\n${normalizedBody}\n`;
377
833
  }
378
- export function canonicalDocument(document) {
834
+ function serializeToonItemDocument(document, options = {}) {
835
+ const normalizedFrontMatter = normalizeFrontMatter(document.front_matter, options);
836
+ const orderedFrontMatter = orderFrontMatter(normalizedFrontMatter);
837
+ const normalizedBody = normalizeBody(document.body ?? "");
838
+ return `${encodeToon({ ...orderedFrontMatter, body: normalizedBody })}\n`;
839
+ }
840
+ export function parseItemDocument(content, options = {}) {
841
+ const conflictMarker = findFirstMergeConflictMarker(content);
842
+ if (conflictMarker) {
843
+ throw new PmCliError(`Merge conflict markers detected in item document at line ${conflictMarker.line} (${conflictMarker.marker}). Resolve <<<<<<< ======= >>>>>>> markers and retry.`, EXIT_CODE.GENERIC_FAILURE, {
844
+ code: "merge_conflict_markers_detected",
845
+ required: "Resolve merge-conflict markers in the item file before parsing or mutation commands.",
846
+ why: "Partially merged documents can corrupt item metadata and history integrity.",
847
+ examples: ["git status", "git add <resolved-file> && git commit"],
848
+ nextSteps: ["Resolve conflicts, save the file, then rerun the pm command."],
849
+ });
850
+ }
851
+ const format = options.format ?? "json_markdown";
852
+ const runtimeContext = resolveRuntimeSchemaValidationContext(options);
853
+ return format === "toon"
854
+ ? parseToonItemDocument(content, runtimeContext, options)
855
+ : parseJsonMarkdownItemDocument(content, runtimeContext, options);
856
+ }
857
+ export function serializeItemDocument(document, options = {}) {
858
+ const format = options.format ?? "json_markdown";
859
+ return format === "toon" ? serializeToonItemDocument(document, options) : serializeJsonMarkdownItemDocument(document, options);
860
+ }
861
+ export function canonicalDocument(document, options = {}) {
379
862
  return {
380
- front_matter: normalizeFrontMatter(document.front_matter),
863
+ front_matter: normalizeFrontMatter(document.front_matter, options),
381
864
  body: normalizeBody(document.body ?? ""),
382
865
  };
383
866
  }