@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.
- package/dist/body-state.d.ts +27 -0
- package/dist/body-state.d.ts.map +1 -0
- package/dist/body-state.js +62 -0
- package/dist/body-state.js.map +1 -0
- package/dist/calendar-mutations.d.ts +124 -0
- package/dist/calendar-mutations.d.ts.map +1 -0
- package/dist/calendar-mutations.js +305 -0
- package/dist/calendar-mutations.js.map +1 -0
- package/dist/calendar.d.ts +54 -0
- package/dist/calendar.d.ts.map +1 -0
- package/dist/calendar.js +430 -0
- package/dist/calendar.js.map +1 -0
- package/dist/cli.d.ts +38 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +72 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.d.ts +91 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +216 -0
- package/dist/config.js.map +1 -0
- package/dist/content-index.d.ts +74 -0
- package/dist/content-index.d.ts.map +1 -0
- package/dist/content-index.js +205 -0
- package/dist/content-index.js.map +1 -0
- package/dist/content-tree-fs-walk.d.ts +54 -0
- package/dist/content-tree-fs-walk.d.ts.map +1 -0
- package/dist/content-tree-fs-walk.js +112 -0
- package/dist/content-tree-fs-walk.js.map +1 -0
- package/dist/content-tree-helpers.d.ts +52 -0
- package/dist/content-tree-helpers.d.ts.map +1 -0
- package/dist/content-tree-helpers.js +116 -0
- package/dist/content-tree-helpers.js.map +1 -0
- package/dist/content-tree-types.d.ts +175 -0
- package/dist/content-tree-types.d.ts.map +1 -0
- package/dist/content-tree-types.js +10 -0
- package/dist/content-tree-types.js.map +1 -0
- package/dist/content-tree.d.ts +93 -0
- package/dist/content-tree.d.ts.map +1 -0
- package/dist/content-tree.js +385 -0
- package/dist/content-tree.js.map +1 -0
- package/dist/doctor/index.d.ts +11 -0
- package/dist/doctor/index.d.ts.map +1 -0
- package/dist/doctor/index.js +10 -0
- package/dist/doctor/index.js.map +1 -0
- package/dist/doctor/project-rules.d.ts +59 -0
- package/dist/doctor/project-rules.d.ts.map +1 -0
- package/dist/doctor/project-rules.js +143 -0
- package/dist/doctor/project-rules.js.map +1 -0
- package/dist/doctor/rules/calendar-uuid-missing.d.ts +19 -0
- package/dist/doctor/rules/calendar-uuid-missing.d.ts.map +1 -0
- package/dist/doctor/rules/calendar-uuid-missing.js +176 -0
- package/dist/doctor/rules/calendar-uuid-missing.js.map +1 -0
- package/dist/doctor/rules/duplicate-id.d.ts +27 -0
- package/dist/doctor/rules/duplicate-id.d.ts.map +1 -0
- package/dist/doctor/rules/duplicate-id.js +157 -0
- package/dist/doctor/rules/duplicate-id.js.map +1 -0
- package/dist/doctor/rules/legacy-top-level-id-migration.d.ts +40 -0
- package/dist/doctor/rules/legacy-top-level-id-migration.d.ts.map +1 -0
- package/dist/doctor/rules/legacy-top-level-id-migration.js +232 -0
- package/dist/doctor/rules/legacy-top-level-id-migration.js.map +1 -0
- package/dist/doctor/rules/missing-frontmatter-id.d.ts +45 -0
- package/dist/doctor/rules/missing-frontmatter-id.d.ts.map +1 -0
- package/dist/doctor/rules/missing-frontmatter-id.js +283 -0
- package/dist/doctor/rules/missing-frontmatter-id.js.map +1 -0
- package/dist/doctor/rules/orphan-frontmatter-id.d.ts +18 -0
- package/dist/doctor/rules/orphan-frontmatter-id.d.ts.map +1 -0
- package/dist/doctor/rules/orphan-frontmatter-id.js +154 -0
- package/dist/doctor/rules/orphan-frontmatter-id.js.map +1 -0
- package/dist/doctor/rules/schema-rejected.d.ts +20 -0
- package/dist/doctor/rules/schema-rejected.d.ts.map +1 -0
- package/dist/doctor/rules/schema-rejected.js +44 -0
- package/dist/doctor/rules/schema-rejected.js.map +1 -0
- package/dist/doctor/rules/slug-collision.d.ts +18 -0
- package/dist/doctor/rules/slug-collision.d.ts.map +1 -0
- package/dist/doctor/rules/slug-collision.js +65 -0
- package/dist/doctor/rules/slug-collision.js.map +1 -0
- package/dist/doctor/rules/workflow-stale.d.ts +20 -0
- package/dist/doctor/rules/workflow-stale.d.ts.map +1 -0
- package/dist/doctor/rules/workflow-stale.js +136 -0
- package/dist/doctor/rules/workflow-stale.js.map +1 -0
- package/dist/doctor/runner.d.ts +75 -0
- package/dist/doctor/runner.d.ts.map +1 -0
- package/dist/doctor/runner.js +289 -0
- package/dist/doctor/runner.js.map +1 -0
- package/dist/doctor/schema-patch.d.ts +21 -0
- package/dist/doctor/schema-patch.d.ts.map +1 -0
- package/dist/doctor/schema-patch.js +92 -0
- package/dist/doctor/schema-patch.js.map +1 -0
- package/dist/doctor/types.d.ts +185 -0
- package/dist/doctor/types.d.ts.map +1 -0
- package/dist/doctor/types.js +13 -0
- package/dist/doctor/types.js.map +1 -0
- package/dist/frontmatter.d.ts +103 -0
- package/dist/frontmatter.d.ts.map +1 -0
- package/dist/frontmatter.js +306 -0
- package/dist/frontmatter.js.map +1 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +27 -0
- package/dist/index.js.map +1 -0
- package/dist/ingest-derive.d.ts +79 -0
- package/dist/ingest-derive.d.ts.map +1 -0
- package/dist/ingest-derive.js +299 -0
- package/dist/ingest-derive.js.map +1 -0
- package/dist/ingest-paths.d.ts +37 -0
- package/dist/ingest-paths.d.ts.map +1 -0
- package/dist/ingest-paths.js +176 -0
- package/dist/ingest-paths.js.map +1 -0
- package/dist/ingest.d.ts +162 -0
- package/dist/ingest.d.ts.map +1 -0
- package/dist/ingest.js +269 -0
- package/dist/ingest.js.map +1 -0
- package/dist/journal.d.ts +49 -0
- package/dist/journal.d.ts.map +1 -0
- package/dist/journal.js +113 -0
- package/dist/journal.js.map +1 -0
- package/dist/outline-split.d.ts +38 -0
- package/dist/outline-split.d.ts.map +1 -0
- package/dist/outline-split.js +84 -0
- package/dist/outline-split.js.map +1 -0
- package/dist/overrides.d.ts +83 -0
- package/dist/overrides.d.ts.map +1 -0
- package/dist/overrides.js +88 -0
- package/dist/overrides.js.map +1 -0
- package/dist/paths.d.ts +183 -0
- package/dist/paths.d.ts.map +1 -0
- package/dist/paths.js +266 -0
- package/dist/paths.js.map +1 -0
- package/dist/remark-image-figure.mjs +77 -0
- package/dist/remark-strip-first-h1.mjs +26 -0
- package/dist/remark-strip-outline.mjs +44 -0
- package/dist/rename-slug.d.ts +49 -0
- package/dist/rename-slug.d.ts.map +1 -0
- package/dist/rename-slug.js +161 -0
- package/dist/rename-slug.js.map +1 -0
- package/dist/review/handlers.d.ts +55 -0
- package/dist/review/handlers.d.ts.map +1 -0
- package/dist/review/handlers.js +307 -0
- package/dist/review/handlers.js.map +1 -0
- package/dist/review/index.d.ts +14 -0
- package/dist/review/index.d.ts.map +1 -0
- package/dist/review/index.js +13 -0
- package/dist/review/index.js.map +1 -0
- package/dist/review/journal-mappers.d.ts +35 -0
- package/dist/review/journal-mappers.d.ts.map +1 -0
- package/dist/review/journal-mappers.js +48 -0
- package/dist/review/journal-mappers.js.map +1 -0
- package/dist/review/pipeline.d.ts +79 -0
- package/dist/review/pipeline.d.ts.map +1 -0
- package/dist/review/pipeline.js +234 -0
- package/dist/review/pipeline.js.map +1 -0
- package/dist/review/render.d.ts +27 -0
- package/dist/review/render.d.ts.map +1 -0
- package/dist/review/render.js +42 -0
- package/dist/review/render.js.map +1 -0
- package/dist/review/report.d.ts +50 -0
- package/dist/review/report.d.ts.map +1 -0
- package/dist/review/report.js +164 -0
- package/dist/review/report.js.map +1 -0
- package/dist/review/result.d.ts +12 -0
- package/dist/review/result.d.ts.map +1 -0
- package/dist/review/result.js +12 -0
- package/dist/review/result.js.map +1 -0
- package/dist/review/start-handlers.d.ts +62 -0
- package/dist/review/start-handlers.d.ts.map +1 -0
- package/dist/review/start-handlers.js +223 -0
- package/dist/review/start-handlers.js.map +1 -0
- package/dist/review/types.d.ts +169 -0
- package/dist/review/types.d.ts.map +1 -0
- package/dist/review/types.js +26 -0
- package/dist/review/types.js.map +1 -0
- package/dist/review/workflow-paths.d.ts +68 -0
- package/dist/review/workflow-paths.d.ts.map +1 -0
- package/dist/review/workflow-paths.js +112 -0
- package/dist/review/workflow-paths.js.map +1 -0
- package/dist/scaffold.d.ts +67 -0
- package/dist/scaffold.d.ts.map +1 -0
- package/dist/scaffold.js +122 -0
- package/dist/scaffold.js.map +1 -0
- package/dist/scrapbook.d.ts +229 -0
- package/dist/scrapbook.d.ts.map +1 -0
- package/dist/scrapbook.js +500 -0
- package/dist/scrapbook.js.map +1 -0
- package/dist/types.d.ts +197 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +120 -0
- package/dist/types.js.map +1 -0
- package/package.json +160 -0
package/dist/ingest.d.ts
ADDED
|
@@ -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"}
|
package/dist/journal.js
ADDED
|
@@ -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"}
|