@unbrained/pm-cli 2026.3.9 → 2026.5.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.
- package/.agents/pm/extensions/.managed-extensions.json +42 -0
- package/.agents/pm/extensions/beads/index.js +109 -0
- package/.agents/pm/extensions/beads/manifest.json +7 -0
- package/{dist/cli/commands/beads.js → .agents/pm/extensions/beads/runtime.js} +167 -56
- package/.agents/pm/extensions/beads/runtime.ts +702 -0
- package/.agents/pm/extensions/todos/index.js +126 -0
- package/.agents/pm/extensions/todos/manifest.json +7 -0
- package/{dist/extensions/builtins/todos/import-export.js → .agents/pm/extensions/todos/runtime.js} +39 -29
- package/.agents/pm/extensions/todos/runtime.ts +568 -0
- package/AGENTS.md +212 -94
- package/CHANGELOG.md +399 -0
- package/CODE_OF_CONDUCT.md +42 -0
- package/CONTRIBUTING.md +144 -0
- package/PRD.md +522 -173
- package/README.md +996 -495
- package/SECURITY.md +51 -0
- package/dist/cli/commands/activity.d.ts +5 -0
- package/dist/cli/commands/activity.js +66 -3
- package/dist/cli/commands/activity.js.map +1 -1
- package/dist/cli/commands/aggregate.d.ts +54 -0
- package/dist/cli/commands/aggregate.js +181 -0
- package/dist/cli/commands/aggregate.js.map +1 -0
- package/dist/cli/commands/append.js +4 -1
- package/dist/cli/commands/append.js.map +1 -1
- package/dist/cli/commands/calendar.d.ts +109 -0
- package/dist/cli/commands/calendar.js +797 -0
- package/dist/cli/commands/calendar.js.map +1 -0
- package/dist/cli/commands/claim.d.ts +5 -1
- package/dist/cli/commands/claim.js +42 -21
- package/dist/cli/commands/claim.js.map +1 -1
- package/dist/cli/commands/close.d.ts +1 -0
- package/dist/cli/commands/close.js +54 -5
- package/dist/cli/commands/close.js.map +1 -1
- package/dist/cli/commands/comments-audit.d.ts +91 -0
- package/dist/cli/commands/comments-audit.js +195 -0
- package/dist/cli/commands/comments-audit.js.map +1 -0
- package/dist/cli/commands/comments.d.ts +1 -0
- package/dist/cli/commands/comments.js +70 -21
- package/dist/cli/commands/comments.js.map +1 -1
- package/dist/cli/commands/completion.d.ts +10 -4
- package/dist/cli/commands/completion.js +1240 -193
- package/dist/cli/commands/completion.js.map +1 -1
- package/dist/cli/commands/config.d.ts +35 -3
- package/dist/cli/commands/config.js +968 -13
- package/dist/cli/commands/config.js.map +1 -1
- package/dist/cli/commands/context.d.ts +86 -0
- package/dist/cli/commands/context.js +299 -0
- package/dist/cli/commands/context.js.map +1 -0
- package/dist/cli/commands/contracts.d.ts +78 -0
- package/dist/cli/commands/contracts.js +920 -0
- package/dist/cli/commands/contracts.js.map +1 -0
- package/dist/cli/commands/create.d.ts +48 -14
- package/dist/cli/commands/create.js +1331 -160
- package/dist/cli/commands/create.js.map +1 -1
- package/dist/cli/commands/dedupe-audit.d.ts +81 -0
- package/dist/cli/commands/dedupe-audit.js +330 -0
- package/dist/cli/commands/dedupe-audit.js.map +1 -0
- package/dist/cli/commands/deps.d.ts +52 -0
- package/dist/cli/commands/deps.js +204 -0
- package/dist/cli/commands/deps.js.map +1 -0
- package/dist/cli/commands/docs.d.ts +19 -0
- package/dist/cli/commands/docs.js +212 -13
- package/dist/cli/commands/docs.js.map +1 -1
- package/dist/cli/commands/extension.d.ts +122 -0
- package/dist/cli/commands/extension.js +1850 -0
- package/dist/cli/commands/extension.js.map +1 -0
- package/dist/cli/commands/files.d.ts +52 -1
- package/dist/cli/commands/files.js +443 -13
- package/dist/cli/commands/files.js.map +1 -1
- package/dist/cli/commands/gc.d.ts +11 -1
- package/dist/cli/commands/gc.js +89 -11
- package/dist/cli/commands/gc.js.map +1 -1
- package/dist/cli/commands/get.d.ts +13 -0
- package/dist/cli/commands/get.js +35 -3
- package/dist/cli/commands/get.js.map +1 -1
- package/dist/cli/commands/health.d.ts +10 -2
- package/dist/cli/commands/health.js +774 -23
- package/dist/cli/commands/health.js.map +1 -1
- package/dist/cli/commands/history.d.ts +20 -0
- package/dist/cli/commands/history.js +152 -6
- package/dist/cli/commands/history.js.map +1 -1
- package/dist/cli/commands/index.d.ts +16 -3
- package/dist/cli/commands/index.js +16 -3
- package/dist/cli/commands/index.js.map +1 -1
- package/dist/cli/commands/init.d.ts +7 -2
- package/dist/cli/commands/init.js +137 -5
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/learnings.d.ts +17 -0
- package/dist/cli/commands/learnings.js +129 -0
- package/dist/cli/commands/learnings.js.map +1 -0
- package/dist/cli/commands/list.d.ts +29 -1
- package/dist/cli/commands/list.js +292 -56
- package/dist/cli/commands/list.js.map +1 -1
- package/dist/cli/commands/normalize.d.ts +51 -0
- package/dist/cli/commands/normalize.js +298 -0
- package/dist/cli/commands/normalize.js.map +1 -0
- package/dist/cli/commands/notes.d.ts +17 -0
- package/dist/cli/commands/notes.js +129 -0
- package/dist/cli/commands/notes.js.map +1 -0
- package/dist/cli/commands/reindex.d.ts +1 -0
- package/dist/cli/commands/reindex.js +208 -32
- package/dist/cli/commands/reindex.js.map +1 -1
- package/dist/cli/commands/restore.js +164 -30
- package/dist/cli/commands/restore.js.map +1 -1
- package/dist/cli/commands/search.d.ts +14 -1
- package/dist/cli/commands/search.js +479 -85
- package/dist/cli/commands/search.js.map +1 -1
- package/dist/cli/commands/stats.js +26 -10
- package/dist/cli/commands/stats.js.map +1 -1
- package/dist/cli/commands/templates.d.ts +26 -0
- package/dist/cli/commands/templates.js +179 -0
- package/dist/cli/commands/templates.js.map +1 -0
- package/dist/cli/commands/test-all.d.ts +19 -1
- package/dist/cli/commands/test-all.js +161 -13
- package/dist/cli/commands/test-all.js.map +1 -1
- package/dist/cli/commands/test-runs.d.ts +63 -0
- package/dist/cli/commands/test-runs.js +179 -0
- package/dist/cli/commands/test-runs.js.map +1 -0
- package/dist/cli/commands/test.d.ts +75 -1
- package/dist/cli/commands/test.js +1360 -41
- package/dist/cli/commands/test.js.map +1 -1
- package/dist/cli/commands/update-many.d.ts +57 -0
- package/dist/cli/commands/update-many.js +631 -0
- package/dist/cli/commands/update-many.js.map +1 -0
- package/dist/cli/commands/update.d.ts +30 -0
- package/dist/cli/commands/update.js +1393 -84
- package/dist/cli/commands/update.js.map +1 -1
- package/dist/cli/commands/validate.d.ts +30 -0
- package/dist/cli/commands/validate.js +1140 -0
- package/dist/cli/commands/validate.js.map +1 -0
- package/dist/cli/error-guidance.d.ts +33 -0
- package/dist/cli/error-guidance.js +337 -0
- package/dist/cli/error-guidance.js.map +1 -0
- package/dist/cli/extension-command-options.d.ts +1 -0
- package/dist/cli/extension-command-options.js +92 -0
- package/dist/cli/extension-command-options.js.map +1 -1
- package/dist/cli/help-content.d.ts +20 -0
- package/dist/cli/help-content.js +543 -0
- package/dist/cli/help-content.js.map +1 -0
- package/dist/cli/main.js +3648 -467
- package/dist/cli/main.js.map +1 -1
- package/dist/core/extensions/index.d.ts +13 -1
- package/dist/core/extensions/index.js +108 -1
- package/dist/core/extensions/index.js.map +1 -1
- package/dist/core/extensions/item-fields.d.ts +2 -0
- package/dist/core/extensions/item-fields.js +79 -0
- package/dist/core/extensions/item-fields.js.map +1 -0
- package/dist/core/extensions/loader.d.ts +322 -9
- package/dist/core/extensions/loader.js +911 -20
- package/dist/core/extensions/loader.js.map +1 -1
- package/dist/core/extensions/runtime-registrations.d.ts +5 -0
- package/dist/core/extensions/runtime-registrations.js +51 -0
- package/dist/core/extensions/runtime-registrations.js.map +1 -0
- package/dist/core/history/history-stream-policy.d.ts +20 -0
- package/dist/core/history/history-stream-policy.js +53 -0
- package/dist/core/history/history-stream-policy.js.map +1 -0
- package/dist/core/history/history.js +90 -1
- package/dist/core/history/history.js.map +1 -1
- package/dist/core/item/id.d.ts +1 -0
- package/dist/core/item/id.js +10 -3
- package/dist/core/item/id.js.map +1 -1
- package/dist/core/item/index.d.ts +1 -0
- package/dist/core/item/index.js +1 -0
- package/dist/core/item/index.js.map +1 -1
- package/dist/core/item/item-format.d.ts +11 -5
- package/dist/core/item/item-format.js +532 -28
- package/dist/core/item/item-format.js.map +1 -1
- package/dist/core/item/parent-reference-policy.d.ts +6 -0
- package/dist/core/item/parent-reference-policy.js +32 -0
- package/dist/core/item/parent-reference-policy.js.map +1 -0
- package/dist/core/item/parse.d.ts +5 -0
- package/dist/core/item/parse.js +216 -19
- package/dist/core/item/parse.js.map +1 -1
- package/dist/core/item/sprint-release-format.d.ts +6 -0
- package/dist/core/item/sprint-release-format.js +33 -0
- package/dist/core/item/sprint-release-format.js.map +1 -0
- package/dist/core/item/status.d.ts +3 -0
- package/dist/core/item/status.js +24 -0
- package/dist/core/item/status.js.map +1 -0
- package/dist/core/item/type-registry.d.ts +37 -0
- package/dist/core/item/type-registry.js +706 -0
- package/dist/core/item/type-registry.js.map +1 -0
- package/dist/core/lock/lock.d.ts +1 -1
- package/dist/core/lock/lock.js +101 -12
- package/dist/core/lock/lock.js.map +1 -1
- package/dist/core/output/command-aware.d.ts +1 -0
- package/dist/core/output/command-aware.js +394 -0
- package/dist/core/output/command-aware.js.map +1 -0
- package/dist/core/output/output.d.ts +3 -0
- package/dist/core/output/output.js +124 -6
- package/dist/core/output/output.js.map +1 -1
- package/dist/core/schema/runtime-field-filters.d.ts +3 -0
- package/dist/core/schema/runtime-field-filters.js +39 -0
- package/dist/core/schema/runtime-field-filters.js.map +1 -0
- package/dist/core/schema/runtime-field-values.d.ts +8 -0
- package/dist/core/schema/runtime-field-values.js +154 -0
- package/dist/core/schema/runtime-field-values.js.map +1 -0
- package/dist/core/schema/runtime-schema.d.ts +68 -0
- package/dist/core/schema/runtime-schema.js +554 -0
- package/dist/core/schema/runtime-schema.js.map +1 -0
- package/dist/core/search/cache.d.ts +13 -1
- package/dist/core/search/cache.js +123 -14
- package/dist/core/search/cache.js.map +1 -1
- package/dist/core/search/semantic-defaults.d.ts +6 -0
- package/dist/core/search/semantic-defaults.js +120 -0
- package/dist/core/search/semantic-defaults.js.map +1 -0
- package/dist/core/search/vector-stores.js +3 -1
- package/dist/core/search/vector-stores.js.map +1 -1
- package/dist/core/shared/command-types.d.ts +2 -0
- package/dist/core/shared/conflict-markers.d.ts +7 -0
- package/dist/core/shared/conflict-markers.js +27 -0
- package/dist/core/shared/conflict-markers.js.map +1 -0
- package/dist/core/shared/constants.d.ts +15 -4
- package/dist/core/shared/constants.js +146 -1
- package/dist/core/shared/constants.js.map +1 -1
- package/dist/core/shared/errors.d.ts +10 -1
- package/dist/core/shared/errors.js +3 -1
- package/dist/core/shared/errors.js.map +1 -1
- package/dist/core/shared/text-normalization.d.ts +4 -0
- package/dist/core/shared/text-normalization.js +33 -0
- package/dist/core/shared/text-normalization.js.map +1 -0
- package/dist/core/shared/time.d.ts +3 -2
- package/dist/core/shared/time.js +109 -11
- package/dist/core/shared/time.js.map +1 -1
- package/dist/core/store/index.d.ts +1 -0
- package/dist/core/store/index.js +1 -0
- package/dist/core/store/index.js.map +1 -1
- package/dist/core/store/item-format-migration.d.ts +9 -0
- package/dist/core/store/item-format-migration.js +87 -0
- package/dist/core/store/item-format-migration.js.map +1 -0
- package/dist/core/store/item-store.d.ts +13 -4
- package/dist/core/store/item-store.js +243 -52
- package/dist/core/store/item-store.js.map +1 -1
- package/dist/core/store/paths.d.ts +21 -3
- package/dist/core/store/paths.js +59 -4
- package/dist/core/store/paths.js.map +1 -1
- package/dist/core/store/settings.d.ts +14 -1
- package/dist/core/store/settings.js +463 -7
- package/dist/core/store/settings.js.map +1 -1
- package/dist/core/telemetry/consent.d.ts +2 -0
- package/dist/core/telemetry/consent.js +79 -0
- package/dist/core/telemetry/consent.js.map +1 -0
- package/dist/core/telemetry/runtime.d.ts +38 -0
- package/dist/core/telemetry/runtime.js +733 -0
- package/dist/core/telemetry/runtime.js.map +1 -0
- package/dist/core/test/background-runs.d.ts +117 -0
- package/dist/core/test/background-runs.js +760 -0
- package/dist/core/test/background-runs.js.map +1 -0
- package/dist/core/test/item-test-run-tracking.d.ts +9 -0
- package/dist/core/test/item-test-run-tracking.js +50 -0
- package/dist/core/test/item-test-run-tracking.js.map +1 -0
- package/dist/sdk/cli-contracts.d.ts +92 -0
- package/dist/sdk/cli-contracts.js +2357 -0
- package/dist/sdk/cli-contracts.js.map +1 -0
- package/dist/sdk/index.d.ts +34 -0
- package/dist/sdk/index.js +23 -0
- package/dist/sdk/index.js.map +1 -0
- package/dist/types.d.ts +204 -4
- package/dist/types.js +56 -1
- package/dist/types.js.map +1 -1
- package/docs/ARCHITECTURE.md +369 -40
- package/docs/EXTENSIONS.md +454 -49
- package/docs/RELEASING.md +68 -19
- package/docs/SDK.md +123 -0
- package/docs/examples/starter-extension/README.md +48 -0
- package/docs/examples/starter-extension/index.js +191 -0
- package/docs/examples/starter-extension/manifest.json +17 -0
- package/docs/examples/starter-extension/package.json +10 -0
- package/package.json +33 -6
- package/scripts/install.ps1 +2 -1
- package/scripts/install.sh +2 -1
- package/.pi/extensions/pm-cli/index.ts +0 -778
- package/dist/cli/commands/beads.d.ts +0 -15
- package/dist/cli/commands/beads.js.map +0 -1
- package/dist/cli/commands/install.d.ts +0 -18
- package/dist/cli/commands/install.js +0 -87
- package/dist/cli/commands/install.js.map +0 -1
- package/dist/core/extensions/builtins.d.ts +0 -3
- package/dist/core/extensions/builtins.js +0 -47
- package/dist/core/extensions/builtins.js.map +0 -1
- package/dist/extensions/builtins/beads/index.d.ts +0 -8
- package/dist/extensions/builtins/beads/index.js +0 -29
- package/dist/extensions/builtins/beads/index.js.map +0 -1
- package/dist/extensions/builtins/todos/import-export.d.ts +0 -26
- package/dist/extensions/builtins/todos/import-export.js.map +0 -1
- package/dist/extensions/builtins/todos/index.d.ts +0 -8
- package/dist/extensions/builtins/todos/index.js +0 -38
- package/dist/extensions/builtins/todos/index.js.map +0 -1
|
@@ -1,7 +1,14 @@
|
|
|
1
|
-
import {
|
|
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";
|
|
10
|
+
import { compareTimestampStrings, isTimestampLiteral } from "../shared/time.js";
|
|
11
|
+
const LINKED_TEST_PM_CONTEXT_MODE_VALUES = new Set(["schema", "tracker", "auto"]);
|
|
5
12
|
function normalizePathValue(value) {
|
|
6
13
|
return value.replaceAll("\\", "/");
|
|
7
14
|
}
|
|
@@ -12,6 +19,30 @@ const REQUIRED_STRING_FIELDS = [
|
|
|
12
19
|
"created_at",
|
|
13
20
|
"updated_at",
|
|
14
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
|
+
}
|
|
15
46
|
function validationError(message) {
|
|
16
47
|
throw new PmCliError(`Invalid item front matter: ${message}`, EXIT_CODE.GENERIC_FAILURE);
|
|
17
48
|
}
|
|
@@ -24,18 +55,64 @@ function assertTimestampField(record, fieldName) {
|
|
|
24
55
|
const rawValue = record[fieldName];
|
|
25
56
|
assertFrontMatterCondition(typeof rawValue === "string", `${fieldName} must be a string`);
|
|
26
57
|
const timestamp = rawValue;
|
|
27
|
-
assertFrontMatterCondition(
|
|
58
|
+
assertFrontMatterCondition(isTimestampLiteral(timestamp), `${fieldName} must be a valid ISO timestamp`);
|
|
28
59
|
}
|
|
29
|
-
function
|
|
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) {
|
|
30
101
|
assertFrontMatterCondition(typeof frontMatter === "object" && frontMatter !== null && !Array.isArray(frontMatter), "front matter must be an object");
|
|
31
102
|
const record = frontMatter;
|
|
32
103
|
for (const fieldName of REQUIRED_STRING_FIELDS) {
|
|
33
104
|
assertFrontMatterCondition(typeof record[fieldName] === "string", `${fieldName} is required and must be a string`);
|
|
34
105
|
}
|
|
35
106
|
const itemType = record.type;
|
|
36
|
-
assertFrontMatterCondition(typeof itemType === "string" &&
|
|
107
|
+
assertFrontMatterCondition(typeof itemType === "string" && itemType.trim().length > 0, "type must be a non-empty string");
|
|
37
108
|
const status = record.status;
|
|
38
|
-
assertFrontMatterCondition(typeof status === "string" &&
|
|
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(", ")}`);
|
|
39
116
|
const priority = record.priority;
|
|
40
117
|
assertFrontMatterCondition(typeof priority === "number" && Number.isInteger(priority) && [0, 1, 2, 3, 4].includes(priority), "priority must be an integer 0..4");
|
|
41
118
|
const tags = record.tags;
|
|
@@ -77,6 +154,101 @@ function assertValidFrontMatter(frontMatter) {
|
|
|
77
154
|
if (record.deadline !== undefined) {
|
|
78
155
|
assertTimestampField(record, "deadline");
|
|
79
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
|
+
}
|
|
199
|
+
if (record.closed_at !== undefined) {
|
|
200
|
+
const closedAt = record.closed_at;
|
|
201
|
+
assertFrontMatterCondition(typeof closedAt === "string", "closed_at must be a string");
|
|
202
|
+
assertFrontMatterCondition(isTimestampLiteral(closedAt), "closed_at must be a valid ISO timestamp");
|
|
203
|
+
}
|
|
204
|
+
for (const fieldName of ["source_type", "source_owner", "design", "external_ref"]) {
|
|
205
|
+
const value = record[fieldName];
|
|
206
|
+
if (value !== undefined) {
|
|
207
|
+
assertFrontMatterCondition(typeof value === "string", `${fieldName} must be a string`);
|
|
208
|
+
}
|
|
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
|
+
}
|
|
80
252
|
}
|
|
81
253
|
function sortDependencies(values) {
|
|
82
254
|
if (!values || values.length === 0)
|
|
@@ -87,22 +259,26 @@ function sortDependencies(values) {
|
|
|
87
259
|
kind: value.kind,
|
|
88
260
|
created_at: value.created_at,
|
|
89
261
|
author: value.author?.trim() || undefined,
|
|
262
|
+
source_kind: value.source_kind?.trim() || undefined,
|
|
90
263
|
}))
|
|
91
264
|
.sort((a, b) => {
|
|
92
|
-
const byCreated = a.created_at
|
|
265
|
+
const byCreated = compareTimestampStrings(a.created_at, b.created_at);
|
|
93
266
|
if (byCreated !== 0)
|
|
94
267
|
return byCreated;
|
|
95
268
|
const byId = a.id.localeCompare(b.id);
|
|
96
269
|
if (byId !== 0)
|
|
97
270
|
return byId;
|
|
98
|
-
|
|
271
|
+
const byKind = a.kind.localeCompare(b.kind);
|
|
272
|
+
if (byKind !== 0)
|
|
273
|
+
return byKind;
|
|
274
|
+
return (a.source_kind ?? "").localeCompare(b.source_kind ?? "");
|
|
99
275
|
});
|
|
100
276
|
}
|
|
101
277
|
function sortLogValues(values) {
|
|
102
278
|
if (!values || values.length === 0)
|
|
103
279
|
return undefined;
|
|
104
280
|
return [...values].sort((a, b) => {
|
|
105
|
-
const byCreated = a.created_at
|
|
281
|
+
const byCreated = compareTimestampStrings(a.created_at, b.created_at);
|
|
106
282
|
if (byCreated !== 0)
|
|
107
283
|
return byCreated;
|
|
108
284
|
const byText = a.text.localeCompare(b.text);
|
|
@@ -111,24 +287,166 @@ function sortLogValues(values) {
|
|
|
111
287
|
return a.author.localeCompare(b.author);
|
|
112
288
|
});
|
|
113
289
|
}
|
|
114
|
-
function
|
|
290
|
+
function sortReminders(values) {
|
|
115
291
|
if (!values || values.length === 0)
|
|
116
292
|
return undefined;
|
|
117
|
-
|
|
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
|
|
118
394
|
.map((value) => ({
|
|
119
395
|
path: normalizePathValue(value.path),
|
|
120
396
|
scope: value.scope,
|
|
121
397
|
note: value.note?.trim() || undefined,
|
|
122
|
-
}))
|
|
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)
|
|
123
440
|
.sort((a, b) => {
|
|
124
|
-
const
|
|
125
|
-
if (
|
|
126
|
-
return
|
|
127
|
-
const
|
|
128
|
-
if (
|
|
129
|
-
return
|
|
130
|
-
return
|
|
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);
|
|
131
448
|
});
|
|
449
|
+
return normalized.length > 0 ? normalized : undefined;
|
|
132
450
|
}
|
|
133
451
|
function sortTests(values) {
|
|
134
452
|
if (!values || values.length === 0)
|
|
@@ -139,6 +457,52 @@ function sortTests(values) {
|
|
|
139
457
|
path: value.path ? normalizePathValue(value.path) : undefined,
|
|
140
458
|
scope: value.scope,
|
|
141
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,
|
|
142
506
|
note: value.note?.trim() || undefined,
|
|
143
507
|
}))
|
|
144
508
|
.sort((a, b) => {
|
|
@@ -154,6 +518,39 @@ function sortTests(values) {
|
|
|
154
518
|
const byTimeout = (a.timeout_seconds ?? 0) - (b.timeout_seconds ?? 0);
|
|
155
519
|
if (byTimeout !== 0)
|
|
156
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;
|
|
157
554
|
return (a.note ?? "").localeCompare(b.note ?? "");
|
|
158
555
|
});
|
|
159
556
|
}
|
|
@@ -176,6 +573,19 @@ function sortDocs(values) {
|
|
|
176
573
|
return (a.note ?? "").localeCompare(b.note ?? "");
|
|
177
574
|
});
|
|
178
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
|
+
}
|
|
179
589
|
function normalizeBody(body) {
|
|
180
590
|
return body.replace(/^\n+/, "").replace(/\s+$/, "");
|
|
181
591
|
}
|
|
@@ -208,14 +618,18 @@ function normalizeSeverityValue(value) {
|
|
|
208
618
|
}
|
|
209
619
|
return undefined;
|
|
210
620
|
}
|
|
211
|
-
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;
|
|
212
624
|
const tags = Array.from(new Set(frontMatter.tags.map((tag) => tag.trim().toLowerCase()).filter(Boolean))).sort((a, b) => a.localeCompare(b));
|
|
213
625
|
const normalized = {
|
|
214
626
|
id: frontMatter.id,
|
|
215
627
|
title: frontMatter.title,
|
|
216
628
|
description: frontMatter.description,
|
|
217
629
|
type: frontMatter.type,
|
|
218
|
-
|
|
630
|
+
source_type: frontMatter.source_type?.trim() || undefined,
|
|
631
|
+
type_options: normalizeTypeOptions(frontMatter.type_options),
|
|
632
|
+
status: normalizedStatus,
|
|
219
633
|
priority: frontMatter.priority,
|
|
220
634
|
tags,
|
|
221
635
|
created_at: frontMatter.created_at,
|
|
@@ -224,14 +638,21 @@ export function normalizeFrontMatter(frontMatter) {
|
|
|
224
638
|
comments: sortLogValues(frontMatter.comments),
|
|
225
639
|
notes: sortLogValues(frontMatter.notes),
|
|
226
640
|
learnings: sortLogValues(frontMatter.learnings),
|
|
227
|
-
files:
|
|
641
|
+
files: normalizeFiles(frontMatter.files),
|
|
228
642
|
tests: sortTests(frontMatter.tests),
|
|
643
|
+
test_runs: normalizeTestRunSummaries(frontMatter.test_runs),
|
|
229
644
|
docs: sortDocs(frontMatter.docs),
|
|
230
645
|
deadline: frontMatter.deadline || undefined,
|
|
646
|
+
reminders: sortReminders(frontMatter.reminders),
|
|
647
|
+
events: sortEvents(frontMatter.events),
|
|
648
|
+
closed_at: frontMatter.closed_at || undefined,
|
|
231
649
|
assignee: frontMatter.assignee?.trim() || undefined,
|
|
650
|
+
source_owner: frontMatter.source_owner?.trim() || undefined,
|
|
232
651
|
author: frontMatter.author || undefined,
|
|
233
652
|
estimated_minutes: frontMatter.estimated_minutes,
|
|
234
653
|
acceptance_criteria: frontMatter.acceptance_criteria ?? undefined,
|
|
654
|
+
design: frontMatter.design ?? undefined,
|
|
655
|
+
external_ref: frontMatter.external_ref ?? undefined,
|
|
235
656
|
definition_of_ready: frontMatter.definition_of_ready?.trim() || undefined,
|
|
236
657
|
order: frontMatter.order,
|
|
237
658
|
goal: frontMatter.goal?.trim() || undefined,
|
|
@@ -263,6 +684,36 @@ export function normalizeFrontMatter(frontMatter) {
|
|
|
263
684
|
customer_impact: frontMatter.customer_impact?.trim() || undefined,
|
|
264
685
|
close_reason: frontMatter.close_reason || undefined,
|
|
265
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
|
+
}
|
|
266
717
|
for (const [key, value] of Object.entries(normalized)) {
|
|
267
718
|
if (value === undefined) {
|
|
268
719
|
delete normalized[key];
|
|
@@ -322,7 +773,7 @@ export function splitFrontMatter(content) {
|
|
|
322
773
|
const body = content.slice(end + 1).replace(/^\r?\n+/, "");
|
|
323
774
|
return { frontMatter, body };
|
|
324
775
|
}
|
|
325
|
-
|
|
776
|
+
function parseJsonMarkdownItemDocument(content, runtimeContext, options = {}) {
|
|
326
777
|
const { frontMatter, body } = splitFrontMatter(content);
|
|
327
778
|
if (!frontMatter) {
|
|
328
779
|
const trimmed = content.trimStart();
|
|
@@ -338,14 +789,40 @@ export function parseItemDocument(content) {
|
|
|
338
789
|
catch {
|
|
339
790
|
validationError("JSON front matter is not valid JSON");
|
|
340
791
|
}
|
|
341
|
-
assertValidFrontMatter(parsed);
|
|
792
|
+
assertValidFrontMatter(parsed, runtimeContext);
|
|
342
793
|
return {
|
|
343
|
-
front_matter: normalizeFrontMatter(parsed),
|
|
794
|
+
front_matter: normalizeFrontMatter(parsed, options),
|
|
344
795
|
body: normalizeBody(body),
|
|
345
796
|
};
|
|
346
797
|
}
|
|
347
|
-
|
|
348
|
-
|
|
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);
|
|
349
826
|
const orderedFrontMatter = orderFrontMatter(normalizedFrontMatter);
|
|
350
827
|
const serializedFrontMatter = JSON.stringify(orderedFrontMatter, null, 2);
|
|
351
828
|
const normalizedBody = normalizeBody(document.body ?? "");
|
|
@@ -354,9 +831,36 @@ export function serializeItemDocument(document) {
|
|
|
354
831
|
}
|
|
355
832
|
return `${serializedFrontMatter}\n\n${normalizedBody}\n`;
|
|
356
833
|
}
|
|
357
|
-
|
|
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 = {}) {
|
|
358
862
|
return {
|
|
359
|
-
front_matter: normalizeFrontMatter(document.front_matter),
|
|
863
|
+
front_matter: normalizeFrontMatter(document.front_matter, options),
|
|
360
864
|
body: normalizeBody(document.body ?? ""),
|
|
361
865
|
};
|
|
362
866
|
}
|