@deskwork/core 0.9.5

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 (188) hide show
  1. package/dist/body-state.d.ts +27 -0
  2. package/dist/body-state.d.ts.map +1 -0
  3. package/dist/body-state.js +62 -0
  4. package/dist/body-state.js.map +1 -0
  5. package/dist/calendar-mutations.d.ts +124 -0
  6. package/dist/calendar-mutations.d.ts.map +1 -0
  7. package/dist/calendar-mutations.js +305 -0
  8. package/dist/calendar-mutations.js.map +1 -0
  9. package/dist/calendar.d.ts +54 -0
  10. package/dist/calendar.d.ts.map +1 -0
  11. package/dist/calendar.js +430 -0
  12. package/dist/calendar.js.map +1 -0
  13. package/dist/cli.d.ts +38 -0
  14. package/dist/cli.d.ts.map +1 -0
  15. package/dist/cli.js +72 -0
  16. package/dist/cli.js.map +1 -0
  17. package/dist/config.d.ts +91 -0
  18. package/dist/config.d.ts.map +1 -0
  19. package/dist/config.js +216 -0
  20. package/dist/config.js.map +1 -0
  21. package/dist/content-index.d.ts +74 -0
  22. package/dist/content-index.d.ts.map +1 -0
  23. package/dist/content-index.js +205 -0
  24. package/dist/content-index.js.map +1 -0
  25. package/dist/content-tree-fs-walk.d.ts +54 -0
  26. package/dist/content-tree-fs-walk.d.ts.map +1 -0
  27. package/dist/content-tree-fs-walk.js +112 -0
  28. package/dist/content-tree-fs-walk.js.map +1 -0
  29. package/dist/content-tree-helpers.d.ts +52 -0
  30. package/dist/content-tree-helpers.d.ts.map +1 -0
  31. package/dist/content-tree-helpers.js +116 -0
  32. package/dist/content-tree-helpers.js.map +1 -0
  33. package/dist/content-tree-types.d.ts +175 -0
  34. package/dist/content-tree-types.d.ts.map +1 -0
  35. package/dist/content-tree-types.js +10 -0
  36. package/dist/content-tree-types.js.map +1 -0
  37. package/dist/content-tree.d.ts +93 -0
  38. package/dist/content-tree.d.ts.map +1 -0
  39. package/dist/content-tree.js +385 -0
  40. package/dist/content-tree.js.map +1 -0
  41. package/dist/doctor/index.d.ts +11 -0
  42. package/dist/doctor/index.d.ts.map +1 -0
  43. package/dist/doctor/index.js +10 -0
  44. package/dist/doctor/index.js.map +1 -0
  45. package/dist/doctor/project-rules.d.ts +59 -0
  46. package/dist/doctor/project-rules.d.ts.map +1 -0
  47. package/dist/doctor/project-rules.js +143 -0
  48. package/dist/doctor/project-rules.js.map +1 -0
  49. package/dist/doctor/rules/calendar-uuid-missing.d.ts +19 -0
  50. package/dist/doctor/rules/calendar-uuid-missing.d.ts.map +1 -0
  51. package/dist/doctor/rules/calendar-uuid-missing.js +176 -0
  52. package/dist/doctor/rules/calendar-uuid-missing.js.map +1 -0
  53. package/dist/doctor/rules/duplicate-id.d.ts +27 -0
  54. package/dist/doctor/rules/duplicate-id.d.ts.map +1 -0
  55. package/dist/doctor/rules/duplicate-id.js +157 -0
  56. package/dist/doctor/rules/duplicate-id.js.map +1 -0
  57. package/dist/doctor/rules/legacy-top-level-id-migration.d.ts +40 -0
  58. package/dist/doctor/rules/legacy-top-level-id-migration.d.ts.map +1 -0
  59. package/dist/doctor/rules/legacy-top-level-id-migration.js +232 -0
  60. package/dist/doctor/rules/legacy-top-level-id-migration.js.map +1 -0
  61. package/dist/doctor/rules/missing-frontmatter-id.d.ts +45 -0
  62. package/dist/doctor/rules/missing-frontmatter-id.d.ts.map +1 -0
  63. package/dist/doctor/rules/missing-frontmatter-id.js +283 -0
  64. package/dist/doctor/rules/missing-frontmatter-id.js.map +1 -0
  65. package/dist/doctor/rules/orphan-frontmatter-id.d.ts +18 -0
  66. package/dist/doctor/rules/orphan-frontmatter-id.d.ts.map +1 -0
  67. package/dist/doctor/rules/orphan-frontmatter-id.js +154 -0
  68. package/dist/doctor/rules/orphan-frontmatter-id.js.map +1 -0
  69. package/dist/doctor/rules/schema-rejected.d.ts +20 -0
  70. package/dist/doctor/rules/schema-rejected.d.ts.map +1 -0
  71. package/dist/doctor/rules/schema-rejected.js +44 -0
  72. package/dist/doctor/rules/schema-rejected.js.map +1 -0
  73. package/dist/doctor/rules/slug-collision.d.ts +18 -0
  74. package/dist/doctor/rules/slug-collision.d.ts.map +1 -0
  75. package/dist/doctor/rules/slug-collision.js +65 -0
  76. package/dist/doctor/rules/slug-collision.js.map +1 -0
  77. package/dist/doctor/rules/workflow-stale.d.ts +20 -0
  78. package/dist/doctor/rules/workflow-stale.d.ts.map +1 -0
  79. package/dist/doctor/rules/workflow-stale.js +136 -0
  80. package/dist/doctor/rules/workflow-stale.js.map +1 -0
  81. package/dist/doctor/runner.d.ts +75 -0
  82. package/dist/doctor/runner.d.ts.map +1 -0
  83. package/dist/doctor/runner.js +289 -0
  84. package/dist/doctor/runner.js.map +1 -0
  85. package/dist/doctor/schema-patch.d.ts +21 -0
  86. package/dist/doctor/schema-patch.d.ts.map +1 -0
  87. package/dist/doctor/schema-patch.js +92 -0
  88. package/dist/doctor/schema-patch.js.map +1 -0
  89. package/dist/doctor/types.d.ts +185 -0
  90. package/dist/doctor/types.d.ts.map +1 -0
  91. package/dist/doctor/types.js +13 -0
  92. package/dist/doctor/types.js.map +1 -0
  93. package/dist/frontmatter.d.ts +103 -0
  94. package/dist/frontmatter.d.ts.map +1 -0
  95. package/dist/frontmatter.js +306 -0
  96. package/dist/frontmatter.js.map +1 -0
  97. package/dist/index.d.ts +27 -0
  98. package/dist/index.d.ts.map +1 -0
  99. package/dist/index.js +27 -0
  100. package/dist/index.js.map +1 -0
  101. package/dist/ingest-derive.d.ts +79 -0
  102. package/dist/ingest-derive.d.ts.map +1 -0
  103. package/dist/ingest-derive.js +299 -0
  104. package/dist/ingest-derive.js.map +1 -0
  105. package/dist/ingest-paths.d.ts +37 -0
  106. package/dist/ingest-paths.d.ts.map +1 -0
  107. package/dist/ingest-paths.js +176 -0
  108. package/dist/ingest-paths.js.map +1 -0
  109. package/dist/ingest.d.ts +162 -0
  110. package/dist/ingest.d.ts.map +1 -0
  111. package/dist/ingest.js +269 -0
  112. package/dist/ingest.js.map +1 -0
  113. package/dist/journal.d.ts +49 -0
  114. package/dist/journal.d.ts.map +1 -0
  115. package/dist/journal.js +113 -0
  116. package/dist/journal.js.map +1 -0
  117. package/dist/outline-split.d.ts +38 -0
  118. package/dist/outline-split.d.ts.map +1 -0
  119. package/dist/outline-split.js +84 -0
  120. package/dist/outline-split.js.map +1 -0
  121. package/dist/overrides.d.ts +83 -0
  122. package/dist/overrides.d.ts.map +1 -0
  123. package/dist/overrides.js +88 -0
  124. package/dist/overrides.js.map +1 -0
  125. package/dist/paths.d.ts +183 -0
  126. package/dist/paths.d.ts.map +1 -0
  127. package/dist/paths.js +266 -0
  128. package/dist/paths.js.map +1 -0
  129. package/dist/remark-image-figure.mjs +77 -0
  130. package/dist/remark-strip-first-h1.mjs +26 -0
  131. package/dist/remark-strip-outline.mjs +44 -0
  132. package/dist/rename-slug.d.ts +49 -0
  133. package/dist/rename-slug.d.ts.map +1 -0
  134. package/dist/rename-slug.js +161 -0
  135. package/dist/rename-slug.js.map +1 -0
  136. package/dist/review/handlers.d.ts +55 -0
  137. package/dist/review/handlers.d.ts.map +1 -0
  138. package/dist/review/handlers.js +307 -0
  139. package/dist/review/handlers.js.map +1 -0
  140. package/dist/review/index.d.ts +14 -0
  141. package/dist/review/index.d.ts.map +1 -0
  142. package/dist/review/index.js +13 -0
  143. package/dist/review/index.js.map +1 -0
  144. package/dist/review/journal-mappers.d.ts +35 -0
  145. package/dist/review/journal-mappers.d.ts.map +1 -0
  146. package/dist/review/journal-mappers.js +48 -0
  147. package/dist/review/journal-mappers.js.map +1 -0
  148. package/dist/review/pipeline.d.ts +79 -0
  149. package/dist/review/pipeline.d.ts.map +1 -0
  150. package/dist/review/pipeline.js +234 -0
  151. package/dist/review/pipeline.js.map +1 -0
  152. package/dist/review/render.d.ts +27 -0
  153. package/dist/review/render.d.ts.map +1 -0
  154. package/dist/review/render.js +42 -0
  155. package/dist/review/render.js.map +1 -0
  156. package/dist/review/report.d.ts +50 -0
  157. package/dist/review/report.d.ts.map +1 -0
  158. package/dist/review/report.js +164 -0
  159. package/dist/review/report.js.map +1 -0
  160. package/dist/review/result.d.ts +12 -0
  161. package/dist/review/result.d.ts.map +1 -0
  162. package/dist/review/result.js +12 -0
  163. package/dist/review/result.js.map +1 -0
  164. package/dist/review/start-handlers.d.ts +62 -0
  165. package/dist/review/start-handlers.d.ts.map +1 -0
  166. package/dist/review/start-handlers.js +223 -0
  167. package/dist/review/start-handlers.js.map +1 -0
  168. package/dist/review/types.d.ts +169 -0
  169. package/dist/review/types.d.ts.map +1 -0
  170. package/dist/review/types.js +26 -0
  171. package/dist/review/types.js.map +1 -0
  172. package/dist/review/workflow-paths.d.ts +68 -0
  173. package/dist/review/workflow-paths.d.ts.map +1 -0
  174. package/dist/review/workflow-paths.js +112 -0
  175. package/dist/review/workflow-paths.js.map +1 -0
  176. package/dist/scaffold.d.ts +67 -0
  177. package/dist/scaffold.d.ts.map +1 -0
  178. package/dist/scaffold.js +122 -0
  179. package/dist/scaffold.js.map +1 -0
  180. package/dist/scrapbook.d.ts +229 -0
  181. package/dist/scrapbook.d.ts.map +1 -0
  182. package/dist/scrapbook.js +500 -0
  183. package/dist/scrapbook.js.map +1 -0
  184. package/dist/types.d.ts +197 -0
  185. package/dist/types.d.ts.map +1 -0
  186. package/dist/types.js +120 -0
  187. package/dist/types.js.map +1 -0
  188. package/package.json +160 -0
@@ -0,0 +1,162 @@
1
+ /**
2
+ * ingest.ts — discovery primitive for backfilling existing markdown
3
+ * content into the editorial calendar.
4
+ *
5
+ * Turns "files on disk" into "calendar candidates" without touching
6
+ * the calendar itself. The CLI layer wires this output into a
7
+ * dry-run plan or an `--apply` write; this module is purely
8
+ * descriptive.
9
+ *
10
+ * Responsibilities split across three files:
11
+ *
12
+ * - `ingest.ts` (this file) — orchestrates discovery, applies
13
+ * idempotency filtering against an existing calendar, and shapes
14
+ * candidates into `IngestCandidate` records ready for the apply
15
+ * layer.
16
+ * - `ingest-paths.ts` — walks file / directory / glob inputs and
17
+ * produces `(filePath, root)` tuples.
18
+ * - `ingest-derive.ts` — slug / state / date / title derivation
19
+ * with provenance recording.
20
+ *
21
+ * What's intentionally out of scope:
22
+ *
23
+ * - Mutations: `discoverIngestCandidates` never writes to disk,
24
+ * never mutates the calendar.
25
+ * - Auto-detection of the content tree: the operator passes paths
26
+ * explicitly. Walking the entire repo would scoop up
27
+ * node_modules / vendored docs / unrelated markdown.
28
+ * - Migrations from other calendar formats: source is markdown
29
+ * files + their frontmatter. Importing from Notion / Airtable
30
+ * is a separate concern (PRD extension).
31
+ */
32
+ import { type FrontmatterData } from './frontmatter.ts';
33
+ import type { CalendarEntry, EditorialCalendar, Stage } from './types.ts';
34
+ import { type DerivationSource } from './ingest-derive.ts';
35
+ export type { DerivationSource } from './ingest-derive.ts';
36
+ /** A markdown file resolved to a (slug, state, date) triple ready to commit. */
37
+ export interface IngestCandidate {
38
+ /** Absolute path to the source markdown file. */
39
+ filePath: string;
40
+ /** Path relative to the project root, when one was supplied. Display-only. */
41
+ relativePath: string;
42
+ /** Parsed YAML frontmatter (empty object when absent). */
43
+ frontmatter: FrontmatterData;
44
+ /** Body of the markdown file (everything after the closing `---`). */
45
+ body: string;
46
+ /** Derived slug; honours `--slug-from`, `--slug`, frontmatter, then path. */
47
+ derivedSlug: string;
48
+ /** Where `derivedSlug` came from. */
49
+ slugSource: DerivationSource;
50
+ /**
51
+ * Derived stage. `null` when the source produced an unrecognized
52
+ * state value — the operator must pass `--state` to commit.
53
+ * Surfaces in the plan as `state: ambiguous`.
54
+ */
55
+ derivedState: Stage | null;
56
+ /** Where `derivedState` came from. */
57
+ stateSource: DerivationSource;
58
+ /**
59
+ * Raw state string the source produced (e.g. `'published-elsewhere'`)
60
+ * when normalization failed. `undefined` when state was derived
61
+ * unambiguously.
62
+ */
63
+ rawState?: string;
64
+ /** Derived date in ISO YYYY-MM-DD form. */
65
+ derivedDate: string;
66
+ /** Where `derivedDate` came from. */
67
+ dateSource: DerivationSource;
68
+ /**
69
+ * Title pulled from frontmatter — falls back to a humanized slug
70
+ * when absent. The CLI emits this onto the new calendar row.
71
+ */
72
+ title: string;
73
+ /** Description pulled from frontmatter — empty string when absent. */
74
+ description: string;
75
+ }
76
+ /** A candidate that won't be added; carries a reason the operator can act on. */
77
+ export interface IngestSkip {
78
+ filePath: string;
79
+ relativePath: string;
80
+ /** Slug we would have used; `undefined` when we couldn't derive one. */
81
+ slug?: string;
82
+ reason: string;
83
+ }
84
+ /** Result of a discovery pass — successes and skips, never mutations. */
85
+ export interface IngestDiscoveryResult {
86
+ candidates: IngestCandidate[];
87
+ skips: IngestSkip[];
88
+ }
89
+ export type SlugFrom = 'frontmatter' | 'path';
90
+ export type StateFrom = 'frontmatter' | 'datePublished';
91
+ export interface IngestOptions {
92
+ /**
93
+ * Project root for relative-path display + scrapbook detection.
94
+ * Required; the discovery surface always runs in the context of a
95
+ * deskwork-installed project.
96
+ */
97
+ projectRoot: string;
98
+ /** Where to derive slugs from. Default `'path'`. */
99
+ slugFrom?: SlugFrom;
100
+ /** Where to derive states from. Default `'frontmatter'`. */
101
+ stateFrom?: StateFrom;
102
+ /**
103
+ * Explicit slug. Only honored when discovery resolves to exactly
104
+ * one candidate file — the CLI enforces this before calling.
105
+ */
106
+ explicitSlug?: string;
107
+ /** Explicit stage. Wins over derivation when set. */
108
+ explicitState?: Stage;
109
+ /** Explicit ISO date (YYYY-MM-DD). Wins over derivation when set. */
110
+ explicitDate?: string;
111
+ /** Frontmatter field-name overrides — match the operator's project schema. */
112
+ fieldNames?: {
113
+ title?: string;
114
+ description?: string;
115
+ slug?: string;
116
+ state?: string;
117
+ date?: string;
118
+ };
119
+ /**
120
+ * Existing calendar to filter against for idempotency. When omitted,
121
+ * no idempotency check runs (every candidate proceeds). The CLI
122
+ * always supplies this; tests omit it to exercise discovery alone.
123
+ */
124
+ calendar?: EditorialCalendar;
125
+ /**
126
+ * Bypass the duplicate-slug skip. The CLI exposes this as `--force`
127
+ * and warns the operator that existing rows will be left as-is —
128
+ * `discoverIngestCandidates` does not mutate, so "force" simply
129
+ * means "don't skip; pass through and let the apply layer decide".
130
+ */
131
+ force?: boolean;
132
+ /**
133
+ * Skip files under any of these absolute paths — host projects use
134
+ * a scrapbook directory for sketches that aren't on the editorial
135
+ * calendar. The CLI threads `<contentDir>/scrapbook` (one per site)
136
+ * into this list. Default `[]` (no skipping).
137
+ */
138
+ scrapbookRoots?: string[];
139
+ /** Today's date for date-derivation fallback. Test seam; defaults to now(). */
140
+ now?: Date;
141
+ }
142
+ /**
143
+ * Walk the supplied paths, parse markdown files, and produce a
144
+ * candidate list ready to feed to the apply layer.
145
+ *
146
+ * `paths` accepts:
147
+ * - a single markdown file (`.md`, `.mdx`, `.markdown`)
148
+ * - a directory walked recursively
149
+ * - a glob (any path containing `*` or `?`)
150
+ *
151
+ * Errors during parse (bad frontmatter, unreadable file) surface as
152
+ * `IngestSkip` records with a descriptive reason — discovery never
153
+ * throws on a per-file problem so a 100-file run isn't aborted by
154
+ * one corrupt source.
155
+ */
156
+ export declare function discoverIngestCandidates(paths: string[], options: IngestOptions): IngestDiscoveryResult;
157
+ /**
158
+ * Build the CalendarEntry that the apply layer will append for a
159
+ * given candidate. Pure shaping — does not touch the calendar.
160
+ */
161
+ export declare function candidateToEntry(candidate: IngestCandidate, stage: Stage): Omit<CalendarEntry, 'id'>;
162
+ //# sourceMappingURL=ingest.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ingest.d.ts","sourceRoot":"","sources":["../src/ingest.ts"],"names":[],"mappings":"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"}
package/dist/ingest.js ADDED
@@ -0,0 +1,269 @@
1
+ /**
2
+ * ingest.ts — discovery primitive for backfilling existing markdown
3
+ * content into the editorial calendar.
4
+ *
5
+ * Turns "files on disk" into "calendar candidates" without touching
6
+ * the calendar itself. The CLI layer wires this output into a
7
+ * dry-run plan or an `--apply` write; this module is purely
8
+ * descriptive.
9
+ *
10
+ * Responsibilities split across three files:
11
+ *
12
+ * - `ingest.ts` (this file) — orchestrates discovery, applies
13
+ * idempotency filtering against an existing calendar, and shapes
14
+ * candidates into `IngestCandidate` records ready for the apply
15
+ * layer.
16
+ * - `ingest-paths.ts` — walks file / directory / glob inputs and
17
+ * produces `(filePath, root)` tuples.
18
+ * - `ingest-derive.ts` — slug / state / date / title derivation
19
+ * with provenance recording.
20
+ *
21
+ * What's intentionally out of scope:
22
+ *
23
+ * - Mutations: `discoverIngestCandidates` never writes to disk,
24
+ * never mutates the calendar.
25
+ * - Auto-detection of the content tree: the operator passes paths
26
+ * explicitly. Walking the entire repo would scoop up
27
+ * node_modules / vendored docs / unrelated markdown.
28
+ * - Migrations from other calendar formats: source is markdown
29
+ * files + their frontmatter. Importing from Notion / Airtable
30
+ * is a separate concern (PRD extension).
31
+ */
32
+ import { basename, isAbsolute, relative, sep } from 'node:path';
33
+ import { readFileSync } from 'node:fs';
34
+ import { parseFrontmatter } from "./frontmatter.js";
35
+ import { collectMarkdownFiles } from "./ingest-paths.js";
36
+ import { deriveDate, deriveDescription, deriveSlug, deriveState, deriveTitle, } from "./ingest-derive.js";
37
+ // ---------------------------------------------------------------------------
38
+ // Constants
39
+ // ---------------------------------------------------------------------------
40
+ const SLUG_RE = /^[a-z0-9][a-z0-9-]*(\/[a-z0-9][a-z0-9-]*)*$/;
41
+ const DEFAULT_FIELDS = {
42
+ title: 'title',
43
+ description: 'description',
44
+ slug: 'slug',
45
+ state: 'state',
46
+ date: 'datePublished',
47
+ };
48
+ // ---------------------------------------------------------------------------
49
+ // Public API
50
+ // ---------------------------------------------------------------------------
51
+ /**
52
+ * Walk the supplied paths, parse markdown files, and produce a
53
+ * candidate list ready to feed to the apply layer.
54
+ *
55
+ * `paths` accepts:
56
+ * - a single markdown file (`.md`, `.mdx`, `.markdown`)
57
+ * - a directory walked recursively
58
+ * - a glob (any path containing `*` or `?`)
59
+ *
60
+ * Errors during parse (bad frontmatter, unreadable file) surface as
61
+ * `IngestSkip` records with a descriptive reason — discovery never
62
+ * throws on a per-file problem so a 100-file run isn't aborted by
63
+ * one corrupt source.
64
+ */
65
+ export function discoverIngestCandidates(paths, options) {
66
+ if (paths.length === 0) {
67
+ throw new Error('discoverIngestCandidates: at least one path is required');
68
+ }
69
+ if (!options.projectRoot || !isAbsolute(options.projectRoot)) {
70
+ throw new Error(`discoverIngestCandidates: projectRoot must be an absolute path (got "${options.projectRoot ?? ''}")`);
71
+ }
72
+ const collected = collectMarkdownFiles(paths);
73
+ if (options.explicitSlug !== undefined && collected.length !== 1) {
74
+ throw new Error(`--slug requires exactly one matched file; ${collected.length} matched`);
75
+ }
76
+ const candidates = [];
77
+ const skips = [];
78
+ const fields = { ...DEFAULT_FIELDS, ...(options.fieldNames ?? {}) };
79
+ for (const { filePath, root } of collected) {
80
+ const relPath = relativeTo(options.projectRoot, filePath);
81
+ if (isUnderScrapbook(filePath, options.scrapbookRoots)) {
82
+ skips.push({
83
+ filePath,
84
+ relativePath: relPath,
85
+ reason: 'file is under scrapbook/ (skipped by default)',
86
+ });
87
+ continue;
88
+ }
89
+ let raw;
90
+ try {
91
+ raw = readFileSync(filePath, 'utf-8');
92
+ }
93
+ catch (err) {
94
+ skips.push({
95
+ filePath,
96
+ relativePath: relPath,
97
+ reason: `unreadable: ${err instanceof Error ? err.message : String(err)}`,
98
+ });
99
+ continue;
100
+ }
101
+ let parsed;
102
+ try {
103
+ parsed = parseFrontmatter(raw);
104
+ }
105
+ catch (err) {
106
+ skips.push({
107
+ filePath,
108
+ relativePath: relPath,
109
+ reason: `frontmatter parse failed: ${err instanceof Error ? err.message : String(err)}`,
110
+ });
111
+ continue;
112
+ }
113
+ // README.md (case-insensitive) without frontmatter is treated as
114
+ // organizational content describing a folder's purpose — NOT a
115
+ // calendar entry. See #23. README.md WITH frontmatter is still
116
+ // ingested (Phase 13's `--layout readme` produces these).
117
+ if (isReadmeBasename(filePath) &&
118
+ Object.keys(parsed.data).length === 0) {
119
+ skips.push({
120
+ filePath,
121
+ relativePath: relPath,
122
+ reason: 'README.md without frontmatter (organizational, not pipeline)',
123
+ });
124
+ continue;
125
+ }
126
+ const slug = deriveSlug({
127
+ filePath,
128
+ root,
129
+ frontmatter: parsed.data,
130
+ fieldName: fields.slug,
131
+ slugFrom: options.slugFrom ?? 'path',
132
+ ...(options.explicitSlug !== undefined
133
+ ? { explicitSlug: options.explicitSlug }
134
+ : {}),
135
+ });
136
+ if (!slug.value) {
137
+ skips.push({
138
+ filePath,
139
+ relativePath: relPath,
140
+ reason: slug.reason ?? 'could not derive slug',
141
+ });
142
+ continue;
143
+ }
144
+ if (!SLUG_RE.test(slug.value)) {
145
+ skips.push({
146
+ filePath,
147
+ relativePath: relPath,
148
+ slug: slug.value,
149
+ reason: `derived slug "${slug.value}" is not valid kebab-case ` +
150
+ `(must match [a-z0-9][a-z0-9-]* segments separated by '/')`,
151
+ });
152
+ continue;
153
+ }
154
+ if (options.calendar &&
155
+ !options.force &&
156
+ options.calendar.entries.some((e) => e.slug === slug.value)) {
157
+ skips.push({
158
+ filePath,
159
+ relativePath: relPath,
160
+ slug: slug.value,
161
+ reason: `calendar already has an entry with slug "${slug.value}" (use --force to override)`,
162
+ });
163
+ continue;
164
+ }
165
+ const state = deriveState({
166
+ frontmatter: parsed.data,
167
+ stateField: fields.state,
168
+ dateField: fields.date,
169
+ stateFrom: options.stateFrom ?? 'frontmatter',
170
+ ...(options.explicitState !== undefined
171
+ ? { explicitState: options.explicitState }
172
+ : {}),
173
+ ...(options.now !== undefined ? { now: options.now } : {}),
174
+ });
175
+ const date = deriveDate({
176
+ filePath,
177
+ frontmatter: parsed.data,
178
+ dateField: fields.date,
179
+ ...(options.explicitDate !== undefined
180
+ ? { explicitDate: options.explicitDate }
181
+ : {}),
182
+ ...(options.now !== undefined ? { now: options.now } : {}),
183
+ });
184
+ const title = deriveTitle(parsed.data, fields.title, slug.value);
185
+ const description = deriveDescription(parsed.data, fields.description);
186
+ candidates.push({
187
+ filePath,
188
+ relativePath: relPath,
189
+ frontmatter: parsed.data,
190
+ body: parsed.body,
191
+ derivedSlug: slug.value,
192
+ slugSource: slug.source,
193
+ derivedState: state.value,
194
+ stateSource: state.source,
195
+ ...(state.rawValue !== undefined ? { rawState: state.rawValue } : {}),
196
+ derivedDate: date.value,
197
+ dateSource: date.source,
198
+ title,
199
+ description,
200
+ });
201
+ }
202
+ return { candidates, skips };
203
+ }
204
+ /**
205
+ * Build the CalendarEntry that the apply layer will append for a
206
+ * given candidate. Pure shaping — does not touch the calendar.
207
+ */
208
+ export function candidateToEntry(candidate, stage) {
209
+ const entry = {
210
+ slug: candidate.derivedSlug,
211
+ title: candidate.title,
212
+ description: candidate.description,
213
+ stage,
214
+ targetKeywords: [],
215
+ source: 'manual',
216
+ };
217
+ // Published entries carry datePublished; other lanes don't (the
218
+ // calendar renderer only emits the column for Published).
219
+ if (stage === 'Published') {
220
+ entry.datePublished = candidate.derivedDate;
221
+ }
222
+ return entry;
223
+ }
224
+ // ---------------------------------------------------------------------------
225
+ // Helpers
226
+ // ---------------------------------------------------------------------------
227
+ /**
228
+ * True when the file's basename is `README.<ext>` (case-insensitive)
229
+ * for any of our supported markdown extensions. The frontmatter check
230
+ * lives at the call site — this helper is purely about the filename.
231
+ */
232
+ function isReadmeBasename(filePath) {
233
+ const lower = basename(filePath).toLowerCase();
234
+ return (lower === 'readme.md' ||
235
+ lower === 'readme.mdx' ||
236
+ lower === 'readme.markdown');
237
+ }
238
+ function relativeTo(projectRoot, filePath) {
239
+ const rel = relative(projectRoot, filePath);
240
+ return rel.length > 0 ? rel : filePath;
241
+ }
242
+ function isUnderScrapbook(filePath, roots) {
243
+ // Path-segment match: any `scrapbook` directory segment anywhere in
244
+ // the path counts. The configured `roots` list still works (its
245
+ // entries are absolute prefixes) but is no longer required — a
246
+ // hierarchical content tree can have `scrapbook/` dirs at any depth
247
+ // (e.g. `the-outbound/scrapbook/` AND `the-outbound/characters/scrapbook/`)
248
+ // and every one of them must be skipped to avoid duplicate calendar
249
+ // rows from same-named files at different depths (issue #20).
250
+ //
251
+ // We split on the platform separator and test for an exact `scrapbook`
252
+ // segment so a directory literally named `scrapbookery/` does NOT
253
+ // false-positive.
254
+ const segments = filePath.split(sep);
255
+ if (segments.includes('scrapbook'))
256
+ return true;
257
+ // Honor any explicit roots passed in too — preserves the original
258
+ // contract for callers that pass absolute paths (the CLI threads the
259
+ // configured `<contentDir>/scrapbook` per site here).
260
+ if (!roots || roots.length === 0)
261
+ return false;
262
+ for (const root of roots) {
263
+ const r = root.endsWith(sep) ? root : root + sep;
264
+ if (filePath.startsWith(r))
265
+ return true;
266
+ }
267
+ return false;
268
+ }
269
+ //# sourceMappingURL=ingest.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ingest.js","sourceRoot":"","sources":["../src/ingest.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,49 @@
1
+ /**
2
+ * journal.ts — directory-backed append-only record store.
3
+ *
4
+ * Each record lives in its own file under `dir/` named
5
+ * `<normalized-timestamp>-<id>.json`. Timestamp normalization strips
6
+ * filesystem-hostile characters (`:` and `.`) from ISO 8601 so the name
7
+ * sorts chronologically by simple string comparison and stays portable.
8
+ *
9
+ * Why not a single JSONL? Monolithic append logs produce merge conflicts
10
+ * when two branches each add entries to the tail — git sees overlapping
11
+ * line ranges. One file per entry means writes never collide by
12
+ * construction. Ported from audiocontrol.org's scripts/lib/journal/.
13
+ */
14
+ /**
15
+ * Normalize an ISO 8601 timestamp for use in a filename. Colons and dots
16
+ * become dashes so the result is portable across filesystems and still
17
+ * sorts chronologically by string comparison.
18
+ */
19
+ export declare function normalizeTimestamp(iso: string): string;
20
+ export interface ReadJournalOptions {
21
+ /**
22
+ * Field on each record that carries the chronological key. Records are
23
+ * sorted ascending by this field's value. Defaults to `timestamp`;
24
+ * workflow items use `createdAt`.
25
+ */
26
+ timestampField?: string;
27
+ }
28
+ /** Read every record in a journal directory, oldest first. */
29
+ export declare function readJournal<T>(dir: string, options?: ReadJournalOptions): T[];
30
+ export interface AppendJournalOptions {
31
+ /** Field that holds the unique id on each record. Defaults to `id`. */
32
+ idField?: string;
33
+ /** Field that holds the timestamp on each record. Defaults to `timestamp`. */
34
+ timestampField?: string;
35
+ }
36
+ /**
37
+ * Write a record to its own file under `dir`. The filename is derived from
38
+ * the record's timestamp + id. If a file for this id already exists, it's
39
+ * overwritten (so a caller that regenerates an entry stays idempotent).
40
+ */
41
+ export declare function appendJournal<T>(dir: string, record: T, options?: AppendJournalOptions): void;
42
+ /**
43
+ * Merge `patch` into the record identified by `id`. Returns the updated
44
+ * record, or `null` if no record matches. Touches exactly one file.
45
+ */
46
+ export declare function updateJournal<T>(dir: string, id: string, patch: Partial<T>): T | null;
47
+ /** Remove the file for a given id. Returns true if a file was deleted. */
48
+ export declare function deleteJournal(dir: string, id: string): boolean;
49
+ //# sourceMappingURL=journal.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"journal.d.ts","sourceRoot":"","sources":["../src/journal.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAYH;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAEtD;AA4BD,MAAM,WAAW,kBAAkB;IACjC;;;;OAIG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,8DAA8D;AAC9D,wBAAgB,WAAW,CAAC,CAAC,EAC3B,GAAG,EAAE,MAAM,EACX,OAAO,GAAE,kBAAuB,GAC/B,CAAC,EAAE,CAgBL;AAED,MAAM,WAAW,oBAAoB;IACnC,uEAAuE;IACvE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,8EAA8E;IAC9E,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,CAAC,EAC7B,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,CAAC,EACT,OAAO,GAAE,oBAAyB,GACjC,IAAI,CAWN;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,CAAC,EAC7B,GAAG,EAAE,MAAM,EACX,EAAE,EAAE,MAAM,EACV,KAAK,EAAE,OAAO,CAAC,CAAC,CAAC,GAChB,CAAC,GAAG,IAAI,CAQV;AAED,0EAA0E;AAC1E,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAK9D"}
@@ -0,0 +1,113 @@
1
+ /**
2
+ * journal.ts — directory-backed append-only record store.
3
+ *
4
+ * Each record lives in its own file under `dir/` named
5
+ * `<normalized-timestamp>-<id>.json`. Timestamp normalization strips
6
+ * filesystem-hostile characters (`:` and `.`) from ISO 8601 so the name
7
+ * sorts chronologically by simple string comparison and stays portable.
8
+ *
9
+ * Why not a single JSONL? Monolithic append logs produce merge conflicts
10
+ * when two branches each add entries to the tail — git sees overlapping
11
+ * line ranges. One file per entry means writes never collide by
12
+ * construction. Ported from audiocontrol.org's scripts/lib/journal/.
13
+ */
14
+ import { existsSync, mkdirSync, readFileSync, readdirSync, unlinkSync, writeFileSync, } from 'node:fs';
15
+ import { join } from 'node:path';
16
+ /**
17
+ * Normalize an ISO 8601 timestamp for use in a filename. Colons and dots
18
+ * become dashes so the result is portable across filesystems and still
19
+ * sorts chronologically by string comparison.
20
+ */
21
+ export function normalizeTimestamp(iso) {
22
+ return iso.replace(/[:.]/g, '-');
23
+ }
24
+ function ensureDir(dir) {
25
+ if (!existsSync(dir))
26
+ mkdirSync(dir, { recursive: true });
27
+ }
28
+ function recordFilename(timestamp, id) {
29
+ return `${normalizeTimestamp(timestamp)}-${id}.json`;
30
+ }
31
+ function findFileById(dir, id) {
32
+ if (!existsSync(dir))
33
+ return null;
34
+ const suffix = `-${id}.json`;
35
+ for (const name of readdirSync(dir)) {
36
+ if (name.endsWith(suffix))
37
+ return join(dir, name);
38
+ }
39
+ return null;
40
+ }
41
+ function readFile(path) {
42
+ try {
43
+ const text = readFileSync(path, 'utf-8');
44
+ return JSON.parse(text);
45
+ }
46
+ catch {
47
+ return null;
48
+ }
49
+ }
50
+ /** Read every record in a journal directory, oldest first. */
51
+ export function readJournal(dir, options = {}) {
52
+ if (!existsSync(dir))
53
+ return [];
54
+ const timestampField = options.timestampField ?? 'timestamp';
55
+ const records = [];
56
+ for (const name of readdirSync(dir)) {
57
+ if (!name.endsWith('.json'))
58
+ continue;
59
+ const record = readFile(join(dir, name));
60
+ if (record === null)
61
+ continue;
62
+ records.push(record);
63
+ }
64
+ records.sort((a, b) => {
65
+ const aKey = String(a[timestampField] ?? '');
66
+ const bKey = String(b[timestampField] ?? '');
67
+ return aKey.localeCompare(bKey);
68
+ });
69
+ return records;
70
+ }
71
+ /**
72
+ * Write a record to its own file under `dir`. The filename is derived from
73
+ * the record's timestamp + id. If a file for this id already exists, it's
74
+ * overwritten (so a caller that regenerates an entry stays idempotent).
75
+ */
76
+ export function appendJournal(dir, record, options = {}) {
77
+ ensureDir(dir);
78
+ const idField = options.idField ?? 'id';
79
+ const timestampField = options.timestampField ?? 'timestamp';
80
+ const id = String(record[idField] ?? '');
81
+ const timestamp = String(record[timestampField] ?? '');
82
+ if (!id)
83
+ throw new Error(`appendJournal: record has no \`${idField}\` field`);
84
+ if (!timestamp)
85
+ throw new Error(`appendJournal: record has no \`${timestampField}\` field`);
86
+ const existing = findFileById(dir, id);
87
+ const target = existing ?? join(dir, recordFilename(timestamp, id));
88
+ writeFileSync(target, JSON.stringify(record, null, 2) + '\n', 'utf-8');
89
+ }
90
+ /**
91
+ * Merge `patch` into the record identified by `id`. Returns the updated
92
+ * record, or `null` if no record matches. Touches exactly one file.
93
+ */
94
+ export function updateJournal(dir, id, patch) {
95
+ const path = findFileById(dir, id);
96
+ if (!path)
97
+ return null;
98
+ const current = readFile(path);
99
+ if (current === null)
100
+ return null;
101
+ const merged = { ...current, ...patch };
102
+ writeFileSync(path, JSON.stringify(merged, null, 2) + '\n', 'utf-8');
103
+ return merged;
104
+ }
105
+ /** Remove the file for a given id. Returns true if a file was deleted. */
106
+ export function deleteJournal(dir, id) {
107
+ const path = findFileById(dir, id);
108
+ if (!path)
109
+ return false;
110
+ unlinkSync(path);
111
+ return true;
112
+ }
113
+ //# sourceMappingURL=journal.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"journal.js","sourceRoot":"","sources":["../src/journal.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,38 @@
1
+ export interface SplitOutline {
2
+ /** The full outline section including the heading, or empty. */
3
+ outline: string;
4
+ /** Everything else in the document (frontmatter + title + body). */
5
+ body: string;
6
+ /** True if the document has an outline section to split. */
7
+ present: boolean;
8
+ /** Line index where the outline started (for structural rejoin). */
9
+ startLine: number;
10
+ /** Line index where the outline ended (exclusive). */
11
+ endLine: number;
12
+ }
13
+ /**
14
+ * Detect and separate the `## Outline` section. The section runs
15
+ * from its `## Outline` heading through the next `## ` heading
16
+ * (non-inclusive) or end of file. Anything before the outline is
17
+ * prepended to `body`; anything after is appended. Rejoin via
18
+ * `joinOutline`.
19
+ *
20
+ * This mirrors the line-wise logic in `scripts/lib/editorial/
21
+ * body-state.ts` (kept separate so the browser-side bundle
22
+ * doesn't have to drag in the server's fs/path imports).
23
+ */
24
+ export declare function splitOutline(md: string): SplitOutline;
25
+ /**
26
+ * Rejoin an outline section with body content. If `outline` is
27
+ * empty, returns `body` unchanged. Otherwise splices the outline
28
+ * back at the same structural position it was extracted from —
29
+ * which for the scaffold shape (frontmatter + H1 + outline + body
30
+ * sections) is right after the H1.
31
+ *
32
+ * Strategy: find the first `## ` heading in `body` and insert the
33
+ * outline immediately before it, separated by a blank line on each
34
+ * side. If the body has no H2 (e.g., pre-drafting, still just
35
+ * frontmatter + H1), append outline at the end.
36
+ */
37
+ export declare function joinOutline(outline: string, body: string): string;
38
+ //# sourceMappingURL=outline-split.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"outline-split.d.ts","sourceRoot":"","sources":["../src/outline-split.ts"],"names":[],"mappings":"AAsBA,MAAM,WAAW,YAAY;IAC3B,gEAAgE;IAChE,OAAO,EAAE,MAAM,CAAC;IAChB,oEAAoE;IACpE,IAAI,EAAE,MAAM,CAAC;IACb,4DAA4D;IAC5D,OAAO,EAAE,OAAO,CAAC;IACjB,oEAAoE;IACpE,SAAS,EAAE,MAAM,CAAC;IAClB,sDAAsD;IACtD,OAAO,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,YAAY,CAAC,EAAE,EAAE,MAAM,GAAG,YAAY,CAgBrD;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAoBjE"}