@deskwork/core 0.45.1 → 0.46.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 (156) hide show
  1. package/dist/config.d.ts +14 -4
  2. package/dist/config.d.ts.map +1 -1
  3. package/dist/config.js +43 -12
  4. package/dist/config.js.map +1 -1
  5. package/dist/content-index.d.ts +25 -0
  6. package/dist/content-index.d.ts.map +1 -1
  7. package/dist/content-index.js +142 -15
  8. package/dist/content-index.js.map +1 -1
  9. package/dist/doctor/index.d.ts +3 -0
  10. package/dist/doctor/index.d.ts.map +1 -1
  11. package/dist/doctor/index.js +3 -0
  12. package/dist/doctor/index.js.map +1 -1
  13. package/dist/doctor/legacy-config.d.ts +98 -0
  14. package/dist/doctor/legacy-config.d.ts.map +1 -0
  15. package/dist/doctor/legacy-config.js +174 -0
  16. package/dist/doctor/legacy-config.js.map +1 -0
  17. package/dist/doctor/project-scope-gate.d.ts +12 -21
  18. package/dist/doctor/project-scope-gate.d.ts.map +1 -1
  19. package/dist/doctor/project-scope-gate.js +13 -25
  20. package/dist/doctor/project-scope-gate.js.map +1 -1
  21. package/dist/doctor/repair.d.ts +11 -0
  22. package/dist/doctor/repair.d.ts.map +1 -1
  23. package/dist/doctor/repair.js +10 -89
  24. package/dist/doctor/repair.js.map +1 -1
  25. package/dist/doctor/rules/duplicate-id.d.ts +7 -2
  26. package/dist/doctor/rules/duplicate-id.d.ts.map +1 -1
  27. package/dist/doctor/rules/duplicate-id.js +8 -5
  28. package/dist/doctor/rules/duplicate-id.js.map +1 -1
  29. package/dist/doctor/rules/duplicate-id.ts +8 -5
  30. package/dist/doctor/rules/legacy-top-level-id-migration.d.ts.map +1 -1
  31. package/dist/doctor/rules/legacy-top-level-id-migration.js +11 -8
  32. package/dist/doctor/rules/legacy-top-level-id-migration.js.map +1 -1
  33. package/dist/doctor/rules/legacy-top-level-id-migration.ts +10 -11
  34. package/dist/doctor/rules/sites-to-lanes-migration.d.ts +39 -0
  35. package/dist/doctor/rules/sites-to-lanes-migration.d.ts.map +1 -0
  36. package/dist/doctor/rules/sites-to-lanes-migration.js +395 -0
  37. package/dist/doctor/rules/sites-to-lanes-migration.js.map +1 -0
  38. package/dist/doctor/rules/sites-to-lanes-migration.ts +449 -0
  39. package/dist/doctor/rules/workflow-stale.d.ts.map +1 -1
  40. package/dist/doctor/rules/workflow-stale.js +5 -2
  41. package/dist/doctor/rules/workflow-stale.js.map +1 -1
  42. package/dist/doctor/rules/workflow-stale.ts +5 -1
  43. package/dist/doctor/runner.d.ts +15 -1
  44. package/dist/doctor/runner.d.ts.map +1 -1
  45. package/dist/doctor/runner.js +48 -41
  46. package/dist/doctor/runner.js.map +1 -1
  47. package/dist/doctor/sites-migration-backfill.d.ts +71 -0
  48. package/dist/doctor/sites-migration-backfill.d.ts.map +1 -0
  49. package/dist/doctor/sites-migration-backfill.js +164 -0
  50. package/dist/doctor/sites-migration-backfill.js.map +1 -0
  51. package/dist/doctor/validate.d.ts.map +1 -1
  52. package/dist/doctor/validate.js +34 -73
  53. package/dist/doctor/validate.js.map +1 -1
  54. package/dist/entry/resolve-artifact.d.ts +80 -0
  55. package/dist/entry/resolve-artifact.d.ts.map +1 -0
  56. package/dist/entry/resolve-artifact.js +102 -0
  57. package/dist/entry/resolve-artifact.js.map +1 -0
  58. package/dist/entry/shortform-path.d.ts +34 -0
  59. package/dist/entry/shortform-path.d.ts.map +1 -0
  60. package/dist/entry/shortform-path.js +50 -0
  61. package/dist/entry/shortform-path.js.map +1 -0
  62. package/dist/index.d.ts +1 -2
  63. package/dist/index.d.ts.map +1 -1
  64. package/dist/index.js +1 -2
  65. package/dist/index.js.map +1 -1
  66. package/dist/iterate/iterate.d.ts.map +1 -1
  67. package/dist/iterate/iterate.js +21 -26
  68. package/dist/iterate/iterate.js.map +1 -1
  69. package/dist/lanes/bootstrap.d.ts +11 -7
  70. package/dist/lanes/bootstrap.d.ts.map +1 -1
  71. package/dist/lanes/bootstrap.js +42 -17
  72. package/dist/lanes/bootstrap.js.map +1 -1
  73. package/dist/lanes/index.d.ts +1 -0
  74. package/dist/lanes/index.d.ts.map +1 -1
  75. package/dist/lanes/index.js +6 -0
  76. package/dist/lanes/index.js.map +1 -1
  77. package/dist/lanes/loader.d.ts +33 -6
  78. package/dist/lanes/loader.d.ts.map +1 -1
  79. package/dist/lanes/loader.js +44 -11
  80. package/dist/lanes/loader.js.map +1 -1
  81. package/dist/lanes/operations/create.d.ts +11 -2
  82. package/dist/lanes/operations/create.d.ts.map +1 -1
  83. package/dist/lanes/operations/create.js +15 -4
  84. package/dist/lanes/operations/create.js.map +1 -1
  85. package/dist/lanes/operations/list.d.ts +3 -2
  86. package/dist/lanes/operations/list.d.ts.map +1 -1
  87. package/dist/lanes/operations/list.js +3 -2
  88. package/dist/lanes/operations/list.js.map +1 -1
  89. package/dist/lanes/operations/move.d.ts +29 -25
  90. package/dist/lanes/operations/move.d.ts.map +1 -1
  91. package/dist/lanes/operations/move.js +45 -242
  92. package/dist/lanes/operations/move.js.map +1 -1
  93. package/dist/lanes/operations/update.d.ts +15 -5
  94. package/dist/lanes/operations/update.d.ts.map +1 -1
  95. package/dist/lanes/operations/update.js +32 -12
  96. package/dist/lanes/operations/update.js.map +1 -1
  97. package/dist/lanes/scaffold-path.d.ts +88 -0
  98. package/dist/lanes/scaffold-path.d.ts.map +1 -0
  99. package/dist/lanes/scaffold-path.js +122 -0
  100. package/dist/lanes/scaffold-path.js.map +1 -0
  101. package/dist/lanes/types.d.ts +54 -31
  102. package/dist/lanes/types.d.ts.map +1 -1
  103. package/dist/lanes/types.js +60 -21
  104. package/dist/lanes/types.js.map +1 -1
  105. package/dist/outline-split.d.ts +2 -3
  106. package/dist/outline-split.d.ts.map +1 -1
  107. package/dist/outline-split.js +2 -3
  108. package/dist/outline-split.js.map +1 -1
  109. package/dist/paths.d.ts +14 -87
  110. package/dist/paths.d.ts.map +1 -1
  111. package/dist/paths.js +21 -131
  112. package/dist/paths.js.map +1 -1
  113. package/dist/remark-strip-outline.mjs +2 -3
  114. package/dist/rename-slug.d.ts +3 -3
  115. package/dist/rename-slug.d.ts.map +1 -1
  116. package/dist/rename-slug.js +137 -44
  117. package/dist/rename-slug.js.map +1 -1
  118. package/dist/review/handlers.d.ts +3 -3
  119. package/dist/review/handlers.d.ts.map +1 -1
  120. package/dist/review/handlers.js +18 -14
  121. package/dist/review/handlers.js.map +1 -1
  122. package/dist/review/report.d.ts +13 -2
  123. package/dist/review/report.d.ts.map +1 -1
  124. package/dist/review/report.js +48 -18
  125. package/dist/review/report.js.map +1 -1
  126. package/dist/review/start-handlers.d.ts +4 -4
  127. package/dist/review/start-handlers.d.ts.map +1 -1
  128. package/dist/review/start-handlers.js +25 -25
  129. package/dist/review/start-handlers.js.map +1 -1
  130. package/dist/review/workflow-paths.d.ts +24 -26
  131. package/dist/review/workflow-paths.d.ts.map +1 -1
  132. package/dist/review/workflow-paths.js +70 -60
  133. package/dist/review/workflow-paths.js.map +1 -1
  134. package/dist/schema/draft-annotation.d.ts +8 -8
  135. package/dist/schema/entry.d.ts +6 -6
  136. package/dist/schema/journal-events.d.ts +56 -42
  137. package/dist/schema/journal-events.d.ts.map +1 -1
  138. package/dist/schema/journal-events.js +23 -9
  139. package/dist/schema/journal-events.js.map +1 -1
  140. package/dist/sidecar/read.d.ts +8 -0
  141. package/dist/sidecar/read.d.ts.map +1 -1
  142. package/dist/sidecar/read.js +36 -9
  143. package/dist/sidecar/read.js.map +1 -1
  144. package/dist/sidecar/write.d.ts +14 -0
  145. package/dist/sidecar/write.d.ts.map +1 -1
  146. package/dist/sidecar/write.js +25 -0
  147. package/dist/sidecar/write.js.map +1 -1
  148. package/package.json +9 -9
  149. package/dist/body-state.d.ts +0 -27
  150. package/dist/body-state.d.ts.map +0 -1
  151. package/dist/body-state.js +0 -62
  152. package/dist/body-state.js.map +0 -1
  153. package/dist/scaffold.d.ts +0 -67
  154. package/dist/scaffold.d.ts.map +0 -1
  155. package/dist/scaffold.js +0 -122
  156. package/dist/scaffold.js.map +0 -1
package/dist/paths.js CHANGED
@@ -24,7 +24,7 @@
24
24
  * candidate-search by template, the legacy publish path). New code with
25
25
  * access to a calendar entry should prefer `findEntryFile`.
26
26
  */
27
- import { dirname, join } from 'node:path';
27
+ import { join } from 'node:path';
28
28
  import { buildContentIndex } from "./content-index.js";
29
29
  /**
30
30
  * Resolve a user-supplied site argument to a configured site slug.
@@ -48,45 +48,31 @@ function siteConfig(config, site) {
48
48
  const slug = resolveSite(config, site);
49
49
  return config.sites[slug];
50
50
  }
51
- /** Absolute path to the site's editorial calendar file. */
52
- export function resolveCalendarPath(projectRoot, config, site) {
53
- return join(projectRoot, siteConfig(config, site).calendarPath);
54
- }
55
- /** Absolute path to the site's channels file, or undefined when the site declares none. */
56
- export function resolveChannelsPath(projectRoot, config, site) {
57
- const entry = siteConfig(config, site);
58
- return entry.channelsPath === undefined
59
- ? undefined
60
- : join(projectRoot, entry.channelsPath);
51
+ /**
52
+ * Absolute path to the project's editorial calendar file.
53
+ *
54
+ * Phase 39c (sites→lanes retirement) — the calendar is a single,
55
+ * project-level derived projection from sidecars (spec §"Calendar"). The
56
+ * legacy per-site `calendarPath` is retired; there is one calendar at
57
+ * `.deskwork/calendar.md`. The `config`/`site` parameters are retained so
58
+ * call sites (and the CLI-verb resolution path, scoped to 39c-2b) need
59
+ * not change in lockstep — the de-parameterization is internal. Both are
60
+ * intentionally ignored. Closes #234 (divergence), #357 (read-side
61
+ * validator), #223 (regen flip-flop) per spec §"Inherited calendar-surface
62
+ * cluster".
63
+ */
64
+ export function resolveCalendarPath(projectRoot, _config, _site) {
65
+ return join(projectRoot, '.deskwork', 'calendar.md');
61
66
  }
62
67
  /** Absolute path to the site's blog content directory. */
63
68
  export function resolveContentDir(projectRoot, config, site) {
64
69
  return join(projectRoot, siteConfig(config, site).contentDir);
65
70
  }
66
- /**
67
- * Bare public hostname for the site (no protocol). Returns `undefined` for
68
- * collections that aren't published as a website (no `host` configured).
69
- * Callers needing a non-undefined value for display should fall back to the
70
- * site slug; callers needing a real URL should throw if undefined.
71
- */
72
- export function resolveSiteHost(config, site) {
73
- return siteConfig(config, site).host;
74
- }
75
- /**
76
- * Canonical public base URL for the site, with trailing slash. Throws when
77
- * the collection has no `host` configured (i.e. is not published as a
78
- * website) — callers that need a URL must guarantee the collection is a
79
- * website-rendered one before calling.
80
- */
81
- export function resolveSiteBaseUrl(config, site) {
82
- const host = resolveSiteHost(config, site);
83
- if (host === undefined) {
84
- const slug = site ?? config.defaultSite;
85
- throw new Error(`Cannot resolve a base URL for collection "${slug}": no "host" is configured. ` +
86
- `Add a "host" field to .deskwork/config.json if this collection is published as a website.`);
87
- }
88
- return `https://${host}/`;
89
- }
71
+ // Phase 39c (sites→lanes retirement): `resolveSiteHost` and
72
+ // `resolveSiteBaseUrl` are removed. They had ZERO production call sites
73
+ // (the only `host` reads were two studio files reading `lane.host`
74
+ // directly). Per spec §18 they were effectively dead. Host now lives on
75
+ // the lane (`lane.host`); studio reads it from lane configs.
90
76
  const DEFAULT_BLOG_FILENAME_TEMPLATE = '{slug}/index.md';
91
77
  /**
92
78
  * Absolute path to the blog post markdown for a given slug.
@@ -167,100 +153,4 @@ export function findEntryFile(projectRoot, config, site, entryId, index, legacyE
167
153
  }
168
154
  return undefined;
169
155
  }
170
- /**
171
- * Resolve the markdown file backing a calendar entry, preferring the
172
- * UUID frontmatter binding (refactor-proof) and falling back to the
173
- * site's slug-template only when no binding exists.
174
- *
175
- * Equivalent to the studio's `resolveLongformFilePath` but exposed as a
176
- * top-level helper from `paths.ts` so CLI commands can use it without
177
- * pulling in `review/` infrastructure. Always returns an absolute path
178
- * (the slug-template fallback is unconditional); callers should
179
- * `existsSync` if they need an existence guarantee.
180
- *
181
- * Precedence:
182
- * 1. Content index — when `entryId` is supplied (and non-empty), look
183
- * up the file whose frontmatter `deskwork.id:` matches. Refactor-
184
- * proof: the binding follows the file regardless of slug rename or
185
- * directory relocation.
186
- * 2. Slug-template fallback — when the index has no record (entry's
187
- * file isn't bound to frontmatter yet, e.g. pre-doctor / pre-ingest
188
- * state) or no `entryId` was supplied, fall back to
189
- * `resolveBlogFilePath(slug)`.
190
- *
191
- * @param projectRoot Absolute path to the deskwork project root.
192
- * @param config Loaded deskwork config.
193
- * @param site Site slug (or null/undefined for the default site).
194
- * @param slug Calendar entry slug — used both as the legacy fallback
195
- * template input and as a hint for the slug-template fallback.
196
- * @param entryId Calendar entry's stable UUID. When omitted or empty,
197
- * resolution falls straight through to the slug template.
198
- * @param index Pre-built content index. When omitted, this function
199
- * builds one. Pass the per-request memoized index when
200
- * calling from the studio; let the CLI build per call.
201
- */
202
- export function resolveEntryFilePath(projectRoot, config, site, slug, entryId, index) {
203
- if (entryId !== undefined && entryId !== '') {
204
- const idx = index ?? buildContentIndex(projectRoot, config, resolveSite(config, site));
205
- const hit = idx.byId.get(entryId);
206
- if (hit !== undefined)
207
- return hit;
208
- }
209
- return resolveBlogFilePath(projectRoot, config, site, slug);
210
- }
211
- // ---------------------------------------------------------------------------
212
- // Phase 21a — shortform file resolution
213
- // ---------------------------------------------------------------------------
214
- /**
215
- * Channel must be a kebab-case token. Same shape as a slug segment so the
216
- * filename remains URL-safe and matches the rest of deskwork's vocabulary.
217
- */
218
- const CHANNEL_RE = /^[a-z0-9][a-z0-9-]*$/;
219
- /**
220
- * Resolve the markdown file path for a shortform draft.
221
- *
222
- * <contentDir>/<entry-dir>/scrapbook/shortform/<platform>[-<channel>].md
223
- *
224
- * Platform is the lowercase Platform value. Channel (if present) is appended
225
- * as `-<channel>`. Channel must validate against the kebab-case regex —
226
- * deskwork stores channels as kebab-case strings throughout.
227
- *
228
- * The entry directory is resolved through `findEntryFile` (id-driven,
229
- * refactor-proof) with slug-template fallback for legacy entries created
230
- * pre-doctor. The slug-template fallback is intentional migration logic so
231
- * pre-bind entries keep working.
232
- *
233
- * Forward-compatibility: every reference to the shortform file location
234
- * goes through this function. Phase 20 (sandbox migration) redirects this
235
- * single function; everything downstream (handlers, CLI, studio) works
236
- * unchanged.
237
- *
238
- * @param projectRoot Absolute path to the deskwork project root.
239
- * @param config Loaded deskwork config.
240
- * @param site Site slug (or null/undefined for the default site).
241
- * @param entry Calendar entry — `id` preferred, `slug` used both as the
242
- * legacy fallback and to identify the entry directory.
243
- * @param platform Which distribution platform.
244
- * @param channel Optional sub-channel (e.g. `synthdiy` for r/synthdiy).
245
- * Must be kebab-case.
246
- * @param index Optional pre-built content index (per-request memoization).
247
- * @returns absolute file path, or undefined when neither the index nor the
248
- * slug-template fallback resolves the entry's directory.
249
- */
250
- export function resolveShortformFilePath(projectRoot, config, site, entry, platform, channel, index) {
251
- if (channel !== undefined && channel !== '') {
252
- if (!CHANNEL_RE.test(channel)) {
253
- throw new Error(`Invalid shortform channel "${channel}": must match ${CHANNEL_RE} ` +
254
- `(kebab-case, same shape as a slug segment).`);
255
- }
256
- }
257
- const entryFile = findEntryFile(projectRoot, config, site, entry.id ?? '', index, { slug: entry.slug });
258
- if (entryFile === undefined)
259
- return undefined;
260
- const entryDir = dirname(entryFile);
261
- const filename = channel !== undefined && channel !== ''
262
- ? `${platform}-${channel}.md`
263
- : `${platform}.md`;
264
- return join(entryDir, 'scrapbook', 'shortform', filename);
265
- }
266
156
  //# sourceMappingURL=paths.js.map
package/dist/paths.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"paths.js","sourceRoot":"","sources":["../src/paths.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"paths.js","sourceRoot":"","sources":["../src/paths.ts"],"names":[],"mappings":"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"}
@@ -11,9 +11,8 @@
11
11
  *
12
12
  * Stripping shape: find the first H2 whose text starts with "Outline",
13
13
  * then remove that heading plus every subsequent top-level node until
14
- * the next H1 or H2 (non-inclusive) or the end of the document. Matches
15
- * the line-based stripper in `body-state.ts`; kept independent here
16
- * because mdast traversal beats regex on structured content.
14
+ * the next H1 or H2 (non-inclusive) or the end of the document. Uses
15
+ * mdast traversal (beats regex on structured content).
17
16
  *
18
17
  * No-op when the document has no outline section.
19
18
  */
@@ -4,7 +4,8 @@
4
4
  * Renames the per-post directory at `<contentDir>/<slug>/` to its new
5
5
  * name, updates the calendar entry's slug, syncs slug on matching
6
6
  * distribution records, and (optionally) appends a 301 redirect block
7
- * to the site's `_redirects` file. UUID identity keeps workflows,
7
+ * to the `_redirects` file named by the entry's lane (`lane.redirectsPath`,
8
+ * Phase 39c c4). UUID identity keeps workflows,
8
9
  * distribution records, and journal history joined through `entry.id`
9
10
  * across the rename.
10
11
  *
@@ -18,13 +19,12 @@ import type { DeskworkConfig } from './config.ts';
18
19
  export interface RenameSlugOptions {
19
20
  projectRoot: string;
20
21
  config: DeskworkConfig;
21
- site: string;
22
22
  oldSlug: string;
23
23
  newSlug: string;
24
24
  dryRun?: boolean;
25
25
  }
26
26
  export interface RenameSlugPlanAction {
27
- kind: 'dir-rename' | 'calendar-slug-change' | 'distribution-slug-sync' | 'redirect-append';
27
+ kind: 'dir-rename' | 'calendar-slug-change' | 'distribution-slug-sync' | 'redirect-append' | 'redirect-skipped';
28
28
  summary: string;
29
29
  details?: string;
30
30
  }
@@ -1 +1 @@
1
- {"version":3,"file":"rename-slug.d.ts","sourceRoot":"","sources":["../src/rename-slug.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAIH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAKlD,MAAM,WAAW,iBAAiB;IAChC,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,cAAc,CAAC;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,oBAAoB;IACnC,IAAI,EACA,YAAY,GACZ,sBAAsB,GACtB,wBAAwB,GACxB,iBAAiB,CAAC;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,oBAAoB,EAAE,CAAC;IAChC,MAAM,EAAE,OAAO,CAAC;CACjB;AAID,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAI/C;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAS3E;AAUD;;GAEG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,iBAAiB,GAAG,gBAAgB,CA6HvE"}
1
+ {"version":3,"file":"rename-slug.d.ts","sourceRoot":"","sources":["../src/rename-slug.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAIH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AA8ClD,MAAM,WAAW,iBAAiB;IAChC,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,cAAc,CAAC;IACvB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,oBAAoB;IACnC,IAAI,EACA,YAAY,GACZ,sBAAsB,GACtB,wBAAwB,GACxB,iBAAiB,GACjB,kBAAkB,CAAC;IACvB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,oBAAoB,EAAE,CAAC;IAChC,MAAM,EAAE,OAAO,CAAC;CACjB;AAID,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAI/C;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAS3E;AAED;;GAEG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,iBAAiB,GAAG,gBAAgB,CA8LvE"}
@@ -4,7 +4,8 @@
4
4
  * Renames the per-post directory at `<contentDir>/<slug>/` to its new
5
5
  * name, updates the calendar entry's slug, syncs slug on matching
6
6
  * distribution records, and (optionally) appends a 301 redirect block
7
- * to the site's `_redirects` file. UUID identity keeps workflows,
7
+ * to the `_redirects` file named by the entry's lane (`lane.redirectsPath`,
8
+ * Phase 39c c4). UUID identity keeps workflows,
8
9
  * distribution records, and journal history joined through `entry.id`
9
10
  * across the rename.
10
11
  *
@@ -16,9 +17,45 @@
16
17
  */
17
18
  import { existsSync, renameSync, writeFileSync, appendFileSync } from 'node:fs';
18
19
  import { join } from 'node:path';
19
- import { resolveBlogPostDir, resolveCalendarPath } from "./paths.js";
20
+ import { resolveCalendarPath } from "./paths.js";
20
21
  import { readCalendar, writeCalendar } from "./calendar.js";
21
- import { effectiveContentType } from "./types.js";
22
+ import { readSidecarSync } from "./sidecar/read.js";
23
+ import { writeSidecarSync } from "./sidecar/write.js";
24
+ import { sidecarPath } from "./sidecar/paths.js";
25
+ import { loadLaneConfigSchemaOnly } from "./lanes/loader.js";
26
+ /**
27
+ * Detect a slug rename's filesystem shape from the entry's stored
28
+ * (POSIX, relative) `artifactPath` (Phase 39c-2b(a), spec AUDIT-36).
29
+ *
30
+ * - `…/<slug>/index.md` → move the per-post DIR `<base>/<slug>`
31
+ * - `…/<slug>/README.md` → move the per-post DIR `<base>/<slug>`
32
+ * - `…/<slug>.<ext>` → move the FILE
33
+ *
34
+ * Returns the relative move source + target (dir or file) plus the new
35
+ * relative artifactPath. No naive slug-substring replacement — only the
36
+ * slug segment changes, never a same-named ancestor directory.
37
+ */
38
+ function planArtifactMove(artifactPath, newSlug) {
39
+ const segments = artifactPath.split('/');
40
+ const filename = segments[segments.length - 1];
41
+ const isDirLayout = filename === 'index.md' || filename === 'README.md';
42
+ if (isDirLayout) {
43
+ // <base>/<slug>/<filename> → move the <slug> dir.
44
+ const slugDirRel = segments.slice(0, -1).join('/'); // <base>/<slug>
45
+ const baseRel = segments.slice(0, -2).join('/'); // <base>
46
+ const toDirRel = baseRel === '' ? newSlug : `${baseRel}/${newSlug}`;
47
+ return {
48
+ fromRel: slugDirRel,
49
+ toRel: toDirRel,
50
+ newArtifactRel: `${toDirRel}/${filename}`,
51
+ };
52
+ }
53
+ // Flat: <base>/<slug>.<ext> → move the file.
54
+ const ext = filename.slice(filename.lastIndexOf('.'));
55
+ const baseRel = segments.slice(0, -1).join('/');
56
+ const toFileRel = baseRel === '' ? `${newSlug}${ext}` : `${baseRel}/${newSlug}${ext}`;
57
+ return { fromRel: artifactPath, toRel: toFileRel, newArtifactRel: toFileRel };
58
+ }
22
59
  const SLUG_RE = /^[a-z0-9][a-z0-9-]*(\/[a-z0-9][a-z0-9-]*)*$/;
23
60
  export function validateSlug(slug) {
24
61
  if (!SLUG_RE.test(slug)) {
@@ -40,29 +77,21 @@ export function buildRedirectBlock(oldSlug, newSlug) {
40
77
  '',
41
78
  ].join('\n');
42
79
  }
43
- function siteEntry(config, site) {
44
- if (!(site in config.sites)) {
45
- const known = Object.keys(config.sites).join(', ');
46
- throw new Error(`unknown site "${site}". Configured sites: ${known}`);
47
- }
48
- return config.sites[site];
49
- }
50
80
  /**
51
81
  * Execute (or dry-run) a slug rename.
52
82
  */
53
83
  export function renameSlug(options) {
54
- const { projectRoot, config, site, oldSlug, newSlug, dryRun = false } = options;
84
+ const { projectRoot, config, oldSlug, newSlug, dryRun = false } = options;
55
85
  validateSlug(oldSlug);
56
86
  validateSlug(newSlug);
57
87
  if (oldSlug === newSlug) {
58
88
  throw new Error('oldSlug and newSlug are identical — nothing to do');
59
89
  }
60
- const siteCfg = siteEntry(config, site);
61
- const calendarPath = resolveCalendarPath(projectRoot, config, site);
90
+ const calendarPath = resolveCalendarPath(projectRoot, config);
62
91
  const calendar = readCalendar(calendarPath);
63
92
  const entry = calendar.entries.find((e) => e.slug === oldSlug);
64
93
  if (!entry) {
65
- throw new Error(`no calendar entry with slug "${oldSlug}" on site "${site}"`);
94
+ throw new Error(`no calendar entry with slug "${oldSlug}"`);
66
95
  }
67
96
  if (!entry.id) {
68
97
  throw new Error(`entry "${oldSlug}" has no UUID — re-save the calendar to backfill`);
@@ -72,35 +101,90 @@ export function renameSlug(options) {
72
101
  throw new Error(`slug "${newSlug}" is already taken by entry ${collision.id ?? '(no id)'} (${collision.title})`);
73
102
  }
74
103
  const actions = [];
75
- const oldDir = resolveBlogPostDir(projectRoot, config, site, oldSlug);
76
- const newDir = resolveBlogPostDir(projectRoot, config, site, newSlug);
77
- // 1. Directory rename. Under the dir-based layout blog posts live as
78
- // `<contentDir>/<slug>/` with assets co-located, so a single mv
79
- // carries the markdown + co-located assets in one atomic
80
- // operation. For blog entries the directory must exist when the
81
- // layout is dir-based if it's missing the calendar row has
82
- // drifted from disk and the operator needs to reconcile before
83
- // rename can proceed. Flat-file layouts (e.g.
84
- // `blogFilenameTemplate: "{slug}.md"`) don't have a per-post
85
- // directory; the rename is then calendar-only.
86
- const isDirLayout = !siteCfg.blogFilenameTemplate || siteCfg.blogFilenameTemplate.includes('/');
87
- const dirExists = existsSync(oldDir);
88
- if (isDirLayout && !dirExists && effectiveContentType(entry) === 'blog') {
89
- throw new Error(`calendar entry "${oldSlug}" is a blog post but no directory exists at ${oldDir}. ` +
90
- `The calendar row has drifted from disk — reconcile the row's slug to match the actual ` +
91
- `directory name, then re-run the rename against the real slug.`);
92
- }
93
- if (dirExists) {
94
- if (existsSync(newDir)) {
95
- throw new Error(`target directory already exists: ${newDir}`);
104
+ // 1. Move the artifact. Phase 39c-2b(a) / spec AUDIT-36: the move is
105
+ // derived from the entry's STORED artifactPath (layout-detected)
106
+ // not a slug-template dir. For an index/README layout the per-post
107
+ // DIR moves (carrying co-located assets in one mv); for a flat
108
+ // layout the FILE moves. The sidecar's artifactPath is rewritten to
109
+ // the new location. No naive slug-substring replacement.
110
+ // AUDIT-20260604-02: an entry with a calendar row but no sidecar file is
111
+ // a drift case like any other surface the actionable doctor --fix
112
+ // guidance, not the raw `sidecar not found` ENOENT from readSidecarSync.
113
+ // AUDIT-20260604-05: distinguish a MISSING sidecar (file absent) from a
114
+ // CORRUPT one (file present but invalid JSON/schema). A bare catch would
115
+ // misreport corruption as "no sidecar on disk" and send the operator to
116
+ // the wrong remedy — re-throw readSidecarSync's accurate diagnosis when
117
+ // the file actually exists.
118
+ let sidecar;
119
+ try {
120
+ sidecar = readSidecarSync(projectRoot, entry.id);
121
+ }
122
+ catch (err) {
123
+ if (existsSync(sidecarPath(projectRoot, entry.id))) {
124
+ throw err;
96
125
  }
97
- actions.push({
98
- kind: 'dir-rename',
99
- summary: 'rename post directory',
100
- details: `${oldDir}\n → ${newDir}`,
126
+ throw new Error(`entry "${oldSlug}" (${entry.id}) has a calendar row but no sidecar on ` +
127
+ `disk — run \`deskwork doctor --fix\` to reconcile before renaming.`);
128
+ }
129
+ if (sidecar.artifactPath === undefined) {
130
+ throw new Error(`entry "${oldSlug}" (${entry.id}) has no artifactPath — run ` +
131
+ `\`deskwork doctor --fix\` to backfill it before renaming.`);
132
+ }
133
+ // Resolve the optional 301-redirect target (lane.redirectsPath, spec
134
+ // Decision #23) BEFORE any filesystem/calendar mutation (AUDIT-20260604-08
135
+ // — resolving here, not at the append step, means a resolution failure can
136
+ // never leave a half-applied rename).
137
+ //
138
+ // The redirect lives on the lane, but the pipeline template is ORTHOGONAL
139
+ // to it: read the lane SCHEMA-ONLY (AUDIT-20260604-19) so a valid
140
+ // `redirectsPath` still appends when the lane's pipeline is mid-edit /
141
+ // renamed / deleted. `loadLaneConfig` would cross-validate the pipeline and
142
+ // throw — silently dropping a perfectly valid redirect because an unrelated
143
+ // field didn't resolve (the SEO regression AUDIT-17/19 filed).
144
+ //
145
+ // When the lane cannot be consulted at all — config absent (archived /
146
+ // purged / legacy-stale) or unreadable/corrupt — the optional redirect is
147
+ // skipped, but VISIBLY: a `redirect-skipped` action records the reason so
148
+ // the omission surfaces in the result the caller renders (AUDIT-20260604-19
149
+ // — no silent drop, no phantom "attributability"). A renamed entry whose
150
+ // lane is gone is still a valid rename; the broken lane is surfaced by the
151
+ // lane-config doctor rules, not by crashing the rename.
152
+ let redirectsPath;
153
+ if (sidecar.lane !== undefined) {
154
+ try {
155
+ redirectsPath = loadLaneConfigSchemaOnly(sidecar.lane, projectRoot).redirectsPath;
156
+ }
157
+ catch (err) {
158
+ const reason = err instanceof Error ? err.message : String(err);
159
+ actions.push({
160
+ kind: 'redirect-skipped',
161
+ summary: `skipped 301 redirect — lane "${sidecar.lane}" could not be read`,
162
+ details: reason,
163
+ });
164
+ redirectsPath = undefined;
165
+ }
166
+ }
167
+ const move = planArtifactMove(sidecar.artifactPath, newSlug);
168
+ const fromAbs = join(projectRoot, move.fromRel);
169
+ const toAbs = join(projectRoot, move.toRel);
170
+ if (!existsSync(fromAbs)) {
171
+ throw new Error(`entry "${oldSlug}" artifactPath ${sidecar.artifactPath} does not exist on disk at ` +
172
+ `${fromAbs}. The calendar/sidecar has drifted from disk — reconcile before renaming.`);
173
+ }
174
+ if (existsSync(toAbs)) {
175
+ throw new Error(`target already exists: ${toAbs}`);
176
+ }
177
+ actions.push({
178
+ kind: 'dir-rename',
179
+ summary: 'move post artifact',
180
+ details: `${fromAbs}\n → ${toAbs}`,
181
+ });
182
+ if (!dryRun) {
183
+ renameSync(fromAbs, toAbs);
184
+ writeSidecarSync(projectRoot, {
185
+ ...sidecar,
186
+ artifactPath: move.newArtifactRel,
101
187
  });
102
- if (!dryRun)
103
- renameSync(oldDir, newDir);
104
188
  }
105
189
  // 2. Calendar entry slug change
106
190
  actions.push({
@@ -129,9 +213,18 @@ export function renameSlug(options) {
129
213
  if (!dryRun) {
130
214
  writeCalendar(calendarPath, calendar);
131
215
  }
132
- // 4. _redirects append (when site has redirectsPath configured)
133
- if (siteCfg.redirectsPath) {
134
- const redirectsFile = join(projectRoot, siteCfg.redirectsPath);
216
+ // 4. _redirects append (when the entry's LANE has redirectsPath
217
+ // configured). Phase 39c (c4 / spec Decision #23): `redirectsPath`
218
+ // re-homed from the retired `SiteConfig` onto the lane (an optional
219
+ // sibling of `lane.host`), mirroring Decision #2's `host` re-home.
220
+ // The renamed entry's lane is resolved from its sidecar's `lane`
221
+ // field. When the sidecar carries no lane, or the lane declares no
222
+ // redirectsPath, the append is SKIPPED — this is optional
223
+ // website-publishing metadata; skipping is the correct unset
224
+ // behavior, NOT an error (a collection without a renderer is valid).
225
+ // `redirectsPath` was resolved before the mutation steps (see above).
226
+ if (redirectsPath !== undefined) {
227
+ const redirectsFile = join(projectRoot, redirectsPath);
135
228
  const block = buildRedirectBlock(oldSlug, newSlug);
136
229
  actions.push({
137
230
  kind: 'redirect-append',
@@ -1 +1 @@
1
- {"version":3,"file":"rename-slug.js","sourceRoot":"","sources":["../src/rename-slug.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAChF,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEjC,OAAO,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AACrE,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC5D,OAAO,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAC;AA6BlD,MAAM,OAAO,GAAG,6CAA6C,CAAC;AAE9D,MAAM,UAAU,YAAY,CAAC,IAAY;IACvC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;QACxB,MAAM,IAAI,KAAK,CAAC,iBAAiB,IAAI,kBAAkB,OAAO,EAAE,CAAC,CAAC;IACpE,CAAC;AACH,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,kBAAkB,CAAC,OAAe,EAAE,OAAe;IACjE,OAAO;QACL,EAAE;QACF,wBAAwB,OAAO,aAAa,OAAO,GAAG;QACtD,SAAS,OAAO,iBAAiB,OAAO,gBAAgB;QACxD,SAAS,OAAO,iBAAiB,OAAO,gBAAgB;QACxD,SAAS,OAAO,iBAAiB,OAAO,gBAAgB;QACxD,EAAE;KACH,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACf,CAAC;AAED,SAAS,SAAS,CAAC,MAAsB,EAAE,IAAY;IACrD,IAAI,CAAC,CAAC,IAAI,IAAI,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC;QAC5B,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACnD,MAAM,IAAI,KAAK,CAAC,iBAAiB,IAAI,wBAAwB,KAAK,EAAE,CAAC,CAAC;IACxE,CAAC;IACD,OAAO,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;AAC5B,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,UAAU,CAAC,OAA0B;IACnD,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,GAAG,KAAK,EAAE,GAAG,OAAO,CAAC;IAChF,YAAY,CAAC,OAAO,CAAC,CAAC;IACtB,YAAY,CAAC,OAAO,CAAC,CAAC;IACtB,IAAI,OAAO,KAAK,OAAO,EAAE,CAAC;QACxB,MAAM,IAAI,KAAK,CAAC,mDAAmD,CAAC,CAAC;IACvE,CAAC;IAED,MAAM,OAAO,GAAG,SAAS,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;IACxC,MAAM,YAAY,GAAG,mBAAmB,CAAC,WAAW,EAAE,MAAM,EAAE,IAAI,CAAC,CAAC;IACpE,MAAM,QAAQ,GAAG,YAAY,CAAC,YAAY,CAAC,CAAC;IAC5C,MAAM,KAAK,GAAG,QAAQ,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,OAAO,CAAC,CAAC;IAC/D,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,MAAM,IAAI,KAAK,CACb,gCAAgC,OAAO,cAAc,IAAI,GAAG,CAC7D,CAAC;IACJ,CAAC;IACD,IAAI,CAAC,KAAK,CAAC,EAAE,EAAE,CAAC;QACd,MAAM,IAAI,KAAK,CACb,UAAU,OAAO,kDAAkD,CACpE,CAAC;IACJ,CAAC;IAED,MAAM,SAAS,GAAG,QAAQ,CAAC,OAAO,CAAC,IAAI,CACrC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,OAAO,IAAI,CAAC,CAAC,EAAE,KAAK,KAAK,CAAC,EAAE,CAC/C,CAAC;IACF,IAAI,SAAS,EAAE,CAAC;QACd,MAAM,IAAI,KAAK,CACb,SAAS,OAAO,+BAA+B,SAAS,CAAC,EAAE,IAAI,SAAS,KAAK,SAAS,CAAC,KAAK,GAAG,CAChG,CAAC;IACJ,CAAC;IAED,MAAM,OAAO,GAA2B,EAAE,CAAC;IAC3C,MAAM,MAAM,GAAG,kBAAkB,CAAC,WAAW,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,CAAC,CAAC;IACtE,MAAM,MAAM,GAAG,kBAAkB,CAAC,WAAW,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,CAAC,CAAC;IAEtE,qEAAqE;IACrE,mEAAmE;IACnE,4DAA4D;IAC5D,mEAAmE;IACnE,gEAAgE;IAChE,kEAAkE;IAClE,iDAAiD;IACjD,gEAAgE;IAChE,kDAAkD;IAClD,MAAM,WAAW,GAAG,CAAC,OAAO,CAAC,oBAAoB,IAAI,OAAO,CAAC,oBAAoB,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC;IAChG,MAAM,SAAS,GAAG,UAAU,CAAC,MAAM,CAAC,CAAC;IACrC,IAAI,WAAW,IAAI,CAAC,SAAS,IAAI,oBAAoB,CAAC,KAAK,CAAC,KAAK,MAAM,EAAE,CAAC;QACxE,MAAM,IAAI,KAAK,CACb,mBAAmB,OAAO,+CAA+C,MAAM,IAAI;YACjF,wFAAwF;YACxF,+DAA+D,CAClE,CAAC;IACJ,CAAC;IACD,IAAI,SAAS,EAAE,CAAC;QACd,IAAI,UAAU,CAAC,MAAM,CAAC,EAAE,CAAC;YACvB,MAAM,IAAI,KAAK,CAAC,oCAAoC,MAAM,EAAE,CAAC,CAAC;QAChE,CAAC;QACD,OAAO,CAAC,IAAI,CAAC;YACX,IAAI,EAAE,YAAY;YAClB,OAAO,EAAE,uBAAuB;YAChC,OAAO,EAAE,GAAG,MAAM,gBAAgB,MAAM,EAAE;SAC3C,CAAC,CAAC;QACH,IAAI,CAAC,MAAM;YAAE,UAAU,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC1C,CAAC;IAED,gCAAgC;IAChC,OAAO,CAAC,IAAI,CAAC;QACX,IAAI,EAAE,sBAAsB;QAC5B,OAAO,EAAE,yBAAyB,OAAO,QAAQ,OAAO,GAAG;QAC3D,OAAO,EAAE,YAAY,KAAK,CAAC,EAAE,wDAAwD;KACtF,CAAC,CAAC;IACH,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,KAAK,CAAC,IAAI,GAAG,OAAO,CAAC;IACvB,CAAC;IAED,+DAA+D;IAC/D,MAAM,qBAAqB,GAAG,QAAQ,CAAC,aAAa,CAAC,MAAM,CACzD,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,KAAK,KAAK,CAAC,EAAE,CAC9B,CAAC;IACF,IAAI,qBAAqB,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACrC,OAAO,CAAC,IAAI,CAAC;YACX,IAAI,EAAE,wBAAwB;YAC9B,OAAO,EAAE,gBAAgB,qBAAqB,CAAC,MAAM,yBAAyB;YAC9E,OAAO,EAAE,qBAAqB;iBAC3B,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,CAAC,QAAQ,GAAG,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,YAAY,OAAO,GAAG,CAAC;iBACpF,IAAI,CAAC,IAAI,CAAC;SACd,CAAC,CAAC;QACH,IAAI,CAAC,MAAM,EAAE,CAAC;YACZ,KAAK,MAAM,CAAC,IAAI,qBAAqB;gBAAE,CAAC,CAAC,IAAI,GAAG,OAAO,CAAC;QAC1D,CAAC;IACH,CAAC;IAED,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,aAAa,CAAC,YAAY,EAAE,QAAQ,CAAC,CAAC;IACxC,CAAC;IAED,gEAAgE;IAChE,IAAI,OAAO,CAAC,aAAa,EAAE,CAAC;QAC1B,MAAM,aAAa,GAAG,IAAI,CAAC,WAAW,EAAE,OAAO,CAAC,aAAa,CAAC,CAAC;QAC/D,MAAM,KAAK,GAAG,kBAAkB,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;QACnD,OAAO,CAAC,IAAI,CAAC;YACX,IAAI,EAAE,iBAAiB;YACvB,OAAO,EAAE,yCAAyC;YAClD,OAAO,EAAE,WAAW,aAAa,KAAK,KAAK;iBACxC,KAAK,CAAC,IAAI,CAAC;iBACX,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,YAAY,CAAC,EAAE,CAAC;iBAC3B,IAAI,CAAC,IAAI,CAAC,EAAE;SAChB,CAAC,CAAC;QACH,IAAI,CAAC,MAAM,EAAE,CAAC;YACZ,IAAI,CAAC,UAAU,CAAC,aAAa,CAAC,EAAE,CAAC;gBAC/B,aAAa,CAAC,aAAa,EAAE,KAAK,EAAE,OAAO,CAAC,CAAC;YAC/C,CAAC;iBAAM,CAAC;gBACN,cAAc,CAAC,aAAa,EAAE,KAAK,EAAE,OAAO,CAAC,CAAC;YAChD,CAAC;QACH,CAAC;IACH,CAAC;IAED,OAAO;QACL,OAAO,EAAE,KAAK,CAAC,EAAE;QACjB,OAAO;QACP,OAAO;QACP,OAAO;QACP,MAAM;KACP,CAAC;AACJ,CAAC"}
1
+ {"version":3,"file":"rename-slug.js","sourceRoot":"","sources":["../src/rename-slug.ts"],"names":[],"mappings":"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"}
@@ -4,9 +4,9 @@
4
4
  * { status, body } results that routes serialize as JSON.
5
5
  *
6
6
  * Ported from audiocontrol.org's scripts/lib/editorial-review/handlers.ts.
7
- * Site validation now checks against config.sites rather than a hardcoded
8
- * SITES array, and the longform blog file path uses resolveBlogFilePath
9
- * (which honors the site's blogFilenameTemplate config).
7
+ * Phase 39c-2b(a): the longform + shortform file paths now resolve from
8
+ * the entry's stored `artifactPath` (via `review/workflow-paths.ts`), not
9
+ * the slug-template see that module.
10
10
  */
11
11
  import type { DeskworkConfig } from '../config.ts';
12
12
  import { type HandlerResult } from './result.ts';
@@ -1 +1 @@
1
- {"version":3,"file":"handlers.d.ts","sourceRoot":"","sources":["../../src/review/handlers.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAGH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAanD,OAAO,EAAW,KAAK,aAAa,EAAE,MAAM,aAAa,CAAC;AAE1D,YAAY,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AACjD,OAAO,EAAE,mBAAmB,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAUhF,wBAAgB,cAAc,CAC5B,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,cAAc,EACtB,IAAI,EAAE,OAAO,GACZ,aAAa,CAoJf;AAED,wBAAgB,qBAAqB,CACnC,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,cAAc,EACtB,KAAK,EAAE;IAAE,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,GAC3D,aAAa,CAWf;AAOD,wBAAgB,cAAc,CAC5B,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,cAAc,EACtB,IAAI,EAAE,OAAO,GACZ,aAAa,CAaf;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,iBAAiB,CAC/B,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,cAAc,EACtB,KAAK,EAAE;IACL,EAAE,EAAE,MAAM,GAAG,IAAI,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;CACxB,GACA,aAAa,CAoDf;AAQD;;;;GAIG;AACH,wBAAgB,mBAAmB,CACjC,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,cAAc,EACtB,IAAI,EAAE,OAAO,GACZ,aAAa,CAyDf;AAED;;;;GAIG;AACH,wBAAgB,QAAQ,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAgBrD;AAED,mFAAmF;AACnF,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAQlD"}
1
+ {"version":3,"file":"handlers.d.ts","sourceRoot":"","sources":["../../src/review/handlers.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAGH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAanD,OAAO,EAAW,KAAK,aAAa,EAAE,MAAM,aAAa,CAAC;AAE1D,YAAY,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AACjD,OAAO,EAAE,mBAAmB,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAUhF,wBAAgB,cAAc,CAC5B,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,cAAc,EACtB,IAAI,EAAE,OAAO,GACZ,aAAa,CAoJf;AAED,wBAAgB,qBAAqB,CACnC,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,cAAc,EACtB,KAAK,EAAE;IAAE,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,GAC3D,aAAa,CAWf;AAOD,wBAAgB,cAAc,CAC5B,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,cAAc,EACtB,IAAI,EAAE,OAAO,GACZ,aAAa,CAaf;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,iBAAiB,CAC/B,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,cAAc,EACtB,KAAK,EAAE;IACL,EAAE,EAAE,MAAM,GAAG,IAAI,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;CACxB,GACA,aAAa,CAmDf;AAQD;;;;GAIG;AACH,wBAAgB,mBAAmB,CACjC,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,cAAc,EACtB,IAAI,EAAE,OAAO,GACZ,aAAa,CA6Df;AAED;;;;GAIG;AACH,wBAAgB,QAAQ,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAgBrD;AAED,mFAAmF;AACnF,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAQlD"}
@@ -4,9 +4,9 @@
4
4
  * { status, body } results that routes serialize as JSON.
5
5
  *
6
6
  * Ported from audiocontrol.org's scripts/lib/editorial-review/handlers.ts.
7
- * Site validation now checks against config.sites rather than a hardcoded
8
- * SITES array, and the longform blog file path uses resolveBlogFilePath
9
- * (which honors the site's blogFilenameTemplate config).
7
+ * Phase 39c-2b(a): the longform + shortform file paths now resolve from
8
+ * the entry's stored `artifactPath` (via `review/workflow-paths.ts`), not
9
+ * the slug-template see that module.
10
10
  */
11
11
  import { existsSync, writeFileSync } from 'node:fs';
12
12
  import { appendAnnotation, appendVersion, mintAnnotation, readAnnotations, readVersions, readWorkflow, readWorkflows, transitionState, } from "./pipeline.js";
@@ -212,10 +212,9 @@ export function handleGetWorkflow(projectRoot, config, query) {
212
212
  if (!query.entryId && (!query.site || !query.slug)) {
213
213
  return err(400, 'either id, entryId, or (site & slug) query params are required');
214
214
  }
215
- if (query.site && !(query.site in config.sites)) {
216
- const known = Object.keys(config.sites).join(', ');
217
- return err(400, `unknown site: ${query.site}. Configured: ${known}`);
218
- }
215
+ // Phase 39c c3 (spec Decision #22): `site` is an opaque recorded label,
216
+ // no longer validated against `config.sites`. An unrecognized site for
217
+ // which no workflow matches falls through to the existing 404 below.
219
218
  const contentKind = (query.contentKind ?? 'longform');
220
219
  const candidates = readWorkflows(projectRoot, config).filter((w) => {
221
220
  // Stable-identity join when entryId is present on both sides;
@@ -279,13 +278,18 @@ export function handleCreateVersion(projectRoot, config, body) {
279
278
  // shortform) are disk-backed — Phase 21a removed the shortform special
280
279
  // case so the studio's save/iterate/approve handlers see one shape.
281
280
  //
282
- // Resolve via the content index first (so writingcontrol-shaped layouts
283
- // where slug != fs path work), with template fallback for legacy
284
- // / pre-doctor cases. For shortform, the path goes through
285
- // resolveShortformFilePath (entry-dir + scrapbook/shortform/<…>.md).
286
- const targetFile = workflowFilePath(projectRoot, config, workflow);
287
- if (targetFile === undefined || !existsSync(targetFile)) {
288
- const shown = targetFile ?? '(unresolved)';
281
+ // Phase 39c-2b(a): workflowFilePath resolves via the entry's stored
282
+ // artifactPath and throws doctor --fix for an unmigrated entry surface
283
+ // that as a save error rather than crashing the route handler.
284
+ let targetFile;
285
+ try {
286
+ targetFile = workflowFilePath(projectRoot, config, workflow);
287
+ }
288
+ catch (e) {
289
+ return err(500, e instanceof Error ? e.message : String(e));
290
+ }
291
+ if (!existsSync(targetFile)) {
292
+ const shown = targetFile;
289
293
  if (workflow.contentKind === 'shortform') {
290
294
  return err(500, `cannot save: shortform file missing at ${shown}. ` +
291
295
  `Run /deskwork:shortform-start to scaffold the file before saving edits.`);