@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
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bird's-eye content-tree builder (Phase 16d, fs-inverted in v0.6.0 / #24,
|
|
3
|
+
* fs-keyed in Phase 19c / #33).
|
|
4
|
+
*
|
|
5
|
+
* Derives a tree-of-nodes representation from the host project's
|
|
6
|
+
* filesystem, with the editorial calendar overlaid as a state layer.
|
|
7
|
+
*
|
|
8
|
+
* - The **filesystem walk** is the primary structure source. Every
|
|
9
|
+
* directory under `<contentDir>/` becomes a node, keyed by its
|
|
10
|
+
* fs-relative path (e.g. `projects/the-outbound`).
|
|
11
|
+
* - The **calendar** is the state overlay. A calendar entry whose
|
|
12
|
+
* `id` matches an fs node's frontmatter `id:` (via the content
|
|
13
|
+
* index) sets the node's lane (its lifecycle stage), display
|
|
14
|
+
* title, AND the `slug` display attribute (the entry's
|
|
15
|
+
* host-rendering-engine slug, used by the studio for the
|
|
16
|
+
* "public URL: /blog/<slug>" hover hint).
|
|
17
|
+
*
|
|
18
|
+
* Read-only — never mutates the calendar or the filesystem. Callers
|
|
19
|
+
* cache the result for the lifetime of a single request unless the
|
|
20
|
+
* caller knows the underlying data has changed.
|
|
21
|
+
*
|
|
22
|
+
* Inversion rationale (#33): pre-19c the tree keyed nodes by **slug**.
|
|
23
|
+
* This worked for audiocontrol's flat layout where slug == fs path,
|
|
24
|
+
* but broke for writingcontrol where slug `the-outbound` (the
|
|
25
|
+
* Astro-derived public URL) is unrelated to the file's path
|
|
26
|
+
* (`projects/the-outbound/index.md`). The ghost-root bug came from
|
|
27
|
+
* union-by-slug producing a calendar-only tree at slug
|
|
28
|
+
* `the-outbound` plus a separate untracked tree under `projects/`
|
|
29
|
+
* that never merged.
|
|
30
|
+
*
|
|
31
|
+
* Now: tree placement is filesystem-driven. Each fs node carries the
|
|
32
|
+
* relative path (e.g. `projects/the-outbound`). Calendar entries
|
|
33
|
+
* overlay state onto fs nodes by matching the entry's `id` against
|
|
34
|
+
* the file's frontmatter `id:` via `buildContentIndex`. Slug stops
|
|
35
|
+
* being load-bearing structurally and becomes a display attribute.
|
|
36
|
+
*
|
|
37
|
+
* Legacy slug-fallback (intentional, for pre-doctor entries): when a
|
|
38
|
+
* calendar entry's id isn't found in the content index (its file
|
|
39
|
+
* hasn't been bound to frontmatter yet), the assembly looks for an
|
|
40
|
+
* fs node whose path equals the entry's slug. If found → overlay
|
|
41
|
+
* with a one-time warning hinting at `deskwork doctor`. If not found
|
|
42
|
+
* → place the entry as a ghost node (preserving today's behavior
|
|
43
|
+
* for entries with neither id-binding nor a path-shaped slug).
|
|
44
|
+
* This is NOT a "fallback for missing functionality" in the project
|
|
45
|
+
* rule's sense — it's a deliberate transitional path that the doctor
|
|
46
|
+
* command resolves operator-side.
|
|
47
|
+
*/
|
|
48
|
+
import { buildContentIndex } from "./content-index.js";
|
|
49
|
+
import { listScrapbook } from "./scrapbook.js";
|
|
50
|
+
import { resolveContentDir } from "./paths.js";
|
|
51
|
+
import { defaultFsWalk } from "./content-tree-fs-walk.js";
|
|
52
|
+
import { ancestorsOf, entryHasOwnIndex, findIdBoundPath, idBoundFile, leafOfPath, pickLatestMtime, rootSegment, } from "./content-tree-helpers.js";
|
|
53
|
+
// Re-export the fs-walk + types so external callers (tests, studio) keep
|
|
54
|
+
// importing from `content-tree.ts` as today — the split is internal.
|
|
55
|
+
export { defaultFsWalk } from "./content-tree-fs-walk.js";
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
// Legacy slug-fallback warning de-duplication
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
//
|
|
60
|
+
// `buildContentTree` is called on every studio render (the dashboard polls
|
|
61
|
+
// at ~10s intervals). Pre-doctor entries that legitimately fall through to
|
|
62
|
+
// slug-as-path matching would otherwise emit the same warning hundreds of
|
|
63
|
+
// times an hour. Track which (site, entryId|slug) pairs have already
|
|
64
|
+
// warned this process and skip subsequent warnings.
|
|
65
|
+
//
|
|
66
|
+
// The Set lives at module scope on purpose — it intentionally outlives any
|
|
67
|
+
// single request. Tests that need fresh warning behavior should call the
|
|
68
|
+
// `__resetLegacyFallbackWarnings()` helper below in `beforeEach`.
|
|
69
|
+
const WARNED_LEGACY_FALLBACK = new Set();
|
|
70
|
+
/**
|
|
71
|
+
* Test-only: clear the legacy slug-fallback warning de-dup set so tests
|
|
72
|
+
* that exercise warning behavior can do so independently. Not part of the
|
|
73
|
+
* public API — exposed only because process-level state is awkward to
|
|
74
|
+
* reset from inside tests without a hook.
|
|
75
|
+
*/
|
|
76
|
+
export function __resetLegacyFallbackWarnings() {
|
|
77
|
+
WARNED_LEGACY_FALLBACK.clear();
|
|
78
|
+
}
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
// Build
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
/**
|
|
83
|
+
* Build the content-tree projects for one site. Pure data — no HTML,
|
|
84
|
+
* no path-style decisions.
|
|
85
|
+
*
|
|
86
|
+
* Tree assembly (Phase 19c):
|
|
87
|
+
* 1. Walk the fs (`fsWalk`) to enumerate every directory under
|
|
88
|
+
* contentDir. Each fs entry contributes a candidate node keyed by
|
|
89
|
+
* its fs-relative path.
|
|
90
|
+
* 2. Build the content index (`buildContentIndex`) to map
|
|
91
|
+
* `relPath → entryId` based on frontmatter `id:`.
|
|
92
|
+
* 3. For each fs node with a markdown index/README, look up the
|
|
93
|
+
* bound entry id via `index.byPath`, then resolve the calendar
|
|
94
|
+
* entry by id. If found → overlay state (lane, title from entry,
|
|
95
|
+
* slug as a display attribute).
|
|
96
|
+
* 4. For calendar entries whose id is NOT in the index (their file
|
|
97
|
+
* isn't bound to frontmatter yet — pre-doctor state), do
|
|
98
|
+
* legacy slug fallback: look for an fs node whose path equals
|
|
99
|
+
* the entry's slug. If found → overlay with a one-time warning.
|
|
100
|
+
* If not found → place as a ghost node (today's behavior,
|
|
101
|
+
* preserved for backward compat).
|
|
102
|
+
*/
|
|
103
|
+
export function buildContentTree(site, entries, config, projectRoot, options = {}) {
|
|
104
|
+
const lookup = options.scrapbookLookup ??
|
|
105
|
+
((siteArg, path) => {
|
|
106
|
+
try {
|
|
107
|
+
return listScrapbook(projectRoot, config, siteArg, path);
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
return { items: [], secretItems: [] };
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
const fsWalk = options.fsWalk ?? ((siteArg) => defaultFsWalk(projectRoot, config, siteArg));
|
|
114
|
+
const fsEntries = fsWalk(site);
|
|
115
|
+
const fsEntryByPath = new Map();
|
|
116
|
+
for (const e of fsEntries)
|
|
117
|
+
fsEntryByPath.set(e.slug, e);
|
|
118
|
+
const contentIndex = options.contentIndex ?? buildContentIndex(projectRoot, config, site);
|
|
119
|
+
const warn = options.warn ?? ((msg) => console.warn(msg));
|
|
120
|
+
const contentDir = resolveContentDir(projectRoot, config, site);
|
|
121
|
+
// ---- Phase 1: assemble the union of paths that need nodes ----
|
|
122
|
+
// Calendar entries contribute through TWO routes:
|
|
123
|
+
// (a) id-based binding: index.byPath[relPath] === entry.id binds
|
|
124
|
+
// the entry to that fs path. The path becomes the node key.
|
|
125
|
+
// (b) legacy slug fallback: when (a) doesn't fire, treat the
|
|
126
|
+
// entry's slug as a candidate path. If the fs walk includes
|
|
127
|
+
// it, overlay there with a warning. Otherwise it's a ghost.
|
|
128
|
+
const allPaths = new Set();
|
|
129
|
+
// Track which calendar entries bind to which paths. The path is
|
|
130
|
+
// either the fs-bound path (id-driven) or the entry's slug (legacy
|
|
131
|
+
// fallback / ghost).
|
|
132
|
+
const overlayByPath = new Map();
|
|
133
|
+
// Filesystem-derived paths first — every walked dir contributes its
|
|
134
|
+
// own path AND its ancestors so the tree wires up cleanly.
|
|
135
|
+
for (const e of fsEntries) {
|
|
136
|
+
if (e.hasReadme || e.hasIndex) {
|
|
137
|
+
allPaths.add(e.slug);
|
|
138
|
+
for (const a of ancestorsOf(e.slug))
|
|
139
|
+
allPaths.add(a);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
// Determine each entry's binding target (id-based, slug-fallback, ghost).
|
|
143
|
+
for (const entry of entries) {
|
|
144
|
+
const idBoundPath = findIdBoundPath(entry, contentIndex);
|
|
145
|
+
if (idBoundPath !== null) {
|
|
146
|
+
overlayByPath.set(idBoundPath, entry);
|
|
147
|
+
allPaths.add(idBoundPath);
|
|
148
|
+
for (const a of ancestorsOf(idBoundPath))
|
|
149
|
+
allPaths.add(a);
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
// Legacy slug-fallback: if the slug looks like an fs path AND
|
|
153
|
+
// matches a walked dir, overlay there. Otherwise ghost.
|
|
154
|
+
if (fsEntryByPath.has(entry.slug)) {
|
|
155
|
+
overlayByPath.set(entry.slug, entry);
|
|
156
|
+
allPaths.add(entry.slug);
|
|
157
|
+
for (const a of ancestorsOf(entry.slug))
|
|
158
|
+
allPaths.add(a);
|
|
159
|
+
// De-dup the warning per process — without this the studio's polled
|
|
160
|
+
// dashboard re-emits the same warning on every render. Key by
|
|
161
|
+
// (site, entryId | slug) so a renamed slug warns again.
|
|
162
|
+
const dedupKey = `${site}:${entry.id ?? entry.slug}`;
|
|
163
|
+
if (!WARNED_LEGACY_FALLBACK.has(dedupKey)) {
|
|
164
|
+
WARNED_LEGACY_FALLBACK.add(dedupKey);
|
|
165
|
+
warn(`[content-tree] Calendar entry "${entry.slug}" matched fs node by slug ` +
|
|
166
|
+
`(no frontmatter id binding). Run \`deskwork doctor --fix=missing-frontmatter-id\` ` +
|
|
167
|
+
`to make this binding refactor-proof.`);
|
|
168
|
+
}
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
// Ghost: entry exists but has no fs counterpart at slug-equals-path
|
|
172
|
+
// and no id binding. Surface it (the calendar is authoritative for
|
|
173
|
+
// "this entry exists") so the operator sees it in the tree.
|
|
174
|
+
allPaths.add(entry.slug);
|
|
175
|
+
for (const a of ancestorsOf(entry.slug))
|
|
176
|
+
allPaths.add(a);
|
|
177
|
+
overlayByPath.set(entry.slug, entry);
|
|
178
|
+
}
|
|
179
|
+
// ---- Phase 2: build nodes for every path in the union ----
|
|
180
|
+
const sortedPaths = [...allPaths].sort();
|
|
181
|
+
const nodeByPath = new Map();
|
|
182
|
+
for (const path of sortedPaths) {
|
|
183
|
+
const overlay = overlayByPath.get(path) ?? null;
|
|
184
|
+
const fsEntry = fsEntryByPath.get(path) ?? null;
|
|
185
|
+
const sb = lookup(site, path);
|
|
186
|
+
const items = [...sb.items, ...sb.secretItems];
|
|
187
|
+
const mostRecent = items.reduce((acc, it) => pickLatestMtime(acc, it.mtime), null);
|
|
188
|
+
// Title resolution: calendar wins, then fs frontmatter, then leaf.
|
|
189
|
+
const title = overlay?.title ??
|
|
190
|
+
(fsEntry?.title ?? null) ??
|
|
191
|
+
leafOfPath(path);
|
|
192
|
+
// hasOwnIndex resolution + on-disk file path resolution. Both
|
|
193
|
+
// depend on the content index's id-binding when an entry overlays
|
|
194
|
+
// — the bound file is the SSOT for both the existence check
|
|
195
|
+
// (`hasOwnIndex`) and the renderer's path display (Issue #70).
|
|
196
|
+
let hasOwnIndex = false;
|
|
197
|
+
let boundFile;
|
|
198
|
+
if (overlay !== null) {
|
|
199
|
+
boundFile = idBoundFile(overlay, contentIndex);
|
|
200
|
+
hasOwnIndex = entryHasOwnIndex(contentDir, path, fsEntry?.hasIndex ?? false, fsEntry?.hasReadme ?? false, boundFile, fsEntry !== null);
|
|
201
|
+
}
|
|
202
|
+
else if (fsEntry !== null) {
|
|
203
|
+
hasOwnIndex = fsEntry.hasIndex || fsEntry.hasReadme;
|
|
204
|
+
}
|
|
205
|
+
const node = {
|
|
206
|
+
site,
|
|
207
|
+
path,
|
|
208
|
+
title,
|
|
209
|
+
lane: overlay?.stage ?? null,
|
|
210
|
+
entry: overlay,
|
|
211
|
+
hasOwnIndex,
|
|
212
|
+
hasFsDir: fsEntry !== null,
|
|
213
|
+
scrapbookCount: items.length,
|
|
214
|
+
scrapbookMostRecentMtime: mostRecent,
|
|
215
|
+
children: [],
|
|
216
|
+
};
|
|
217
|
+
if (overlay?.slug !== undefined) {
|
|
218
|
+
// Slug only set when an entry overlays — used by the studio for
|
|
219
|
+
// the "public URL" hover hint. Honoring exactOptionalPropertyTypes:
|
|
220
|
+
// omit the field entirely rather than assigning undefined.
|
|
221
|
+
node.slug = overlay.slug;
|
|
222
|
+
}
|
|
223
|
+
if (boundFile !== undefined) {
|
|
224
|
+
// Issue #70: surface the actual on-disk path so the renderer
|
|
225
|
+
// doesn't have to reconstruct `<path>/index.md` (which is wrong
|
|
226
|
+
// for hierarchical / non-template layouts).
|
|
227
|
+
node.filePath = boundFile;
|
|
228
|
+
}
|
|
229
|
+
nodeByPath.set(path, node);
|
|
230
|
+
}
|
|
231
|
+
// Wire up parent → child links by path shape.
|
|
232
|
+
for (const path of sortedPaths) {
|
|
233
|
+
const parts = path.split('/');
|
|
234
|
+
if (parts.length === 1)
|
|
235
|
+
continue;
|
|
236
|
+
const parentPath = parts.slice(0, -1).join('/');
|
|
237
|
+
const parent = nodeByPath.get(parentPath);
|
|
238
|
+
const node = nodeByPath.get(path);
|
|
239
|
+
if (parent && node)
|
|
240
|
+
parent.children.push(node);
|
|
241
|
+
}
|
|
242
|
+
// Group root-level paths by project (their first segment).
|
|
243
|
+
const projectRootBy = new Map();
|
|
244
|
+
for (const path of sortedPaths) {
|
|
245
|
+
if (path.includes('/'))
|
|
246
|
+
continue;
|
|
247
|
+
const node = nodeByPath.get(path);
|
|
248
|
+
if (!node)
|
|
249
|
+
continue;
|
|
250
|
+
const arr = projectRootBy.get(path) ?? [];
|
|
251
|
+
arr.push(node);
|
|
252
|
+
projectRootBy.set(path, arr);
|
|
253
|
+
}
|
|
254
|
+
// Calendars with entries at deep paths and no top-level fs node
|
|
255
|
+
// need a synthetic project root at the first path segment.
|
|
256
|
+
const knownRoots = new Set(projectRootBy.keys());
|
|
257
|
+
for (const e of entries) {
|
|
258
|
+
// Compute the path the entry resolves to (id-bound or slug).
|
|
259
|
+
const idBoundPath = findIdBoundPath(e, contentIndex);
|
|
260
|
+
const entryPath = idBoundPath ?? e.slug;
|
|
261
|
+
const root = rootSegment(entryPath);
|
|
262
|
+
if (!knownRoots.has(root)) {
|
|
263
|
+
const sb = lookup(site, root);
|
|
264
|
+
const items = [...sb.items, ...sb.secretItems];
|
|
265
|
+
const mostRecent = items.reduce((acc, it) => pickLatestMtime(acc, it.mtime), null);
|
|
266
|
+
const synth = {
|
|
267
|
+
site,
|
|
268
|
+
path: root,
|
|
269
|
+
title: leafOfPath(root),
|
|
270
|
+
lane: null,
|
|
271
|
+
entry: null,
|
|
272
|
+
hasOwnIndex: false,
|
|
273
|
+
hasFsDir: fsEntryByPath.has(root),
|
|
274
|
+
scrapbookCount: items.length,
|
|
275
|
+
scrapbookMostRecentMtime: mostRecent,
|
|
276
|
+
children: [],
|
|
277
|
+
};
|
|
278
|
+
nodeByPath.set(root, synth);
|
|
279
|
+
// Re-attach orphans whose direct parent was a missing
|
|
280
|
+
// intermediate path we hadn't generated.
|
|
281
|
+
for (const path of sortedPaths) {
|
|
282
|
+
if (rootSegment(path) === root && path.includes('/')) {
|
|
283
|
+
const parentPath = path.split('/').slice(0, -1).join('/');
|
|
284
|
+
if (parentPath === root) {
|
|
285
|
+
const child = nodeByPath.get(path);
|
|
286
|
+
if (child && !synth.children.includes(child)) {
|
|
287
|
+
synth.children.push(child);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
projectRootBy.set(root, [synth]);
|
|
293
|
+
knownRoots.add(root);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
// Sort children deterministically.
|
|
297
|
+
for (const node of nodeByPath.values()) {
|
|
298
|
+
node.children.sort((a, b) => a.path.localeCompare(b.path));
|
|
299
|
+
}
|
|
300
|
+
// Fold each project root into a ContentProject summary.
|
|
301
|
+
const projects = [];
|
|
302
|
+
for (const [rootPath, roots] of projectRootBy.entries()) {
|
|
303
|
+
if (roots.length === 0)
|
|
304
|
+
continue;
|
|
305
|
+
const root = roots[0];
|
|
306
|
+
const summary = summarizeProject(site, rootPath, root);
|
|
307
|
+
projects.push(summary);
|
|
308
|
+
}
|
|
309
|
+
projects.sort((a, b) => a.rootSlug.localeCompare(b.rootSlug));
|
|
310
|
+
return projects;
|
|
311
|
+
}
|
|
312
|
+
function summarizeProject(site, rootPath, root) {
|
|
313
|
+
let trackedCount = 0;
|
|
314
|
+
let totalNodes = 0;
|
|
315
|
+
let maxDepth = 0;
|
|
316
|
+
let scrapbookCount = 0;
|
|
317
|
+
const laneCounts = new Map();
|
|
318
|
+
const visit = (node, depth) => {
|
|
319
|
+
totalNodes += 1;
|
|
320
|
+
maxDepth = Math.max(maxDepth, depth);
|
|
321
|
+
scrapbookCount += node.scrapbookCount;
|
|
322
|
+
if (node.entry !== null) {
|
|
323
|
+
trackedCount += 1;
|
|
324
|
+
const stage = node.entry.stage;
|
|
325
|
+
laneCounts.set(stage, (laneCounts.get(stage) ?? 0) + 1);
|
|
326
|
+
}
|
|
327
|
+
for (const c of node.children)
|
|
328
|
+
visit(c, depth + 1);
|
|
329
|
+
};
|
|
330
|
+
visit(root, 1);
|
|
331
|
+
let predominantLane = null;
|
|
332
|
+
let bestCount = 0;
|
|
333
|
+
for (const [stage, count] of laneCounts.entries()) {
|
|
334
|
+
if (count > bestCount) {
|
|
335
|
+
predominantLane = stage;
|
|
336
|
+
bestCount = count;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
return {
|
|
340
|
+
site,
|
|
341
|
+
rootSlug: rootPath,
|
|
342
|
+
title: root.title,
|
|
343
|
+
trackedCount,
|
|
344
|
+
totalNodes,
|
|
345
|
+
maxDepth,
|
|
346
|
+
scrapbookCount,
|
|
347
|
+
predominantLane,
|
|
348
|
+
root,
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* Helper: find the node with the given path under a project tree, or
|
|
353
|
+
* return null. Used by the studio's node-detail panel.
|
|
354
|
+
*/
|
|
355
|
+
export function findNode(project, path) {
|
|
356
|
+
if (project.root.path === path)
|
|
357
|
+
return project.root;
|
|
358
|
+
const queue = [...project.root.children];
|
|
359
|
+
while (queue.length > 0) {
|
|
360
|
+
const head = queue.shift();
|
|
361
|
+
if (!head)
|
|
362
|
+
continue;
|
|
363
|
+
if (head.path === path)
|
|
364
|
+
return head;
|
|
365
|
+
queue.push(...head.children);
|
|
366
|
+
}
|
|
367
|
+
return null;
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* Flatten the tree into a depth-first ordered list. See `FlatNode` in
|
|
371
|
+
* `content-tree-types.ts` for the row shape.
|
|
372
|
+
*/
|
|
373
|
+
export function flattenForRender(root) {
|
|
374
|
+
const out = [];
|
|
375
|
+
const walk = (node, depth, isLast) => {
|
|
376
|
+
out.push({ node, depth, isLast });
|
|
377
|
+
const last = node.children.length - 1;
|
|
378
|
+
for (let i = 0; i < node.children.length; i++) {
|
|
379
|
+
walk(node.children[i], depth + 1, i === last);
|
|
380
|
+
}
|
|
381
|
+
};
|
|
382
|
+
walk(root, 0, true);
|
|
383
|
+
return out;
|
|
384
|
+
}
|
|
385
|
+
//# sourceMappingURL=content-tree.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"content-tree.js","sourceRoot":"","sources":["../src/content-tree.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Doctor — public surface.
|
|
3
|
+
*
|
|
4
|
+
* Re-exports the rule registry, runner, types, and the schema-patch
|
|
5
|
+
* helper. Callers (the CLI command, the studio in a future phase, and
|
|
6
|
+
* tests) only import from here.
|
|
7
|
+
*/
|
|
8
|
+
export type { DoctorContext, DoctorInteraction, DoctorReport, DoctorRule, Finding, FindingSeverity, RepairChoice, RepairPlan, RepairResult, SkipReason, } from './types.ts';
|
|
9
|
+
export { RULES, parseFixArgument, runAudit, runRepair, yesInteraction, declineInteraction, type DoctorRunOptions, } from './runner.ts';
|
|
10
|
+
export { printSchemaPatchInstructions } from './schema-patch.ts';
|
|
11
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/doctor/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,YAAY,EACV,aAAa,EACb,iBAAiB,EACjB,YAAY,EACZ,UAAU,EACV,OAAO,EACP,eAAe,EACf,YAAY,EACZ,UAAU,EACV,YAAY,EACZ,UAAU,GACX,MAAM,YAAY,CAAC;AAEpB,OAAO,EACL,KAAK,EACL,gBAAgB,EAChB,QAAQ,EACR,SAAS,EACT,cAAc,EACd,kBAAkB,EAClB,KAAK,gBAAgB,GACtB,MAAM,aAAa,CAAC;AAErB,OAAO,EAAE,4BAA4B,EAAE,MAAM,mBAAmB,CAAC"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Doctor — public surface.
|
|
3
|
+
*
|
|
4
|
+
* Re-exports the rule registry, runner, types, and the schema-patch
|
|
5
|
+
* helper. Callers (the CLI command, the studio in a future phase, and
|
|
6
|
+
* tests) only import from here.
|
|
7
|
+
*/
|
|
8
|
+
export { RULES, parseFixArgument, runAudit, runRepair, yesInteraction, declineInteraction, } from "./runner.js";
|
|
9
|
+
export { printSchemaPatchInstructions } from "./schema-patch.js";
|
|
10
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/doctor/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAeH,OAAO,EACL,KAAK,EACL,gBAAgB,EAChB,QAAQ,EACR,SAAS,EACT,cAAc,EACd,kBAAkB,GAEnB,MAAM,aAAa,CAAC;AAErB,OAAO,EAAE,4BAA4B,EAAE,MAAM,mBAAmB,CAAC"}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phase 23f — project-level doctor rule loader.
|
|
3
|
+
*
|
|
4
|
+
* Operators can drop a `.ts` module in `<projectRoot>/.deskwork/doctor/`
|
|
5
|
+
* to register a custom rule. The runner merges those project rules
|
|
6
|
+
* with the built-in rules; basename collisions let the project rule
|
|
7
|
+
* override the built-in (e.g., a project-supplied
|
|
8
|
+
* `missing-frontmatter-id.ts` replaces the bundled one).
|
|
9
|
+
*
|
|
10
|
+
* Override resolution by basename, not by `rule.id`: the basename is
|
|
11
|
+
* what the operator types; mapping it to a rule object happens in the
|
|
12
|
+
* import. We chose basename-as-key so operators can author a rule
|
|
13
|
+
* whose internal `id` differs from the file name (e.g., a project's
|
|
14
|
+
* "tighter" version of an existing rule keeps the same id for `--fix`
|
|
15
|
+
* compatibility but lives at a basename-collision path).
|
|
16
|
+
*
|
|
17
|
+
* Discovery is sync — the runner builds the merged rule list once at
|
|
18
|
+
* the start of an audit/repair run, not per finding. We use
|
|
19
|
+
* `readdirSync` + `import()` (the latter is async; we await all
|
|
20
|
+
* project rules in `loadProjectRules`).
|
|
21
|
+
*
|
|
22
|
+
* Failure mode: a project rule that fails to import (bad TypeScript,
|
|
23
|
+
* wrong default export shape) throws. The runner surfaces the throw
|
|
24
|
+
* to the operator rather than silently dropping the rule.
|
|
25
|
+
*/
|
|
26
|
+
import type { DoctorRule } from './types.ts';
|
|
27
|
+
/**
|
|
28
|
+
* One loaded project rule plus its source basename. The basename is
|
|
29
|
+
* what the runner uses to detect override collisions with built-in
|
|
30
|
+
* rules.
|
|
31
|
+
*/
|
|
32
|
+
export interface LoadedProjectRule {
|
|
33
|
+
/** Filename without extension — e.g. `missing-frontmatter-id`. */
|
|
34
|
+
basename: string;
|
|
35
|
+
/** Absolute path of the source file. */
|
|
36
|
+
path: string;
|
|
37
|
+
/** Imported and type-narrowed rule. */
|
|
38
|
+
rule: DoctorRule;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Walk `<projectRoot>/.deskwork/doctor/` and return every rule found,
|
|
42
|
+
* in alphabetical order. Returns an empty list when the directory
|
|
43
|
+
* doesn't exist — no `.deskwork/doctor/` is the common case and must
|
|
44
|
+
* not throw.
|
|
45
|
+
*/
|
|
46
|
+
export declare function loadProjectRules(projectRoot: string): Promise<LoadedProjectRule[]>;
|
|
47
|
+
/**
|
|
48
|
+
* Merge built-in rules with project rules. Project rules with a
|
|
49
|
+
* basename matching a built-in rule's basename REPLACE the built-in
|
|
50
|
+
* (override). Project rules with new basenames are APPENDED in their
|
|
51
|
+
* loaded order (alphabetical from `loadProjectRules`).
|
|
52
|
+
*
|
|
53
|
+
* The basename-of-built-in mapping uses each built-in rule's `id`,
|
|
54
|
+
* because every shipped rule is named after its id. If we ever ship
|
|
55
|
+
* a built-in whose file basename and id differ, this mapping will
|
|
56
|
+
* need an explicit table.
|
|
57
|
+
*/
|
|
58
|
+
export declare function mergeRules(builtIns: ReadonlyArray<DoctorRule>, projectRules: ReadonlyArray<LoadedProjectRule>): DoctorRule[];
|
|
59
|
+
//# sourceMappingURL=project-rules.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"project-rules.d.ts","sourceRoot":"","sources":["../../src/doctor/project-rules.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAIH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AA4D7C;;;;GAIG;AACH,MAAM,WAAW,iBAAiB;IAChC,kEAAkE;IAClE,QAAQ,EAAE,MAAM,CAAC;IACjB,wCAAwC;IACxC,IAAI,EAAE,MAAM,CAAC;IACb,uCAAuC;IACvC,IAAI,EAAE,UAAU,CAAC;CAClB;AAED;;;;;GAKG;AACH,wBAAsB,gBAAgB,CACpC,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,iBAAiB,EAAE,CAAC,CAyB9B;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,UAAU,CACxB,QAAQ,EAAE,aAAa,CAAC,UAAU,CAAC,EACnC,YAAY,EAAE,aAAa,CAAC,iBAAiB,CAAC,GAC7C,UAAU,EAAE,CAyBd"}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phase 23f — project-level doctor rule loader.
|
|
3
|
+
*
|
|
4
|
+
* Operators can drop a `.ts` module in `<projectRoot>/.deskwork/doctor/`
|
|
5
|
+
* to register a custom rule. The runner merges those project rules
|
|
6
|
+
* with the built-in rules; basename collisions let the project rule
|
|
7
|
+
* override the built-in (e.g., a project-supplied
|
|
8
|
+
* `missing-frontmatter-id.ts` replaces the bundled one).
|
|
9
|
+
*
|
|
10
|
+
* Override resolution by basename, not by `rule.id`: the basename is
|
|
11
|
+
* what the operator types; mapping it to a rule object happens in the
|
|
12
|
+
* import. We chose basename-as-key so operators can author a rule
|
|
13
|
+
* whose internal `id` differs from the file name (e.g., a project's
|
|
14
|
+
* "tighter" version of an existing rule keeps the same id for `--fix`
|
|
15
|
+
* compatibility but lives at a basename-collision path).
|
|
16
|
+
*
|
|
17
|
+
* Discovery is sync — the runner builds the merged rule list once at
|
|
18
|
+
* the start of an audit/repair run, not per finding. We use
|
|
19
|
+
* `readdirSync` + `import()` (the latter is async; we await all
|
|
20
|
+
* project rules in `loadProjectRules`).
|
|
21
|
+
*
|
|
22
|
+
* Failure mode: a project rule that fails to import (bad TypeScript,
|
|
23
|
+
* wrong default export shape) throws. The runner surfaces the throw
|
|
24
|
+
* to the operator rather than silently dropping the rule.
|
|
25
|
+
*/
|
|
26
|
+
var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
|
|
27
|
+
if (typeof path === "string" && /^\.\.?\//.test(path)) {
|
|
28
|
+
return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
|
|
29
|
+
return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
return path;
|
|
33
|
+
};
|
|
34
|
+
import { existsSync, readdirSync } from 'node:fs';
|
|
35
|
+
import { join } from 'node:path';
|
|
36
|
+
const RULE_FILE_SUFFIX = '.ts';
|
|
37
|
+
/**
|
|
38
|
+
* Narrow an unknown imported value to a `DoctorRule`. We require:
|
|
39
|
+
* - object with non-null id+label string fields,
|
|
40
|
+
* - audit / plan / apply functions.
|
|
41
|
+
*
|
|
42
|
+
* Failures throw a descriptive message — the operator's rule file is
|
|
43
|
+
* malformed and they need to know which file and which field.
|
|
44
|
+
*/
|
|
45
|
+
function assertDoctorRule(value, source) {
|
|
46
|
+
if (typeof value !== 'object' || value === null) {
|
|
47
|
+
throw new Error(`project doctor rule ${source}: default export must be an object`);
|
|
48
|
+
}
|
|
49
|
+
const id = Reflect.get(value, 'id');
|
|
50
|
+
const label = Reflect.get(value, 'label');
|
|
51
|
+
const audit = Reflect.get(value, 'audit');
|
|
52
|
+
const plan = Reflect.get(value, 'plan');
|
|
53
|
+
const apply = Reflect.get(value, 'apply');
|
|
54
|
+
if (typeof id !== 'string' || id.length === 0) {
|
|
55
|
+
throw new Error(`project doctor rule ${source}: 'id' must be a non-empty string`);
|
|
56
|
+
}
|
|
57
|
+
if (typeof label !== 'string' || label.length === 0) {
|
|
58
|
+
throw new Error(`project doctor rule ${source}: 'label' must be a non-empty string`);
|
|
59
|
+
}
|
|
60
|
+
if (typeof audit !== 'function') {
|
|
61
|
+
throw new Error(`project doctor rule ${source}: 'audit' must be a function`);
|
|
62
|
+
}
|
|
63
|
+
if (typeof plan !== 'function') {
|
|
64
|
+
throw new Error(`project doctor rule ${source}: 'plan' must be a function`);
|
|
65
|
+
}
|
|
66
|
+
if (typeof apply !== 'function') {
|
|
67
|
+
throw new Error(`project doctor rule ${source}: 'apply' must be a function`);
|
|
68
|
+
}
|
|
69
|
+
// The shape checks above narrow `value` to a DoctorRule for
|
|
70
|
+
// practical purposes; we synthesize a plain rule reference rather
|
|
71
|
+
// than `as`-cast to avoid the lint rule about type assertions.
|
|
72
|
+
return {
|
|
73
|
+
id,
|
|
74
|
+
label,
|
|
75
|
+
audit: audit.bind(value),
|
|
76
|
+
plan: plan.bind(value),
|
|
77
|
+
apply: apply.bind(value),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Walk `<projectRoot>/.deskwork/doctor/` and return every rule found,
|
|
82
|
+
* in alphabetical order. Returns an empty list when the directory
|
|
83
|
+
* doesn't exist — no `.deskwork/doctor/` is the common case and must
|
|
84
|
+
* not throw.
|
|
85
|
+
*/
|
|
86
|
+
export async function loadProjectRules(projectRoot) {
|
|
87
|
+
const dir = join(projectRoot, '.deskwork', 'doctor');
|
|
88
|
+
if (!existsSync(dir))
|
|
89
|
+
return [];
|
|
90
|
+
const entries = readdirSync(dir).filter((n) => n.endsWith(RULE_FILE_SUFFIX));
|
|
91
|
+
entries.sort();
|
|
92
|
+
const out = [];
|
|
93
|
+
for (const name of entries) {
|
|
94
|
+
const path = join(dir, name);
|
|
95
|
+
const basename = name.slice(0, -RULE_FILE_SUFFIX.length);
|
|
96
|
+
// Dynamic import via an absolute path — works under tsx (the
|
|
97
|
+
// runtime the CLI uses) and node when project rules are pre-
|
|
98
|
+
// compiled to JS adjacent paths in some future workflow.
|
|
99
|
+
const mod = await import(__rewriteRelativeImportExtension(path));
|
|
100
|
+
if (typeof mod !== 'object' || mod === null) {
|
|
101
|
+
throw new Error(`project doctor rule ${path}: import did not produce a module object`);
|
|
102
|
+
}
|
|
103
|
+
const def = Reflect.get(mod, 'default');
|
|
104
|
+
const rule = assertDoctorRule(def, path);
|
|
105
|
+
out.push({ basename, path, rule });
|
|
106
|
+
}
|
|
107
|
+
return out;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Merge built-in rules with project rules. Project rules with a
|
|
111
|
+
* basename matching a built-in rule's basename REPLACE the built-in
|
|
112
|
+
* (override). Project rules with new basenames are APPENDED in their
|
|
113
|
+
* loaded order (alphabetical from `loadProjectRules`).
|
|
114
|
+
*
|
|
115
|
+
* The basename-of-built-in mapping uses each built-in rule's `id`,
|
|
116
|
+
* because every shipped rule is named after its id. If we ever ship
|
|
117
|
+
* a built-in whose file basename and id differ, this mapping will
|
|
118
|
+
* need an explicit table.
|
|
119
|
+
*/
|
|
120
|
+
export function mergeRules(builtIns, projectRules) {
|
|
121
|
+
// Map built-in rule id (== basename today) → index, so we can
|
|
122
|
+
// splice in the override at the same position.
|
|
123
|
+
const builtInIndexByBasename = new Map();
|
|
124
|
+
for (let i = 0; i < builtIns.length; i++) {
|
|
125
|
+
builtInIndexByBasename.set(builtIns[i].id, i);
|
|
126
|
+
}
|
|
127
|
+
const merged = builtIns.slice();
|
|
128
|
+
const overriddenBasenames = new Set();
|
|
129
|
+
for (const p of projectRules) {
|
|
130
|
+
const idx = builtInIndexByBasename.get(p.basename);
|
|
131
|
+
if (idx !== undefined) {
|
|
132
|
+
merged[idx] = p.rule;
|
|
133
|
+
overriddenBasenames.add(p.basename);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
for (const p of projectRules) {
|
|
137
|
+
if (overriddenBasenames.has(p.basename))
|
|
138
|
+
continue;
|
|
139
|
+
merged.push(p.rule);
|
|
140
|
+
}
|
|
141
|
+
return merged;
|
|
142
|
+
}
|
|
143
|
+
//# sourceMappingURL=project-rules.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"project-rules.js","sourceRoot":"","sources":["../../src/doctor/project-rules.ts"],"names":[],"mappings":"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"}
|