canopycms 0.8.3 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/CHANGELOG.md +73 -47
  2. package/dist/ai/json-to-markdown.js +2 -0
  3. package/dist/api/admin.d.ts +11 -0
  4. package/dist/api/admin.js +2 -0
  5. package/dist/api/archived-branches.d.ts +70 -0
  6. package/dist/api/archived-branches.js +146 -0
  7. package/dist/api/branch-entry-changes.js +2 -4
  8. package/dist/api/branch-merge.js +16 -1
  9. package/dist/api/branch-status.d.ts +11 -0
  10. package/dist/api/branch-status.js +2 -0
  11. package/dist/api/branch-withdraw.js +1 -1
  12. package/dist/api/branch.js +46 -108
  13. package/dist/api/client.d.ts +25 -21
  14. package/dist/api/client.js +46 -38
  15. package/dist/branch-archive.d.ts +19 -0
  16. package/dist/branch-archive.js +27 -0
  17. package/dist/branch-delete.d.ts +45 -0
  18. package/dist/branch-delete.js +97 -0
  19. package/dist/branch-metadata-file.d.ts +3 -0
  20. package/dist/branch-metadata-file.js +1 -0
  21. package/dist/branch-metadata.d.ts +6 -1
  22. package/dist/branch-metadata.js +15 -2
  23. package/dist/cli/cli.js +85 -16
  24. package/dist/cli/generate-ai-content.js +83 -15
  25. package/dist/cli/init.js +1 -1
  26. package/dist/config/types.d.ts +4 -4
  27. package/dist/config/types.js +1 -1
  28. package/dist/config/validation.d.ts +9 -3
  29. package/dist/config/validation.js +30 -5
  30. package/dist/content-listing.js +2 -3
  31. package/dist/content-store.js +5 -5
  32. package/dist/editor/BranchManager.d.ts +20 -0
  33. package/dist/editor/BranchManager.js +92 -26
  34. package/dist/editor/Editor.js +5 -1
  35. package/dist/editor/FormRenderer.js +5 -0
  36. package/dist/editor/admin/SystemHealthPanel.js +61 -6
  37. package/dist/editor/admin/useSystemHealth.d.ts +3 -0
  38. package/dist/editor/admin/useSystemHealth.js +59 -0
  39. package/dist/editor/branch-status.js +9 -0
  40. package/dist/editor/components/EntryChangesDrawer.js +3 -2
  41. package/dist/editor/copy.d.ts +3 -0
  42. package/dist/editor/copy.js +3 -0
  43. package/dist/editor/entry-diff.js +7 -1
  44. package/dist/editor/fields/DateField.d.ts +19 -0
  45. package/dist/editor/fields/DateField.js +53 -0
  46. package/dist/editor/fields/DateTimeField.d.ts +10 -0
  47. package/dist/editor/fields/DateTimeField.js +5 -15
  48. package/dist/editor/fields/list-item-title.js +2 -1
  49. package/dist/editor/hooks/useBranchManager.d.ts +5 -0
  50. package/dist/editor/hooks/useBranchManager.js +55 -4
  51. package/dist/editor/hooks/useBranchesData.d.ts +2 -0
  52. package/dist/editor/hooks/useBranchesData.js +3 -1
  53. package/dist/editor/theme.js +12 -4
  54. package/dist/entry-schema-registry.js +2 -1
  55. package/dist/static/index.js +5 -2
  56. package/dist/types.d.ts +33 -0
  57. package/dist/utils/calendar-day.d.ts +23 -0
  58. package/dist/utils/calendar-day.js +102 -0
  59. package/dist/utils/content-serialize.d.ts +4 -2
  60. package/dist/utils/content-serialize.js +8 -6
  61. package/dist/validation/entry-validator.js +7 -1
  62. package/dist/validation/field-traversal.js +1 -1
  63. package/dist/version.d.ts +1 -1
  64. package/dist/version.js +1 -1
  65. package/dist/worker/archived-sweep.d.ts +23 -0
  66. package/dist/worker/archived-sweep.js +137 -0
  67. package/dist/worker/cms-worker.d.ts +6 -0
  68. package/dist/worker/cms-worker.js +7 -0
  69. package/dist/worker/git-sync.d.ts +4 -4
  70. package/dist/worker/git-sync.js +3 -0
  71. package/dist/worker/github-commits.d.ts +2 -2
  72. package/dist/worker/github-commits.js +5 -2
  73. package/dist/worker/github-gateway-wire.d.ts +2 -2
  74. package/dist/worker/rebase.js +6 -2
  75. package/dist/worker/task-runner.js +7 -4
  76. package/dist/worker/worker-context.d.ts +2 -0
  77. package/package.json +1 -1
@@ -68,6 +68,10 @@ export class CanopyApiClient {
68
68
  markMerged: (params, options) => {
69
69
  return this.request('POST', this.buildPath('/:branch/mark-merged', params), undefined, 'standard', options);
70
70
  },
71
+ /** POST /:branch/archive */
72
+ archive: (params, options) => {
73
+ return this.request('POST', this.buildPath('/:branch/archive', params), undefined, 'standard', options);
74
+ },
71
75
  /** GET /:branch/status */
72
76
  getStatus: (params, options) => {
73
77
  return this.request('GET', this.buildPath('/:branch/status', params), undefined, 'standard', options);
@@ -77,6 +81,48 @@ export class CanopyApiClient {
77
81
  return this.request('POST', this.buildPath('/:branch/submit', params), body, 'long', options);
78
82
  },
79
83
  };
84
+ this.admin = {
85
+ /** POST /admin/archived-branches/delete */
86
+ deleteArchivedBranches: (body, options) => {
87
+ return this.request('POST', '/admin/archived-branches/delete', body, 'long', options);
88
+ },
89
+ /** GET /admin/branch-health */
90
+ branchHealth: (params, options) => {
91
+ return this.request('GET', this.buildPath('/admin/branch-health', params), undefined, 'long', options);
92
+ },
93
+ /** POST /admin/branch-dirs/:dirName/purge */
94
+ purgeBranchDir: (params, options) => {
95
+ return this.request('POST', this.buildPath('/admin/branch-dirs/:dirName/purge', params), undefined, 'long', options);
96
+ },
97
+ /** POST /admin/branch-dirs/:dirName/repair-metadata */
98
+ repairBranchDir: (params, options) => {
99
+ return this.request('POST', this.buildPath('/admin/branch-dirs/:dirName/repair-metadata', params), undefined, 'long', options);
100
+ },
101
+ /** POST /admin/branch-dirs/:dirName/dismiss-unpublished */
102
+ dismissUnpublished: (params, options) => {
103
+ return this.request('POST', this.buildPath('/admin/branch-dirs/:dirName/dismiss-unpublished', params), undefined, 'standard', options);
104
+ },
105
+ /** POST /admin/branch-dirs/:dirName/repair-content-duplicates */
106
+ repairContentDuplicates: (params, options) => {
107
+ return this.request('POST', this.buildPath('/admin/branch-dirs/:dirName/repair-content-duplicates', params), undefined, 'long', options);
108
+ },
109
+ /** GET /admin/status */
110
+ status: (options) => {
111
+ return this.request('GET', '/admin/status', undefined, 'standard', options);
112
+ },
113
+ /** GET /admin/tasks/:status */
114
+ listTasks: (params, options) => {
115
+ return this.request('GET', this.buildPath('/admin/tasks/:status', params), undefined, 'standard', options);
116
+ },
117
+ /** POST /admin/tasks/:taskId/retry */
118
+ retryTask: (params, options) => {
119
+ return this.request('POST', this.buildPath('/admin/tasks/:taskId/retry', params), undefined, 'standard', options);
120
+ },
121
+ /** DELETE /admin/tasks/:status/:fileName */
122
+ deleteTask: (params, options) => {
123
+ return this.request('DELETE', this.buildPath('/admin/tasks/:status/:fileName', params), undefined, 'standard', options);
124
+ },
125
+ };
80
126
  this.comments = {
81
127
  /** GET /:branch/comments */
82
128
  list: (params, options) => {
@@ -246,44 +292,6 @@ export class CanopyApiClient {
246
292
  return this.request('POST', this.buildPath('/:branch/schema/invalidate-cache', params), undefined, 'standard', options);
247
293
  },
248
294
  };
249
- this.admin = {
250
- /** GET /admin/branch-health */
251
- branchHealth: (params, options) => {
252
- return this.request('GET', this.buildPath('/admin/branch-health', params), undefined, 'long', options);
253
- },
254
- /** POST /admin/branch-dirs/:dirName/purge */
255
- purgeBranchDir: (params, options) => {
256
- return this.request('POST', this.buildPath('/admin/branch-dirs/:dirName/purge', params), undefined, 'long', options);
257
- },
258
- /** POST /admin/branch-dirs/:dirName/repair-metadata */
259
- repairBranchDir: (params, options) => {
260
- return this.request('POST', this.buildPath('/admin/branch-dirs/:dirName/repair-metadata', params), undefined, 'long', options);
261
- },
262
- /** POST /admin/branch-dirs/:dirName/dismiss-unpublished */
263
- dismissUnpublished: (params, options) => {
264
- return this.request('POST', this.buildPath('/admin/branch-dirs/:dirName/dismiss-unpublished', params), undefined, 'standard', options);
265
- },
266
- /** POST /admin/branch-dirs/:dirName/repair-content-duplicates */
267
- repairContentDuplicates: (params, options) => {
268
- return this.request('POST', this.buildPath('/admin/branch-dirs/:dirName/repair-content-duplicates', params), undefined, 'long', options);
269
- },
270
- /** GET /admin/status */
271
- status: (options) => {
272
- return this.request('GET', '/admin/status', undefined, 'standard', options);
273
- },
274
- /** GET /admin/tasks/:status */
275
- listTasks: (params, options) => {
276
- return this.request('GET', this.buildPath('/admin/tasks/:status', params), undefined, 'standard', options);
277
- },
278
- /** POST /admin/tasks/:taskId/retry */
279
- retryTask: (params, options) => {
280
- return this.request('POST', this.buildPath('/admin/tasks/:taskId/retry', params), undefined, 'standard', options);
281
- },
282
- /** DELETE /admin/tasks/:status/:fileName */
283
- deleteTask: (params, options) => {
284
- return this.request('DELETE', this.buildPath('/admin/tasks/:status/:fileName', params), undefined, 'standard', options);
285
- },
286
- };
287
295
  this.baseUrl = options.baseUrl ?? '/api/canopycms';
288
296
  // An unbound fetch throws "Illegal invocation" in browsers; Node has no window.
289
297
  this.fetchFn = options.fetch ?? (typeof window !== 'undefined' ? fetch.bind(window) : fetch);
@@ -0,0 +1,19 @@
1
+ import type { BranchMetadata } from './types.js';
2
+ type ArchiveFields = Pick<BranchMetadata, 'status' | 'archivedAt' | 'mergedAt' | 'unpublishedAtMerge' | 'rewrittenOnGitHub'>;
3
+ /** When the branch was archived, or undefined for one that records neither time. */
4
+ export declare const archivedTime: (branch: ArchiveFields) => number | undefined;
5
+ /** Archived before `cutoffMs`. `updatedAt` is no clock: any metadata write moves it. */
6
+ export declare const archivedBefore: (branch: ArchiveFields, cutoffMs: number) => boolean;
7
+ /** Why deletion by age leaves a branch for a person to delete. */
8
+ export type AgeDeletionHold = 'not-merged' | 'unpublished';
9
+ /**
10
+ * What keeps a branch from deletion by age, past any age: one archived without merging holds work
11
+ * that reached no base branch, and one merged with an unfinished submit, or rewritten on GitHub,
12
+ * may hold the only copy of commits the merge left out.
13
+ */
14
+ export declare const ageDeletionHold: (branch: ArchiveFields) => AgeDeletionHold | undefined;
15
+ /** The one rule the retention sweep and the admin bulk delete both select by. */
16
+ export declare const deletableByAge: (branch: ArchiveFields, cutoffMs: number) => boolean;
17
+ /** `now` less `days`, as the cutoff the functions above take. */
18
+ export declare const archivedCutoff: (days: number, now: number) => number;
19
+ export {};
@@ -0,0 +1,27 @@
1
+ /** When the branch was archived, or undefined for one that records neither time. */
2
+ export const archivedTime = (branch) => {
3
+ const recorded = branch.archivedAt ?? branch.mergedAt;
4
+ if (recorded === undefined)
5
+ return undefined;
6
+ const time = Date.parse(recorded);
7
+ return Number.isNaN(time) ? undefined : time;
8
+ };
9
+ /** Archived before `cutoffMs`. `updatedAt` is no clock: any metadata write moves it. */
10
+ export const archivedBefore = (branch, cutoffMs) => {
11
+ const time = archivedTime(branch);
12
+ return branch.status === 'archived' && time !== undefined && time < cutoffMs;
13
+ };
14
+ /**
15
+ * What keeps a branch from deletion by age, past any age: one archived without merging holds work
16
+ * that reached no base branch, and one merged with an unfinished submit, or rewritten on GitHub,
17
+ * may hold the only copy of commits the merge left out.
18
+ */
19
+ export const ageDeletionHold = (branch) => branch.mergedAt === undefined
20
+ ? 'not-merged'
21
+ : branch.unpublishedAtMerge === true || branch.rewrittenOnGitHub !== undefined
22
+ ? 'unpublished'
23
+ : undefined;
24
+ /** The one rule the retention sweep and the admin bulk delete both select by. */
25
+ export const deletableByAge = (branch, cutoffMs) => archivedBefore(branch, cutoffMs) && ageDeletionHold(branch) === undefined;
26
+ /** `now` less `days`, as the cutoff the functions above take. */
27
+ export const archivedCutoff = (days, now) => now - days * 24 * 60 * 60_000;
@@ -0,0 +1,45 @@
1
+ import type { BranchRegistry } from './branch-registry.js';
2
+ import type { BranchContext, BranchMetadata } from './types.js';
3
+ export interface DeleteBranchWorkspaceOptions {
4
+ context: Pick<BranchContext, 'branch' | 'branchRoot' | 'baseRoot'>;
5
+ registry: Pick<BranchRegistry, 'invalidate'>;
6
+ /**
7
+ * For a delete a person asked for. Deletion by age passes none: commits pushed to a GitHub branch
8
+ * after its merge have no other copy, and reconcile prunes the mirror's head once GitHub drops it.
9
+ */
10
+ remoteCopies?: {
11
+ /** The branch it forked from, or the deployment's base branch. */
12
+ baseBranch: string;
13
+ /** Removes the branch's head from `remote.git`; a failure costs a warning, not the delete. */
14
+ deleteMirrorHead: () => Promise<void>;
15
+ /** Deletes or queues deleting the branch on GitHub, resolving a warning if that failed. */
16
+ deleteOnGitHub: () => Promise<string | undefined>;
17
+ };
18
+ /**
19
+ * Re-decides from a fresh read inside the lock every save() takes, so a change before the rename
20
+ * keeps the branch and one after it fails. For a caller that chose the branch earlier.
21
+ */
22
+ stillWanted?: (current: BranchMetadata) => boolean;
23
+ }
24
+ export type DeleteBranchWorkspaceOutcome = {
25
+ kind: 'deleted';
26
+ cleanupWarning?: string;
27
+ }
28
+ /** `stillWanted` refused it, or it was already gone. */
29
+ | {
30
+ kind: 'kept';
31
+ }
32
+ /** The lock was not taken in time, or branch.json could not be read under it. */
33
+ | {
34
+ kind: 'busy';
35
+ error: string;
36
+ };
37
+ /**
38
+ * Delete a branch and everything stored with it: workspace, branch.json and comments.
39
+ *
40
+ * The directory leaves its name by one rename, under the server-enforced lockfile branch-metadata
41
+ * saves hold (utils/occ-json-write.ts), so a save() that reaches that lock after the rename fails
42
+ * rather than recreating the tree. The `rm` runs last: a process killed during it leaves only a
43
+ * `.deleting-*` directory the worker sweeps, never residue under a name that may be created again.
44
+ */
45
+ export declare function deleteBranchWorkspace(options: DeleteBranchWorkspaceOptions): Promise<DeleteBranchWorkspaceOutcome>;
@@ -0,0 +1,97 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { BRANCH_META_DIR, BRANCH_META_FILE, readBranchMetadataFile } from './branch-metadata-file.js';
4
+ import { deletingDirName, removeLeftoverDir } from './branch-provisioning.js';
5
+ import { RESERVED_SETTINGS_BRANCH_PREFIX, sanitizeBranchName } from './paths/branch-name.js';
6
+ import { getErrorMessage, isNotFoundError, sanitizeErrorMessage } from './utils/error.js';
7
+ import { filePathExists } from './utils/fs.js';
8
+ import { canopyLogError, canopyLogWarn } from './utils/logger.js';
9
+ import { withOccFileLock } from './utils/occ-json-write.js';
10
+ /**
11
+ * Delete a branch and everything stored with it: workspace, branch.json and comments.
12
+ *
13
+ * The directory leaves its name by one rename, under the server-enforced lockfile branch-metadata
14
+ * saves hold (utils/occ-json-write.ts), so a save() that reaches that lock after the rename fails
15
+ * rather than recreating the tree. The `rm` runs last: a process killed during it leaves only a
16
+ * `.deleting-*` directory the worker sweeps, never residue under a name that may be created again.
17
+ */
18
+ export async function deleteBranchWorkspace(options) {
19
+ const { context, registry, remoteCopies, stillWanted } = options;
20
+ const metadataFile = path.join(context.branchRoot, BRANCH_META_DIR, BRANCH_META_FILE);
21
+ let cleanupWarning;
22
+ let deletingPath;
23
+ const warn = (warning) => {
24
+ cleanupWarning = cleanupWarning ? `${cleanupWarning}; ${warning}` : warning;
25
+ };
26
+ let wanted = true;
27
+ try {
28
+ await withOccFileLock(metadataFile, async () => {
29
+ if (stillWanted) {
30
+ const current = await readBranchMetadataFile(context.branchRoot);
31
+ if (current === null || !stillWanted(current.branch)) {
32
+ wanted = false;
33
+ return;
34
+ }
35
+ }
36
+ if (context.branchRoot === context.baseRoot) {
37
+ await fs.unlink(metadataFile).catch((err) => {
38
+ if (!isNotFoundError(err))
39
+ throw err;
40
+ });
41
+ return;
42
+ }
43
+ const target = path.join(context.baseRoot, deletingDirName(path.basename(context.branchRoot)));
44
+ try {
45
+ await fs.rename(context.branchRoot, target);
46
+ deletingPath = target;
47
+ }
48
+ catch (err) {
49
+ if (isNotFoundError(err))
50
+ return;
51
+ // Still deleted logically: without branch.json the branch is unlisted,
52
+ // and the worker quarantines what is left.
53
+ await fs.unlink(metadataFile).catch(() => { });
54
+ // [REDACT] Returned to the browser; the console line keeps the path.
55
+ warn(`Failed to remove branch directory: ${sanitizeErrorMessage(getErrorMessage(err))}`);
56
+ canopyLogError(`CanopyCMS: Failed to move branch directory for ${context.branch.name} aside:`, getErrorMessage(err));
57
+ }
58
+ });
59
+ }
60
+ catch (err) {
61
+ // A lock on a directory another delete took fails like any other.
62
+ if (!(await filePathExists(context.branchRoot)))
63
+ return { kind: 'kept' };
64
+ return { kind: 'busy', error: getErrorMessage(err) };
65
+ }
66
+ if (!wanted)
67
+ return { kind: 'kept' };
68
+ // Defense-in-depth only: callers refuse the base branch, and settings branches never resolve
69
+ // to a context here.
70
+ const sanitizedDeleted = sanitizeBranchName(context.branch.name);
71
+ if (remoteCopies &&
72
+ sanitizedDeleted !== sanitizeBranchName(remoteCopies.baseBranch) &&
73
+ !sanitizedDeleted.startsWith(RESERVED_SETTINGS_BRANCH_PREFIX)) {
74
+ try {
75
+ await remoteCopies.deleteMirrorHead();
76
+ }
77
+ catch (err) {
78
+ // Client-facing copy is sanitized (API-H2 — no absolute EFS paths); the console line keeps
79
+ // the raw detail for server logs.
80
+ warn(`Failed to remove the deleted branch's head from the local mirror: ${sanitizeErrorMessage(getErrorMessage(err))}`);
81
+ canopyLogWarn(`CanopyCMS: failed to remove deleted branch's mirror head (branch ${context.branch.name}): ${getErrorMessage(err)}`);
82
+ }
83
+ const warning = await remoteCopies.deleteOnGitHub();
84
+ if (warning)
85
+ warn(warning);
86
+ }
87
+ try {
88
+ await registry.invalidate();
89
+ }
90
+ catch (err) {
91
+ // The branch is gone; a listing corrects itself at the next bump.
92
+ warn(`Failed to refresh the branch list: ${sanitizeErrorMessage(getErrorMessage(err))}`);
93
+ }
94
+ if (deletingPath)
95
+ await removeLeftoverDir(deletingPath);
96
+ return { kind: 'deleted', ...(cleanupWarning && { cleanupWarning }) };
97
+ }
@@ -68,6 +68,7 @@ export declare const branchMetadataSchema: z.ZodObject<{
68
68
  merged: "merged";
69
69
  }>>;
70
70
  mergedAt: z.ZodOptional<z.ZodString>;
71
+ archivedAt: z.ZodOptional<z.ZodString>;
71
72
  rebaseFailure: z.ZodOptional<z.ZodObject<{
72
73
  message: z.ZodString;
73
74
  firstAt: z.ZodString;
@@ -183,6 +184,7 @@ export declare const branchMetadataSchema: z.ZodObject<{
183
184
  merged: "merged";
184
185
  }>>;
185
186
  mergedAt: z.ZodOptional<z.ZodString>;
187
+ archivedAt: z.ZodOptional<z.ZodString>;
186
188
  rebaseFailure: z.ZodOptional<z.ZodObject<{
187
189
  message: z.ZodString;
188
190
  firstAt: z.ZodString;
@@ -298,6 +300,7 @@ export declare const branchMetadataSchema: z.ZodObject<{
298
300
  merged: "merged";
299
301
  }>>;
300
302
  mergedAt: z.ZodOptional<z.ZodString>;
303
+ archivedAt: z.ZodOptional<z.ZodString>;
301
304
  rebaseFailure: z.ZodOptional<z.ZodObject<{
302
305
  message: z.ZodString;
303
306
  firstAt: z.ZodString;
@@ -77,6 +77,7 @@ const branchMetadataShape = {
77
77
  merged: 'merged',
78
78
  }).optional(),
79
79
  mergedAt: z.string().optional(),
80
+ archivedAt: z.string().optional(),
80
81
  rebaseFailure: z
81
82
  .object({ message: z.string(), firstAt: z.string(), lastAt: z.string() })
82
83
  .optional(),
@@ -130,7 +130,12 @@ export declare const awaitsSubmitRetry: ({ submitUnfinished, status }: BranchMet
130
130
  * existing values for fields the incoming update omits, so the branch's ref and
131
131
  * its recorded PR number and URL survive untouched.
132
132
  */
133
- export declare function buildMergedBranchUpdate(branch: BranchMetadata, now?: Date): NonNullable<BranchMetadataUpdate['branch']>;
133
+ export declare function buildMergedBranchUpdate(branch: BranchMetadata, mergedAt?: Date): NonNullable<BranchMetadataUpdate['branch']>;
134
+ /**
135
+ * The metadata update for archiving a branch whose PR closed without merging. Records no merge:
136
+ * `mergedAt` is what tells the two apart, and `pullRequestState` stays `closed`.
137
+ */
138
+ export declare function buildUnmergedArchiveUpdate(): NonNullable<BranchMetadataUpdate['branch']>;
134
139
  /** {@link BranchMetadataFileManager.get} as a function. */
135
140
  export declare const getBranchMetadataFileManager: (branchRoot: string, baseRoot: string, options?: {
136
141
  settleMs?: number;
@@ -282,11 +282,13 @@ const owesSubmit = (branch) => awaitsSubmitRetry(branch) ||
282
282
  * existing values for fields the incoming update omits, so the branch's ref and
283
283
  * its recorded PR number and URL survive untouched.
284
284
  */
285
- export function buildMergedBranchUpdate(branch, now = new Date()) {
285
+ export function buildMergedBranchUpdate(branch, mergedAt = new Date()) {
286
286
  return {
287
287
  status: 'archived',
288
288
  pullRequestState: 'merged',
289
- mergedAt: now.toISOString(),
289
+ mergedAt: mergedAt.toISOString(),
290
+ // Now, not the merge: retention's clock must not start before a worker outage that hid it.
291
+ archivedAt: new Date().toISOString(),
290
292
  // The merge settled every conflict the record named.
291
293
  conflictStatus: 'clean',
292
294
  conflictFiles: [],
@@ -294,6 +296,17 @@ export function buildMergedBranchUpdate(branch, now = new Date()) {
294
296
  ...(owesSubmit(branch) ? { unpublishedAtMerge: true } : {}),
295
297
  };
296
298
  }
299
+ /**
300
+ * The metadata update for archiving a branch whose PR closed without merging. Records no merge:
301
+ * `mergedAt` is what tells the two apart, and `pullRequestState` stays `closed`.
302
+ */
303
+ export function buildUnmergedArchiveUpdate() {
304
+ return {
305
+ status: 'archived',
306
+ archivedAt: new Date().toISOString(),
307
+ submitUnfinished: undefined,
308
+ };
309
+ }
297
310
  /** {@link BranchMetadataFileManager.get} as a function. */
298
311
  export const getBranchMetadataFileManager = (branchRoot, baseRoot, options) => {
299
312
  return BranchMetadataFileManager.get(branchRoot, baseRoot, options);
package/dist/cli/cli.js CHANGED
@@ -1349,7 +1349,7 @@ var init_init = __esm({
1349
1349
  init_types = __esm2({
1350
1350
  "dist/config/types.js"() {
1351
1351
  "use strict";
1352
- scalarFieldTypes = ["string", "number", "boolean", "datetime", "code"];
1352
+ scalarFieldTypes = ["string", "number", "boolean", "date", "datetime", "code"];
1353
1353
  markdownFieldTypes = ["markdown", "mdx"];
1354
1354
  primitiveFieldTypes = [...scalarFieldTypes, ...markdownFieldTypes];
1355
1355
  fieldTypes = [
@@ -3495,7 +3495,7 @@ async function atomicWriteFile2(filePath, content) {
3495
3495
  }
3496
3496
  }
3497
3497
  function isPlainRecord(value) {
3498
- return typeof value === "object" && value !== null && !Array.isArray(value);
3498
+ return typeof value === "object" && value !== null && !Array.isArray(value) && !(value instanceof Date);
3499
3499
  }
3500
3500
  function resolveBlockItem(blockField, item) {
3501
3501
  const templateName = typeof item.template === "string" ? item.template : typeof item._type === "string" ? item._type : void 0;
@@ -4068,6 +4068,20 @@ async function writeWorkerStatus(taskDir, report) {
4068
4068
  function isBlockStructuralKey(key) {
4069
4069
  return BLOCK_STRUCTURAL_KEYS.has(key);
4070
4070
  }
4071
+ function withCalendarDays(fields, data) {
4072
+ let out = data;
4073
+ for (const field of fields) {
4074
+ if (field.type === "group") {
4075
+ out = withCalendarDays(field.fields, out);
4076
+ continue;
4077
+ }
4078
+ const value = out[field.name];
4079
+ const next = valueWithDays(field, value);
4080
+ if (next !== value)
4081
+ out = { ...out, [field.name]: next };
4082
+ }
4083
+ return out;
4084
+ }
4071
4085
  function createDebugLogger2(options) {
4072
4086
  return new DebugLogger2(options);
4073
4087
  }
@@ -4711,9 +4725,9 @@ function asJson(value) {
4711
4725
  const key = identityKey(value);
4712
4726
  return key === void 0 || key === "undefined" ? void 0 : JSON.parse(key);
4713
4727
  }
4714
- function frontmatterAsRead(raw) {
4728
+ function frontmatterAsRead(raw, fields) {
4715
4729
  try {
4716
- return asJson(matter(raw, {}).data);
4730
+ return asJson(withCalendarDays(fields, matter(raw, {}).data));
4717
4731
  } catch {
4718
4732
  return void 0;
4719
4733
  }
@@ -4781,7 +4795,7 @@ ${asKey ? `${text3}: v` : `v: ${text3}`}
4781
4795
  return true;
4782
4796
  }
4783
4797
  }
4784
- function serializeFrontmatter(editorBody, data, existingRaw, format) {
4798
+ function serializeFrontmatter(editorBody, data, existingRaw, format, fields = []) {
4785
4799
  const file = { content: bodyToWrite(editorBody, data, format, existingRaw) };
4786
4800
  if (existingRaw === void 0)
4787
4801
  return matter.stringify(file, data);
@@ -4789,7 +4803,7 @@ function serializeFrontmatter(editorBody, data, existingRaw, format) {
4789
4803
  if (existingFrontmatter === void 0)
4790
4804
  return matter.stringify(file, data);
4791
4805
  const onDisk = parseDocument2(existingFrontmatter);
4792
- const toReconcile = onDisk.errors.length === 0 ? withUnchangedAsOnDisk(data, frontmatterAsRead(existingRaw), onDisk.toJS()) : data;
4806
+ const toReconcile = onDisk.errors.length === 0 ? withUnchangedAsOnDisk(data, frontmatterAsRead(existingRaw, fields), onDisk.toJS()) : data;
4793
4807
  const reconciled = reconcileYamlSource(existingFrontmatter, isPlainRecord2(toReconcile) ? toReconcile : data, { rootAtColumnZero: true, quote: readsAsAnotherValue });
4794
4808
  if (reconciled === void 0)
4795
4809
  return matter.stringify(file, data);
@@ -4801,7 +4815,7 @@ function serializeFrontmatter(editorBody, data, existingRaw, format) {
4801
4815
  }
4802
4816
  }
4803
4817
  });
4804
- if (isDeepStrictEqual2(frontmatterAsRead(written), asJson(data)))
4818
+ if (isDeepStrictEqual2(frontmatterAsRead(written, fields), asJson(data)))
4805
4819
  return written;
4806
4820
  log22.debug("content-serialize", "reconciled frontmatter reads back differently; writing as gray-matter prints it");
4807
4821
  return matter.stringify(file, data);
@@ -6131,6 +6145,7 @@ _${field.description}_` : "";
6131
6145
  switch (field.type) {
6132
6146
  case "string":
6133
6147
  case "number":
6148
+ case "date":
6134
6149
  case "datetime":
6135
6150
  return `${heading} ${label}${descriptionLine}
6136
6151
 
@@ -8054,6 +8069,11 @@ function validateScalar(field, value, path272) {
8054
8069
  return [{ fieldPath: path272, message: "Expected true or false" }];
8055
8070
  }
8056
8071
  return [];
8072
+ case "date":
8073
+ if (value !== "" && !isCalendarDay(value)) {
8074
+ return [{ fieldPath: path272, message: "Expected a real date, written YYYY-MM-DD" }];
8075
+ }
8076
+ return [];
8057
8077
  case "datetime":
8058
8078
  if (typeof value !== "string" || value !== "" && Number.isNaN(Date.parse(value))) {
8059
8079
  return [{ fieldPath: path272, message: "Expected a valid date/time string" }];
@@ -8206,7 +8226,7 @@ function findInvalidEntries(items) {
8206
8226
  for (const item of items) {
8207
8227
  if (!item.schema)
8208
8228
  continue;
8209
- const data = normalizeDatesDeep(item.data);
8229
+ const data = normalizeDatesDeep(withCalendarDays(item.schema, item.data));
8210
8230
  const errors = validateEntryData(item.schema, data);
8211
8231
  if (errors.length > 0) {
8212
8232
  invalid.push({ entryPath: item.entryPath, errors });
@@ -8434,7 +8454,7 @@ async function generateAIContentCLI(options) {
8434
8454
  console.log(` Output: ${result.outputDir}
8435
8455
  `);
8436
8456
  }
8437
- var __defProp3, __getOwnPropNames3, __esm3, __export3, init_error3, init_atomic_write2, init_field_traversal, ITEM_KEY, ITEM_KEY_PATTERN, isItemKey, BASE58, isListField, init_item_keys, active2, init_logger2, SENTINEL_BASE2, DOT_SEGMENT2, init_sanitize_href2, init_normalize, RESERVED_SETTINGS_BRANCH_PREFIX2, init_branch_name, BASE58_PATTERN2, CONTENT_ID_PATTERN2, PHYSICAL_SEGMENT_PATTERN2, MAX_BRANCH_NAME_LENGTH2, init_validation, ROOT_COLLECTION_ID, init_types2, normalizePathValue, flattenSchema, init_flatten, scalarFieldTypes2, markdownFieldTypes2, primitiveFieldTypes2, fieldTypes2, init_types22, SAFE_HTML_TAGS2, COMPONENT_NAME2, MDX_BINDINGS2, FORBIDDEN_ATTRIBUTES2, init_mdx_allowlist2, fieldBaseSchema2, selectOptionSchema2, referenceOptionSchema2, primitiveFieldSchema2, mdxPropAllowSchema2, mdxAllowlistSchema2, markdownFieldSchema2, ASPECT_RATIO_RE2, imageFieldSchema2, selectFieldSchema2, referenceSortFieldSchema2, referenceFieldSchema2, fieldHolder2, blockSchema2, blockFieldSchema2, objectFieldSchema2, inlineGroupFieldSchema2, customFieldSchema2, knownFieldSchema2, fieldSchema2, init_field2, relativePathSchema2, entryTypeSchema2, collectionSchema2, init_collection2, uploadTargetUrlSchema2, previewPrefixSchema2, editorUrlSchema2, init_url2, mediaSchema2, init_media2, defaultBranchAccessSchema2, defaultPathAccessLevelSchema2, defaultPathAccessLevelsSchema2, defaultPathAccessSchema2, defaultBaseBranchSchema2, defaultRemoteNameSchema2, defaultRemoteUrlSchema2, gitBotAuthorNameSchema2, gitBotAuthorEmailSchema2, githubTokenEnvVarSchema2, operatingModeSchema2, deployedAsSchema2, contentRootSchema2, sourceRootSchema2, basePathSchema2, deploymentNameSchema2, devContentSyncSchema2, devConfigSchema2, editorConfigSchema2, seoFieldNameSchema2, seoSchema2, CanopyConfigSchema2, DEFAULT_PROD_WORKSPACE2, init_config3, SERVER_MODE_ENV_VAR, BROWSER_MODE_ENV_VAR, warned22, init_mode_env, forEachReferenceField, init_validation2, ProdClientSafeStrategy2, DevClientSafeStrategy2, clientStrategyCache, init_client_safe_strategy, warned3, VALID_DEPLOYMENT_NAME2, isValidDeploymentName2, init_deployment_name, warned4, DEPLOYMENT_PLACEHOLDER2, DEFAULT_BRANCH_NAMESPACE_TEMPLATE2, BRANCH_NAMESPACE_ENV2, init_branch_namespace, init_config22, ProdStrategy2, DevStrategy2, strategyCache2, init_client_unsafe_strategy, operating_mode_exports, init_operating_mode, task_queue_config_exports2, init_task_queue_config2, worker_status_exports, WORKER_STATUS_FILE, init_worker_status, BLOCK_DISCRIMINATOR_KEYS, BLOCK_STRUCTURAL_KEYS, envFlag2, LOG_LEVELS2, DebugLogger2, testLogger2, MARKDOWN_EXPORT_OPTIONS, BULLETS, DELIMITERS, EMPHASIS, STRONG, RULE, WORD_CHAR, GAP, Restyler, END, FRESH, LINE_WIDTH, MIN_LINE_WIDTH, SourceSplicer, log2, MAX_LCS_CELLS, Splicer, log22, EVIDENCE_MAX_DEPTH, log3, locks, PROVISIONING_LOCK_STALE_MS, sleep, META_DIR, CONTENT_WRITE_LOCK_NAME, DEFAULT_CONTENT_WRITE_LOCK_WAIT_MS, ContentWriteLockBusyError, sleep2, CONTENT_WRITE_LOCK_STALE_MS, heldRoots, getFormatExtension, isDataOnlyFormat, asRecord, DEFAULT_SEO_FIELD_NAMES, asRecord2, asText, asFlag, asImageUrl, asOgType, asTwitterCard, RESTRICTED_REFERENCE_MARKER, SEO_GROUP_FIELDS, MAX_WARNED, warned5, SchemaUnavailableError, isValidId, parseTypedFilename, EMPTY_LOGICAL_PATH, ENTRY_EXTENSIONS, ContentIdIndex, CONTENT_EXTENSIONS, registry, finalization, log4, META_DIR2, entryTypeMetaSchema, collectionMetaSchema, rootCollectionMetaSchema, fingerprints, editDistance, closestName, forEachEntryType, collectEntryTypeNames, validateReferenceEntryTypes, isDeployedStatic, isBuildMode, readsFromCheckout, STATIC_DEPLOY_USER, storage, log5, BaseBranchUnresolvedError, NETWORK_SCHEME_PATTERN, TRANSPORT_HELPER_PATTERN, LEADING_OPTION_PATTERN, WINDOWS_DRIVE_PATTERN, COLON_BEFORE_SLASH_PATTERN, MISSING_REMOTE_REF_REASONS, CANOPY_META_DIR, SCHEMA_CACHE_VERSION, MTIME_CHECK_DEBOUNCE_MS, SNAPSHOT_MEMO_MAX_BYTES, SCHEMA_CACHE_FILE, SCHEMA_GENERATION_RESOURCE, reportedIssues, BranchSchemaCache, RESOURCE, BranchPathError, resolveContentBranchesRoot, ContentStoreError, ContentConflictError, EntryGoneError, BranchSyncingError, DuplicateContentIdError, UrlPathConflictError, isEmptyFieldValue, DEFAULT_INDEX_FRESHNESS_INTERVAL_MS, FORCED_REFRESH_MIN_INTERVAL_MS, STALE_LOOKUP, isMissingReference, createReferenceResolveCache, ContentStore, ATTR_CONTENT, BLOCK_PREFIX, BLOCK_SUFFIX, BLOCK_RESTORE_RE, INLINE_PREFIX, INLINE_SUFFIX, INLINE_RESTORE_RE, DEFAULT_REFERENCE_RENDERING, FLAT_CELL_TYPES, log6, BASE58_CHAR, ENTRY_LINK_PATTERN, INLINE_LINK_TARGET, BranchMetadataCorruptError, BRANCH_META_DIR, BRANCH_META_FILE, branchMetadataFilePath, UNKNOWN_TIME, enumOf, branchMetadataShape, branchMetadataSchema, branchMetadataFileSchema, log7, REGISTRY_FILE, REGISTRY_STALE_FILE, REGISTRY_TEMP_FILE, REGISTRY_VERSION, RESOURCE2, GET_MISS_REFRESH_MIN_INTERVAL_MS, snapshotSchema, reportedUnreadable, BranchRegistry, log8, OccWriteConflictError, CURRENT_SCHEMA_VERSION, BranchMetadataConflictError, BranchMetadataFileManager, loadBranchContext, GitProcessError, STDERR_CAP_BYTES, KILL_GRACE_MS, log9, GIT_ENV_PASSTHROUGH, FORCE_C_LOCALE, GIT_NETWORK_ENV_PASSTHROUGH, NO_AUTO_GC_CONFIG, HASH_INACTIVITY_TIMEOUT_MS, MAX_HASH_RUNS, LS_REMOTE_INACTIVITY_TIMEOUT_MS, symlinkBlobSha, REMOTE_GIT_CONFIG, REMOTE_GIT_MAX_LOOSE_OBJECTS, REMOTE_GIT_MAX_PACKS, PARALLEL_CHECKOUT_ARGS, remoteInitLocks, GITHUB_TRACKING_REF_PREFIX, BaseBranchMissingError, GitConflictError, GitRemoteRefMissingError, RemoteNotReadyError, SettingsRemoteUnreadableError, SettingsBranchDivergedError, SETTINGS_BRANCH_FILES, SettingsBranchHasContentHistoryError, GitManager, PUBLISH_LOCK_WAIT_MS, CREATE_RESIDUE_MIN_QUIET_MS, STAGING_MAX_AGE_MS, REPAIR_MAX_AGE_MS, DELETING_MAX_AGE_MS, SETTINGS_TRASH_MAX_AGE_MS, BranchProvisioningBusyError, BranchDirOccupiedError, STAGING_PREFIX, REPAIR_PREFIX, TRASH_PREFIX, stagingDirName, NO_GIT, RENAME_BLOCKED, SPARSE_CONE_RECORD, defaultSink, sink, ProvisionLog, testHooks, provisionsInFlight, BranchWorkspaceManager, AI_USER_ID, log10, normalizePath, parseEntryParts, parseEntryData, readEntryData, createReferenceResolver, withoutUnavailableEntries, resolveCollectionItemReferences, looksLikeMalformedEntry, listCollectionEntries, MAX_INPUT_PIXELS, CROP_PRECISION, KNOWN_FIELD_TYPES, STRING_FIELD_TYPES, selectOptionValues, GENERATED_RECORD_FILENAME, SAFE_BUILD_ID, BUILD_ID_RULE, jiti;
8457
+ var __defProp3, __getOwnPropNames3, __esm3, __export3, init_error3, init_atomic_write2, init_field_traversal, ITEM_KEY, ITEM_KEY_PATTERN, isItemKey, BASE58, isListField, init_item_keys, active2, init_logger2, SENTINEL_BASE2, DOT_SEGMENT2, init_sanitize_href2, init_normalize, RESERVED_SETTINGS_BRANCH_PREFIX2, init_branch_name, BASE58_PATTERN2, CONTENT_ID_PATTERN2, PHYSICAL_SEGMENT_PATTERN2, MAX_BRANCH_NAME_LENGTH2, init_validation, ROOT_COLLECTION_ID, init_types2, normalizePathValue, flattenSchema, init_flatten, scalarFieldTypes2, markdownFieldTypes2, primitiveFieldTypes2, fieldTypes2, init_types22, SAFE_HTML_TAGS2, COMPONENT_NAME2, MDX_BINDINGS2, FORBIDDEN_ATTRIBUTES2, init_mdx_allowlist2, fieldBaseSchema2, selectOptionSchema2, referenceOptionSchema2, primitiveFieldSchema2, mdxPropAllowSchema2, mdxAllowlistSchema2, markdownFieldSchema2, ASPECT_RATIO_RE2, imageFieldSchema2, selectFieldSchema2, referenceSortFieldSchema2, referenceFieldSchema2, fieldHolder2, blockSchema2, blockFieldSchema2, objectFieldSchema2, inlineGroupFieldSchema2, customFieldSchema2, knownFieldSchema2, fieldSchema2, init_field2, relativePathSchema2, entryTypeSchema2, collectionSchema2, init_collection2, uploadTargetUrlSchema2, previewPrefixSchema2, editorUrlSchema2, init_url2, mediaSchema2, init_media2, defaultBranchAccessSchema2, defaultPathAccessLevelSchema2, defaultPathAccessLevelsSchema2, defaultPathAccessSchema2, defaultBaseBranchSchema2, defaultRemoteNameSchema2, defaultRemoteUrlSchema2, gitBotAuthorNameSchema2, gitBotAuthorEmailSchema2, githubTokenEnvVarSchema2, operatingModeSchema2, deployedAsSchema2, contentRootSchema2, sourceRootSchema2, basePathSchema2, deploymentNameSchema2, devContentSyncSchema2, devConfigSchema2, editorConfigSchema2, seoFieldNameSchema2, seoSchema2, CanopyConfigSchema2, DEFAULT_PROD_WORKSPACE2, init_config3, SERVER_MODE_ENV_VAR, BROWSER_MODE_ENV_VAR, warned22, init_mode_env, forEachReferenceField, init_validation2, ProdClientSafeStrategy2, DevClientSafeStrategy2, clientStrategyCache, init_client_safe_strategy, warned3, VALID_DEPLOYMENT_NAME2, isValidDeploymentName2, init_deployment_name, warned4, DEPLOYMENT_PLACEHOLDER2, DEFAULT_BRANCH_NAMESPACE_TEMPLATE2, BRANCH_NAMESPACE_ENV2, init_branch_namespace, init_config22, ProdStrategy2, DevStrategy2, strategyCache2, init_client_unsafe_strategy, operating_mode_exports, init_operating_mode, task_queue_config_exports2, init_task_queue_config2, worker_status_exports, WORKER_STATUS_FILE, init_worker_status, BLOCK_DISCRIMINATOR_KEYS, BLOCK_STRUCTURAL_KEYS, DAY, MS_PER_DAY, isRealDay, dayParts, isCalendarDay, dayOfBareDate, mapItems, blockItemWithDays, valueWithDays, entryAsRead, envFlag2, LOG_LEVELS2, DebugLogger2, testLogger2, MARKDOWN_EXPORT_OPTIONS, BULLETS, DELIMITERS, EMPHASIS, STRONG, RULE, WORD_CHAR, GAP, Restyler, END, FRESH, LINE_WIDTH, MIN_LINE_WIDTH, SourceSplicer, log2, MAX_LCS_CELLS, Splicer, log22, EVIDENCE_MAX_DEPTH, log3, locks, PROVISIONING_LOCK_STALE_MS, sleep, META_DIR, CONTENT_WRITE_LOCK_NAME, DEFAULT_CONTENT_WRITE_LOCK_WAIT_MS, ContentWriteLockBusyError, sleep2, CONTENT_WRITE_LOCK_STALE_MS, heldRoots, getFormatExtension, isDataOnlyFormat, asRecord, DEFAULT_SEO_FIELD_NAMES, asRecord2, asText, asFlag, asImageUrl, asOgType, asTwitterCard, RESTRICTED_REFERENCE_MARKER, SEO_GROUP_FIELDS, MAX_WARNED, warned5, SchemaUnavailableError, isValidId, parseTypedFilename, EMPTY_LOGICAL_PATH, ENTRY_EXTENSIONS, ContentIdIndex, CONTENT_EXTENSIONS, registry, finalization, log4, META_DIR2, entryTypeMetaSchema, collectionMetaSchema, rootCollectionMetaSchema, fingerprints, editDistance, closestName, forEachEntryType, collectEntryTypeNames, validateReferenceEntryTypes, isDeployedStatic, isBuildMode, readsFromCheckout, STATIC_DEPLOY_USER, storage, log5, BaseBranchUnresolvedError, NETWORK_SCHEME_PATTERN, TRANSPORT_HELPER_PATTERN, LEADING_OPTION_PATTERN, WINDOWS_DRIVE_PATTERN, COLON_BEFORE_SLASH_PATTERN, MISSING_REMOTE_REF_REASONS, CANOPY_META_DIR, SCHEMA_CACHE_VERSION, MTIME_CHECK_DEBOUNCE_MS, SNAPSHOT_MEMO_MAX_BYTES, SCHEMA_CACHE_FILE, SCHEMA_GENERATION_RESOURCE, reportedIssues, BranchSchemaCache, RESOURCE, BranchPathError, resolveContentBranchesRoot, ContentStoreError, ContentConflictError, EntryGoneError, BranchSyncingError, DuplicateContentIdError, UrlPathConflictError, isEmptyFieldValue, DEFAULT_INDEX_FRESHNESS_INTERVAL_MS, FORCED_REFRESH_MIN_INTERVAL_MS, STALE_LOOKUP, isMissingReference, createReferenceResolveCache, ContentStore, ATTR_CONTENT, BLOCK_PREFIX, BLOCK_SUFFIX, BLOCK_RESTORE_RE, INLINE_PREFIX, INLINE_SUFFIX, INLINE_RESTORE_RE, DEFAULT_REFERENCE_RENDERING, FLAT_CELL_TYPES, log6, BASE58_CHAR, ENTRY_LINK_PATTERN, INLINE_LINK_TARGET, BranchMetadataCorruptError, BRANCH_META_DIR, BRANCH_META_FILE, branchMetadataFilePath, UNKNOWN_TIME, enumOf, branchMetadataShape, branchMetadataSchema, branchMetadataFileSchema, log7, REGISTRY_FILE, REGISTRY_STALE_FILE, REGISTRY_TEMP_FILE, REGISTRY_VERSION, RESOURCE2, GET_MISS_REFRESH_MIN_INTERVAL_MS, snapshotSchema, reportedUnreadable, BranchRegistry, log8, OccWriteConflictError, CURRENT_SCHEMA_VERSION, BranchMetadataConflictError, BranchMetadataFileManager, loadBranchContext, GitProcessError, STDERR_CAP_BYTES, KILL_GRACE_MS, log9, GIT_ENV_PASSTHROUGH, FORCE_C_LOCALE, GIT_NETWORK_ENV_PASSTHROUGH, NO_AUTO_GC_CONFIG, HASH_INACTIVITY_TIMEOUT_MS, MAX_HASH_RUNS, LS_REMOTE_INACTIVITY_TIMEOUT_MS, symlinkBlobSha, REMOTE_GIT_CONFIG, REMOTE_GIT_MAX_LOOSE_OBJECTS, REMOTE_GIT_MAX_PACKS, PARALLEL_CHECKOUT_ARGS, remoteInitLocks, GITHUB_TRACKING_REF_PREFIX, BaseBranchMissingError, GitConflictError, GitRemoteRefMissingError, RemoteNotReadyError, SettingsRemoteUnreadableError, SettingsBranchDivergedError, SETTINGS_BRANCH_FILES, SettingsBranchHasContentHistoryError, GitManager, PUBLISH_LOCK_WAIT_MS, CREATE_RESIDUE_MIN_QUIET_MS, STAGING_MAX_AGE_MS, REPAIR_MAX_AGE_MS, DELETING_MAX_AGE_MS, SETTINGS_TRASH_MAX_AGE_MS, BranchProvisioningBusyError, BranchDirOccupiedError, STAGING_PREFIX, REPAIR_PREFIX, TRASH_PREFIX, stagingDirName, NO_GIT, RENAME_BLOCKED, SPARSE_CONE_RECORD, defaultSink, sink, ProvisionLog, testHooks, provisionsInFlight, BranchWorkspaceManager, AI_USER_ID, log10, normalizePath, parseEntryParts, parseEntryData, readEntryData, createReferenceResolver, withoutUnavailableEntries, resolveCollectionItemReferences, looksLikeMalformedEntry, listCollectionEntries, MAX_INPUT_PIXELS, CROP_PRECISION, KNOWN_FIELD_TYPES, STRING_FIELD_TYPES, selectOptionValues, GENERATED_RECORD_FILENAME, SAFE_BUILD_ID, BUILD_ID_RULE, jiti;
8438
8458
  var init_generate_ai_content = __esm({
8439
8459
  "dist/cli/generate-ai-content.js"() {
8440
8460
  "use strict";
@@ -8616,7 +8636,7 @@ var init_generate_ai_content = __esm({
8616
8636
  init_types22 = __esm3({
8617
8637
  "dist/config/types.js"() {
8618
8638
  "use strict";
8619
- scalarFieldTypes2 = ["string", "number", "boolean", "datetime", "code"];
8639
+ scalarFieldTypes2 = ["string", "number", "boolean", "date", "datetime", "code"];
8620
8640
  markdownFieldTypes2 = ["markdown", "mdx"];
8621
8641
  primitiveFieldTypes2 = [...scalarFieldTypes2, ...markdownFieldTypes2];
8622
8642
  fieldTypes2 = [
@@ -9350,6 +9370,54 @@ var init_generate_ai_content = __esm({
9350
9370
  BLOCK_DISCRIMINATOR_KEYS = ["template", "_type"];
9351
9371
  BLOCK_STRUCTURAL_KEYS = new Set(BLOCK_DISCRIMINATOR_KEYS);
9352
9372
  init_item_keys();
9373
+ init_item_keys();
9374
+ init_field_traversal();
9375
+ DAY = /^(\d{4})-(\d{2})-(\d{2})$/;
9376
+ MS_PER_DAY = 864e5;
9377
+ isRealDay = (year, month, day) => {
9378
+ const lastOfMonth = /* @__PURE__ */ new Date(0);
9379
+ lastOfMonth.setUTCFullYear(year, month, 0);
9380
+ return month >= 1 && month <= 12 && day >= 1 && day <= lastOfMonth.getUTCDate();
9381
+ };
9382
+ dayParts = (value) => {
9383
+ const match = typeof value === "string" ? DAY.exec(value) : null;
9384
+ if (!match)
9385
+ return void 0;
9386
+ const [year, month, day] = match.slice(1).map(Number);
9387
+ return isRealDay(year, month, day) ? [year, month, day] : void 0;
9388
+ };
9389
+ isCalendarDay = (value) => dayParts(value) !== void 0;
9390
+ dayOfBareDate = (value) => value.getTime() % MS_PER_DAY === 0 ? value.toISOString().slice(0, 10) : void 0;
9391
+ mapItems = (items, mapItem) => {
9392
+ const next = items.map((item) => isPlainRecord(item) ? mapItem(item) : item);
9393
+ return next.every((item, i) => item === items[i]) ? items : next;
9394
+ };
9395
+ blockItemWithDays = (field, item) => {
9396
+ const resolved = resolveBlockItem(field, item);
9397
+ if (!resolved)
9398
+ return item;
9399
+ const data = withCalendarDays(resolved.fields, resolved.data);
9400
+ if (data === resolved.data)
9401
+ return item;
9402
+ return resolved.data === item ? data : { ...item, value: data };
9403
+ };
9404
+ valueWithDays = (field, value) => {
9405
+ if (field.type === "date") {
9406
+ return value instanceof Date ? dayOfBareDate(value) ?? value : value;
9407
+ }
9408
+ if (field.type === "object") {
9409
+ const { fields, list } = field;
9410
+ if (list) {
9411
+ return Array.isArray(value) ? mapItems(value, (item) => withCalendarDays(fields ?? [], item)) : value;
9412
+ }
9413
+ return isPlainRecord(value) ? withCalendarDays(fields ?? [], value) : value;
9414
+ }
9415
+ if (field.type === "block" && Array.isArray(value)) {
9416
+ return mapItems(value, (item) => blockItemWithDays(field, item));
9417
+ }
9418
+ return value;
9419
+ };
9420
+ entryAsRead = (fields, data) => withItemKeys(fields, withCalendarDays(fields, data), "content");
9353
9421
  envFlag2 = (name) => typeof process !== "undefined" && process.env[name] === "true";
9354
9422
  LOG_LEVELS2 = {
9355
9423
  DEBUG: 0,
@@ -10317,7 +10385,6 @@ var init_generate_ai_content = __esm({
10317
10385
  };
10318
10386
  isDataOnlyFormat = (format) => format === "json" || format === "yaml";
10319
10387
  asRecord = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
10320
- init_item_keys();
10321
10388
  DEFAULT_SEO_FIELD_NAMES = {
10322
10389
  title: "metaTitle",
10323
10390
  description: "metaDescription",
@@ -11624,7 +11691,7 @@ var init_generate_ai_content = __esm({
11624
11691
  absolutePath
11625
11692
  };
11626
11693
  }
11627
- doc.data = withItemKeys(fields, doc.data, "content");
11694
+ doc.data = entryAsRead(fields, doc.data);
11628
11695
  if (options.resolveReferences !== false) {
11629
11696
  doc.data = await this.resolveReferencesInData(doc.data, fields, void 0, options.referenceAccess, {
11630
11697
  entry: slug ? entryLogicalPath(schemaItem.logicalPath, slug) : schemaItem.logicalPath,
@@ -11728,7 +11795,7 @@ var init_generate_ai_content = __esm({
11728
11795
  } else if (input.format === "yaml") {
11729
11796
  content = serializeYaml(input.data ?? {}, existingRaw);
11730
11797
  } else {
11731
- content = serializeFrontmatter(input.body, input.data ?? {}, existingRaw, input.format);
11798
+ content = serializeFrontmatter(input.body, input.data ?? {}, existingRaw, input.format, fields);
11732
11799
  }
11733
11800
  await atomicWriteFile2(absolutePath, content);
11734
11801
  const liveIndex = this._idIndex;
@@ -12324,6 +12391,7 @@ var init_generate_ai_content = __esm({
12324
12391
  FLAT_CELL_TYPES = /* @__PURE__ */ new Set([
12325
12392
  "string",
12326
12393
  "number",
12394
+ "date",
12327
12395
  "datetime",
12328
12396
  "boolean",
12329
12397
  "select",
@@ -12391,6 +12459,7 @@ var init_generate_ai_content = __esm({
12391
12459
  merged: "merged"
12392
12460
  }).optional(),
12393
12461
  mergedAt: z72.string().optional(),
12462
+ archivedAt: z72.string().optional(),
12394
12463
  rebaseFailure: z72.object({ message: z72.string(), firstAt: z72.string(), lastAt: z72.string() }).optional(),
12395
12464
  historyRewrittenFrom: z72.string().optional(),
12396
12465
  lastGitHubTip: z72.string().optional(),
@@ -14595,7 +14664,6 @@ var init_generate_ai_content = __esm({
14595
14664
  };
14596
14665
  AI_USER_ID = "canopycms-ai";
14597
14666
  init_error3();
14598
- init_item_keys();
14599
14667
  log10 = createDebugLogger2({ prefix: "ContentListing" });
14600
14668
  normalizePath = (root, target) => {
14601
14669
  const result = validateAndNormalizePath(root, target);
@@ -14705,7 +14773,7 @@ var init_generate_ai_content = __esm({
14705
14773
  fs20.stat(absolutePath),
14706
14774
  readEntryData(absolutePath, format, bodyField)
14707
14775
  ]);
14708
- const data = withItemKeys(entryType?.schema ?? [], rawData, "content");
14776
+ const data = entryAsRead(entryType?.schema ?? [], rawData);
14709
14777
  const item = {
14710
14778
  logicalPath: entryLogicalPath(collection.logicalPath, slug),
14711
14779
  contentId,
@@ -16599,7 +16667,7 @@ var scalarFieldTypes3, markdownFieldTypes3, primitiveFieldTypes3, fieldTypes3;
16599
16667
  var init_types3 = __esm({
16600
16668
  "dist/config/types.js"() {
16601
16669
  "use strict";
16602
- scalarFieldTypes3 = ["string", "number", "boolean", "datetime", "code"];
16670
+ scalarFieldTypes3 = ["string", "number", "boolean", "date", "datetime", "code"];
16603
16671
  markdownFieldTypes3 = ["markdown", "mdx"];
16604
16672
  primitiveFieldTypes3 = [...scalarFieldTypes3, ...markdownFieldTypes3];
16605
16673
  fieldTypes3 = [
@@ -17868,6 +17936,7 @@ var init_branch_metadata_file = __esm({
17868
17936
  merged: "merged"
17869
17937
  }).optional(),
17870
17938
  mergedAt: z16.string().optional(),
17939
+ archivedAt: z16.string().optional(),
17871
17940
  rebaseFailure: z16.object({ message: z16.string(), firstAt: z16.string(), lastAt: z16.string() }).optional(),
17872
17941
  historyRewrittenFrom: z16.string().optional(),
17873
17942
  lastGitHubTip: z16.string().optional(),