@opum-ai/lore 0.1.0 → 0.1.1
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/README.md +33 -22
- package/bin/lore.cjs +35 -7
- package/package.json +17 -17
- package/src/adapters/backlog.ts +0 -1084
- package/src/adapters/git.ts +0 -221
- package/src/cli.ts +0 -667
- package/src/commands/agent.ts +0 -301
- package/src/commands/agents.ts +0 -302
- package/src/commands/args.ts +0 -209
- package/src/commands/changed.ts +0 -70
- package/src/commands/check.ts +0 -1031
- package/src/commands/codex-bridge.ts +0 -49
- package/src/commands/concurrency.ts +0 -48
- package/src/commands/context.ts +0 -292
- package/src/commands/discover.ts +0 -89
- package/src/commands/explorer.ts +0 -253
- package/src/commands/export.ts +0 -93
- package/src/commands/fswrite.ts +0 -928
- package/src/commands/graph.ts +0 -291
- package/src/commands/help.ts +0 -151
- package/src/commands/impact.ts +0 -59
- package/src/commands/init.ts +0 -583
- package/src/commands/instructions.ts +0 -91
- package/src/commands/link.ts +0 -929
- package/src/commands/new.ts +0 -476
- package/src/commands/orphans.ts +0 -457
- package/src/commands/path.ts +0 -67
- package/src/commands/provenance.ts +0 -68
- package/src/commands/query.ts +0 -312
- package/src/commands/reconcile-shared.ts +0 -280
- package/src/commands/rename.ts +0 -585
- package/src/commands/replace.ts +0 -320
- package/src/commands/scaffold.ts +0 -346
- package/src/commands/schema.ts +0 -293
- package/src/commands/snapshot.ts +0 -130
- package/src/commands/supersede.ts +0 -400
- package/src/commands/sync.ts +0 -371
- package/src/commands/tasks.ts +0 -271
- package/src/commands/traversal.ts +0 -151
- package/src/commands/validate.ts +0 -226
- package/src/config.ts +0 -598
- package/src/core/agent-bridge.ts +0 -287
- package/src/core/agent-context.ts +0 -498
- package/src/core/agent-profile.ts +0 -447
- package/src/core/bundle.ts +0 -893
- package/src/core/check.ts +0 -853
- package/src/core/codex-bridge.ts +0 -100
- package/src/core/concept.ts +0 -597
- package/src/core/consumer-scaffold.ts +0 -433
- package/src/core/context.ts +0 -271
- package/src/core/explorer-contract.ts +0 -441
- package/src/core/explorer-qualification.ts +0 -58
- package/src/core/explorer.ts +0 -518
- package/src/core/finding.ts +0 -31
- package/src/core/graph.ts +0 -201
- package/src/core/indexes.ts +0 -436
- package/src/core/instructions.ts +0 -209
- package/src/core/ladybug-driver.ts +0 -1795
- package/src/core/ladybug-lifecycle.ts +0 -1178
- package/src/core/ladybug-native.ts +0 -95
- package/src/core/ladybug-source.ts +0 -667
- package/src/core/links.ts +0 -681
- package/src/core/log.ts +0 -253
- package/src/core/managed-block.ts +0 -540
- package/src/core/manifest.ts +0 -718
- package/src/core/order.ts +0 -13
- package/src/core/profile.ts +0 -1007
- package/src/core/projection.ts +0 -195
- package/src/core/query.ts +0 -542
- package/src/core/reconcile.ts +0 -236
- package/src/core/replace.ts +0 -419
- package/src/core/retrieval.ts +0 -213
- package/src/core/rewrite.ts +0 -940
- package/src/core/scaffold.ts +0 -255
- package/src/core/schema.ts +0 -366
- package/src/core/snapshot-runtime.ts +0 -52
- package/src/core/snapshot-store.ts +0 -287
- package/src/core/snapshot.ts +0 -711
- package/src/core/template.ts +0 -429
- package/src/core/traversal.ts +0 -487
- package/src/core/validate.ts +0 -517
- package/src/core/workspace-contract.ts +0 -473
- package/src/core/workspace-projection.ts +0 -365
- package/src/core/workspace-retrieval.ts +0 -196
- package/src/core/workspace-source.ts +0 -174
- package/src/errors.ts +0 -697
- package/src/meta.ts +0 -7
- package/src/output.ts +0 -589
- package/src/scripts/upstream-backlog-watch.ts +0 -288
- package/src/state.ts +0 -390
package/src/core/template.ts
DELETED
|
@@ -1,429 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* template.ts — the **pure** rendering core behind `lore new`.
|
|
3
|
-
*
|
|
4
|
-
* Where `commands/new.ts` is the thin side-effecting layer (read the template file, write
|
|
5
|
-
* the result), this module computes — with no filesystem access — the exact bytes a new
|
|
6
|
-
* concept file should contain, and is the reason `lore new`'s output validates **by
|
|
7
|
-
* construction** ([ADR-0006](../../docs/adr/0006-schema-types-templates.md) §6; LORE-18 AC#1).
|
|
8
|
-
*
|
|
9
|
-
* The split that makes that guarantee hold (cli-surface §new: "valid OKF frontmatter **and**
|
|
10
|
-
* the per-type required sections; the **body** is rendered from `.lore/templates/<type>.md`"):
|
|
11
|
-
*
|
|
12
|
-
* - **lore owns the frontmatter.** {@link buildNewConcept} builds the frontmatter mapping
|
|
13
|
-
* *structurally* from typed inputs (type/title/summary/timestamp/tags) and serializes it
|
|
14
|
-
* through the byte-stable concept boundary, so js-yaml quotes whatever needs quoting — a
|
|
15
|
-
* title or summary containing `:`, `#`, or a leading `-` can never corrupt the YAML the way
|
|
16
|
-
* raw string substitution into a `key: {{value}}` line would.
|
|
17
|
-
* - **the template owns the body.** A template is body-only markdown with `{{placeholders}}`;
|
|
18
|
-
* {@link renderTemplate} fills the auto tokens (`{{title}}`/`{{type}}`/`{{timestamp}}`/
|
|
19
|
-
* `{{summary}}`) plus any `--var`, and *reports* an unfilled token rather than leaving a
|
|
20
|
-
* literal `{{…}}` in the file, so a missing value fails loud (exit `6`).
|
|
21
|
-
*
|
|
22
|
-
* The {@link BUILTIN_TEMPLATES} carry each known type's conventional section skeleton; a user
|
|
23
|
-
* template under `.lore/templates/` overrides the built-in body wholesale (AC#2) — that
|
|
24
|
-
* filesystem resolution is the command's concern, not this module's.
|
|
25
|
-
*/
|
|
26
|
-
|
|
27
|
-
import { posix } from "node:path";
|
|
28
|
-
import { LoreError, WarningCollector } from "../errors";
|
|
29
|
-
import { type Concept, idFromPath, serializeConcept, serializeConceptWithModeline } from "./concept";
|
|
30
|
-
import { encodePathSegments } from "./links";
|
|
31
|
-
import { defaultProfile, type Profile, slugForTypeName } from "./profile";
|
|
32
|
-
import { validateFrontmatter } from "./schema";
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Derive a filename slug from a concept title — the LOWER-KEBAB transform {@link slugForTypeName}
|
|
36
|
-
* applies to type names (`"Bulk Archive Orders!"` → `"bulk-archive-orders"`). The result is the last
|
|
37
|
-
* path segment of the new doc's id, so it uses the `[a-z0-9-]` alphabet a portable bundle path
|
|
38
|
-
* needs. A title with no alphanumeric content yields `""`; the caller treats that as "cannot derive
|
|
39
|
-
* a path, pass `--out`" rather than writing a `-.md` file. Aliased to the canonical slug algorithm
|
|
40
|
-
* so a title and a type name can never slug two different ways.
|
|
41
|
-
*/
|
|
42
|
-
export const slugify = slugForTypeName;
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* The OKF `resource` link value for a concept at `docPath` under `resourceBase` (LORE-47 / AC#4) —
|
|
46
|
-
* the canonical published location a reader follows, joined producer-side from two parts:
|
|
47
|
-
*
|
|
48
|
-
* - **`resourceBase`** is the configured prefix verbatim (`[profile].resource_base`), with any
|
|
49
|
-
* trailing slash(es) trimmed so the join contributes exactly one. It is *not* re-encoded: it is
|
|
50
|
-
* a user-authored URL that may legitimately carry a scheme, host, and its own path separators.
|
|
51
|
-
* - **`docPath`** is the doc's repo-relative POSIX path (e.g. `docs/stories/bulk-archive.md`),
|
|
52
|
-
* appended with each segment encoded through the shared {@link encodePathSegments} so a
|
|
53
|
-
* title-derived slug stays byte-identical (the `-` `_` `.` set is preserved) while a space-,
|
|
54
|
-
* paren-, or non-ASCII-bearing `--out` path is percent-escaped into a valid URL. Using the same
|
|
55
|
-
* encoder as the body cross-link writer ({@link normalizeLink}) keeps a doc's `resource` URL and
|
|
56
|
-
* its cross-links encoded identically. The `/` separators and the `.md` suffix are kept.
|
|
57
|
-
*
|
|
58
|
-
* Pure and total: the result is `<base-without-trailing-slash>/<encoded-doc-path>`, always exactly
|
|
59
|
-
* one slash at the seam. The caller decides *whether* to stamp ({@link stampResource}); this only
|
|
60
|
-
* computes the value.
|
|
61
|
-
*/
|
|
62
|
-
export function resourceFor(resourceBase: string, docPath: string): string {
|
|
63
|
-
// Trim before joining so a base padded with stray whitespace (or one whose configured value
|
|
64
|
-
// carried trailing whitespace after a slash) can never contribute an embedded space — `https://x/ `
|
|
65
|
-
// + `docs/a.md` would otherwise yield the broken `https://x/ /docs/a.md`.
|
|
66
|
-
const base = resourceBase.trim().replace(/\/+$/, "");
|
|
67
|
-
return `${base}/${encodePathSegments(docPath)}`;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* Any brace-shaped `{{…}}` token — matches regardless of what the inside holds, so a malformed
|
|
72
|
-
* shape ({@link PLACEHOLDER_NAME} rejects it) is still captured for {@link renderTemplate} to
|
|
73
|
-
* flag rather than falling through the old strict-grammar-only match untouched (LORE-157).
|
|
74
|
-
*/
|
|
75
|
-
const PLACEHOLDER = /\{\{([^{}]*)\}\}/g;
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* The strict `{{ key }}` name grammar: word chars, dots, and dashes, once a token's inner text
|
|
79
|
-
* is trimmed of its optional padding. A brace-shaped token whose trimmed inner text fails this —
|
|
80
|
-
* internal whitespace (`{{owner name}}`), empty (`{{}}`), or a disallowed character
|
|
81
|
-
* (`{{ owner/name }}`) — is not a legitimate placeholder (LORE-157).
|
|
82
|
-
*/
|
|
83
|
-
const PLACEHOLDER_NAME = /^[A-Za-z0-9_.-]+$/;
|
|
84
|
-
|
|
85
|
-
/** The outcome of {@link renderTemplate}: the filled text and any placeholders no value covered. */
|
|
86
|
-
export interface RenderResult {
|
|
87
|
-
/** The template with every resolved `{{key}}` substituted; unresolved tokens are left verbatim. */
|
|
88
|
-
text: string;
|
|
89
|
-
/**
|
|
90
|
-
* Distinct placeholder names that had no value in `vars`, in first-seen order — plus any
|
|
91
|
-
* malformed brace-shaped token (LORE-157), reported by its trimmed inner text since it can
|
|
92
|
-
* never be resolved regardless of what `vars` holds.
|
|
93
|
-
*/
|
|
94
|
-
unresolved: string[];
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* Substitute every `{{key}}` in `template` with `vars[key]`, reporting the names that had
|
|
99
|
-
* no value rather than silently dropping or blanking them. Membership is tested with
|
|
100
|
-
* {@link Object.hasOwn}, so a key present with an empty-string value resolves to `""`
|
|
101
|
-
* (intentional) while a key that is merely inherited (or absent) is reported unresolved —
|
|
102
|
-
* the caller turns a non-empty {@link RenderResult.unresolved} into a fail-loud error so a
|
|
103
|
-
* literal `{{…}}` never reaches a written file.
|
|
104
|
-
*
|
|
105
|
-
* A brace-shaped token whose trimmed inner text fails {@link PLACEHOLDER_NAME} is never
|
|
106
|
-
* resolvable no matter what `vars` holds, so it is always reported unresolved (LORE-157) instead
|
|
107
|
-
* of silently reaching `text` verbatim the way a strict-grammar-only match would miss it.
|
|
108
|
-
*/
|
|
109
|
-
export function renderTemplate(template: string, vars: Record<string, string>): RenderResult {
|
|
110
|
-
const unresolved: string[] = [];
|
|
111
|
-
const seen = new Set<string>();
|
|
112
|
-
const report = (key: string): void => {
|
|
113
|
-
if (!seen.has(key)) {
|
|
114
|
-
seen.add(key);
|
|
115
|
-
unresolved.push(key);
|
|
116
|
-
}
|
|
117
|
-
};
|
|
118
|
-
const text = template.replace(PLACEHOLDER, (match: string, inner: string) => {
|
|
119
|
-
const key = inner.trim();
|
|
120
|
-
if (!PLACEHOLDER_NAME.test(key)) {
|
|
121
|
-
report(key);
|
|
122
|
-
return match;
|
|
123
|
-
}
|
|
124
|
-
if (Object.hasOwn(vars, key)) {
|
|
125
|
-
return vars[key] as string;
|
|
126
|
-
}
|
|
127
|
-
report(key);
|
|
128
|
-
return match;
|
|
129
|
-
});
|
|
130
|
-
return { text, unresolved };
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
/** The inputs {@link buildNewConcept} turns into final concept bytes. */
|
|
134
|
-
export interface BuildNewConceptInput {
|
|
135
|
-
/** The new doc's repo-relative POSIX path — used for the id, diagnostics, and the modeline depth. */
|
|
136
|
-
docPath: string;
|
|
137
|
-
/** The concept `type` (canonical for a known type, verbatim for a producer extension). */
|
|
138
|
-
type: string;
|
|
139
|
-
/** The title — set on `title:` and available as `{{title}}` in the body. */
|
|
140
|
-
title: string;
|
|
141
|
-
/** The one-line summary — set on `summary:` and available as `{{summary}}` in the body. */
|
|
142
|
-
summary: string;
|
|
143
|
-
/** The ISO-8601 creation timestamp — set on `timestamp:` and available as `{{timestamp}}`. */
|
|
144
|
-
timestamp: string;
|
|
145
|
-
/** Optional `--tags` list, set on `tags:` as a YAML sequence (built structurally, never substituted). */
|
|
146
|
-
tags?: readonly string[];
|
|
147
|
-
/** The resolved **body** template (a user template, or a {@link builtinTemplateFor} fallback). */
|
|
148
|
-
bodyTemplate: string;
|
|
149
|
-
/** Extra `--var` placeholder values for the body (the auto tokens are added and take precedence). */
|
|
150
|
-
vars: Record<string, string>;
|
|
151
|
-
/**
|
|
152
|
-
* The editor modeline to splice inside the fence, or absent for none. The command decides: a
|
|
153
|
-
* known type whose exported schema actually exists on disk gets one; an unknown type, or a
|
|
154
|
-
* doc written outside an initialized bundle, gets none rather than a modeline pointing at a
|
|
155
|
-
* schema file that is not there.
|
|
156
|
-
*/
|
|
157
|
-
modeline?: string;
|
|
158
|
-
/** The active profile to validate/serialize against; defaults to the built-in {@link defaultProfile}. */
|
|
159
|
-
profile?: Profile;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
/** The result of {@link buildNewConcept}: the bytes to write and any advisory warnings raised on validation. */
|
|
163
|
-
export interface BuildNewConceptResult {
|
|
164
|
-
/** The exact bytes to write — frontmatter (+ editor modeline for a known type) + rendered body. */
|
|
165
|
-
contents: string;
|
|
166
|
-
/** The resolved (trimmed, non-empty) `type` of the concept, for the command to report. */
|
|
167
|
-
type: string;
|
|
168
|
-
/** Advisory warnings from validating the frontmatter (unknown type, …). */
|
|
169
|
-
warnings: readonly string[];
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
/**
|
|
173
|
-
* Build the final concept bytes from typed inputs and a body template.
|
|
174
|
-
*
|
|
175
|
-
* The body is rendered first ({@link renderTemplate}); an unfilled `{{placeholder}}` is a
|
|
176
|
-
* fail-loud `validation` {@link LoreError} (exit `6`, matching cli-surface §new "template
|
|
177
|
-
* missing required `{{var}}`") so no `{{…}}` is ever written. The auto tokens
|
|
178
|
-
* (`type`/`title`/`timestamp`/`summary`) are layered over any `--var` and **win**, so the
|
|
179
|
-
* positional title/type and resolved summary are authoritative.
|
|
180
|
-
*
|
|
181
|
-
* The frontmatter is assembled **structurally** — never by substituting into YAML text — so
|
|
182
|
-
* it is valid by construction regardless of what characters the title/summary/tags contain.
|
|
183
|
-
* It is validated ({@link validateFrontmatter}: a known type yields a clean concept; an
|
|
184
|
-
* unknown type warns and is tolerated) and serialized through the byte-stable concept
|
|
185
|
-
* boundary, with the caller-supplied editor modeline spliced inside the fence when present
|
|
186
|
-
* ({@link serializeConceptWithModeline}), else no modeline. A `--var` that shadows an auto
|
|
187
|
-
* token is reported as an advisory warning rather than silently dropped.
|
|
188
|
-
*/
|
|
189
|
-
export function buildNewConcept(input: BuildNewConceptInput): BuildNewConceptResult {
|
|
190
|
-
const warnings = new WarningCollector();
|
|
191
|
-
warnShadowedVars(input.vars, input.docPath, warnings);
|
|
192
|
-
const body = renderBody(input);
|
|
193
|
-
|
|
194
|
-
const frontmatter: Record<string, unknown> = {
|
|
195
|
-
type: input.type,
|
|
196
|
-
title: input.title,
|
|
197
|
-
summary: input.summary,
|
|
198
|
-
timestamp: input.timestamp,
|
|
199
|
-
};
|
|
200
|
-
if (input.tags && input.tags.length > 0) {
|
|
201
|
-
frontmatter.tags = [...input.tags];
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
const profile = input.profile ?? defaultProfile();
|
|
205
|
-
stampResource(frontmatter, input.type, input.docPath, profile);
|
|
206
|
-
const resolvedType = validateFrontmatter(frontmatter, { warnings, path: input.docPath, profile });
|
|
207
|
-
const concept: Concept = {
|
|
208
|
-
id: idFromPath(input.docPath),
|
|
209
|
-
path: input.docPath,
|
|
210
|
-
type: resolvedType,
|
|
211
|
-
frontmatter,
|
|
212
|
-
body,
|
|
213
|
-
};
|
|
214
|
-
|
|
215
|
-
const contents =
|
|
216
|
-
input.modeline !== undefined
|
|
217
|
-
? serializeConceptWithModeline(concept, input.modeline, { profile })
|
|
218
|
-
: serializeConcept(concept, { profile });
|
|
219
|
-
return { contents, type: resolvedType, warnings: warnings.list() };
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
/**
|
|
223
|
-
* The OKF `resource` value lore stamps for a concept of `type` at `docPath` under `profile`, or
|
|
224
|
-
* `undefined` when none should be stamped (LORE-47 / AC#4). The **single source** shared by the
|
|
225
|
-
* `lore new` write path ({@link stampResource}) and the `lore validate` drift check, so a value lore
|
|
226
|
-
* writes and a value it later checks for staleness are computed one way and can never disagree.
|
|
227
|
-
* Three guards keep the zero-config default **byte-identical to before the key existed** and stop
|
|
228
|
-
* lore writing a value its own profile would reject:
|
|
229
|
-
*
|
|
230
|
-
* - **`resourceBase` empty** (the default) → none. A project that sets no `[profile].resource_base`
|
|
231
|
-
* gets no `resource` line at all.
|
|
232
|
-
* - **the doc is an index** (`index.md`, root or sub-index) → none. Index/sub-index files are
|
|
233
|
-
* bundle-structure pages, not authored concepts a reader cites; a `resource` link on them would
|
|
234
|
-
* point a reader at scaffolding. The basename test covers `docs/index.md` and every
|
|
235
|
-
* `docs/<dir>/index.md` in one rule.
|
|
236
|
-
* - **the concept's own type owns an incompatible `resource` field** → none. If *this* type declares
|
|
237
|
-
* a `resource` field whose shape a URL string cannot satisfy (a `datetime`/`number`/`list` field,
|
|
238
|
-
* or a closed `enum`), the field is the profile's to fill, not lore's — auto-stamping a URL would
|
|
239
|
-
* fail that type's validator. The test is **per-type** ({@link import("./profile").CompiledType.acceptsStampedResource}),
|
|
240
|
-
* not the old global key-order union: one type declaring its own `resource` no longer suppresses
|
|
241
|
-
* stamping for every other type, and a `resource = { required = true }` *string* field is now
|
|
242
|
-
* **satisfied** by the stamp instead of making `lore new` fail with a missing-required error.
|
|
243
|
-
*
|
|
244
|
-
* Otherwise the value is {@link resourceFor}; `concept.ts` emits it as a recognized key and
|
|
245
|
-
* `schema.ts` keeps it from tripping the extra-key warning.
|
|
246
|
-
*/
|
|
247
|
-
export function expectedResource(type: string, docPath: string, profile: Profile): string | undefined {
|
|
248
|
-
if (profile.resourceBase === "" || posix.basename(docPath) === "index.md") {
|
|
249
|
-
return undefined;
|
|
250
|
-
}
|
|
251
|
-
if (profile.types.get(type)?.acceptsStampedResource === false) {
|
|
252
|
-
return undefined;
|
|
253
|
-
}
|
|
254
|
-
return resourceFor(profile.resourceBase, docPath);
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
/** Stamp {@link expectedResource} onto `frontmatter` in place when one is due, else leave it untouched. */
|
|
258
|
-
function stampResource(frontmatter: Record<string, unknown>, type: string, docPath: string, profile: Profile): void {
|
|
259
|
-
const value = expectedResource(type, docPath, profile);
|
|
260
|
-
if (value !== undefined) {
|
|
261
|
-
frontmatter.resource = value;
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
/** The placeholder names lore fills automatically; a `--var` for one of these is ignored (it would be overridden). */
|
|
266
|
-
const AUTO_TOKENS = ["type", "title", "timestamp", "summary"] as const;
|
|
267
|
-
|
|
268
|
-
/** Warn for each `--var` whose key shadows an auto token, so a discarded override is visible rather than silent. */
|
|
269
|
-
function warnShadowedVars(vars: Record<string, string>, path: string, warnings: WarningCollector): void {
|
|
270
|
-
for (const token of AUTO_TOKENS) {
|
|
271
|
-
if (Object.hasOwn(vars, token)) {
|
|
272
|
-
warnings.add(`ignoring --var ${token} in ${path}; \`${token}\` is set automatically by \`lore new\``);
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
/** Render the body template with the auto tokens layered over `--var` (autos win), failing loud on any unfilled token. */
|
|
278
|
-
function renderBody(input: BuildNewConceptInput): string {
|
|
279
|
-
const vars: Record<string, string> = Object.create(null);
|
|
280
|
-
for (const key of Object.keys(input.vars)) {
|
|
281
|
-
vars[key] = input.vars[key] as string;
|
|
282
|
-
}
|
|
283
|
-
vars.type = input.type;
|
|
284
|
-
vars.title = input.title;
|
|
285
|
-
vars.timestamp = input.timestamp;
|
|
286
|
-
vars.summary = input.summary;
|
|
287
|
-
|
|
288
|
-
const rendered = renderTemplate(input.bodyTemplate, vars);
|
|
289
|
-
if (rendered.unresolved.length > 0) {
|
|
290
|
-
const tokens = rendered.unresolved.map((key) => `{{${key}}}`).join(", ");
|
|
291
|
-
throw new LoreError(
|
|
292
|
-
"validation",
|
|
293
|
-
`template for ${input.docPath} has unfilled placeholder(s): ${tokens}`,
|
|
294
|
-
"supply each missing value with `--var key=value`",
|
|
295
|
-
{ path: input.docPath, placeholders: [...rendered.unresolved] },
|
|
296
|
-
);
|
|
297
|
-
}
|
|
298
|
-
return rendered.text;
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
/**
|
|
302
|
-
* The built-in **body** template for `type`: the matching {@link BUILTIN_TEMPLATES} entry for
|
|
303
|
-
* a known type, else the lenient {@link GENERIC_TEMPLATE} for a tolerated producer-extension
|
|
304
|
-
* type (cli-surface §new accepts unknown types). Always returns a usable body, so `lore new`
|
|
305
|
-
* has a fallback for every type when no user template is present.
|
|
306
|
-
*/
|
|
307
|
-
export function builtinTemplateFor(type: string): string {
|
|
308
|
-
return Object.hasOwn(BUILTIN_TEMPLATES, type) ? (BUILTIN_TEMPLATES[type] as string) : GENERIC_TEMPLATE;
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
// ── Built-in body templates ──────────────────────────────────────────────────────
|
|
312
|
-
//
|
|
313
|
-
// Each is the markdown **body** (everything after the frontmatter fence) for a type, using
|
|
314
|
-
// only the auto-filled tokens so `lore new <type> "<title>"` with no flags renders cleanly.
|
|
315
|
-
// Each leads with a blank line (the body begins right after the closing `---\n`, so the
|
|
316
|
-
// leading newline gives the heading its own line) and carries the type's conventional
|
|
317
|
-
// section skeleton. lore builds the frontmatter and splices the editor modeline itself —
|
|
318
|
-
// templates contain neither.
|
|
319
|
-
|
|
320
|
-
/** Reference: a documentation page — intro plus a place for detail. */
|
|
321
|
-
const REFERENCE_TEMPLATE = `
|
|
322
|
-
# {{title}}
|
|
323
|
-
|
|
324
|
-
Describe the subject of this reference here.
|
|
325
|
-
|
|
326
|
-
## Details
|
|
327
|
-
`;
|
|
328
|
-
|
|
329
|
-
/** Spec: a design/requirements document with the usual sections. */
|
|
330
|
-
const SPEC_TEMPLATE = `
|
|
331
|
-
# {{title}}
|
|
332
|
-
|
|
333
|
-
## Summary
|
|
334
|
-
|
|
335
|
-
## Requirements
|
|
336
|
-
|
|
337
|
-
## Design
|
|
338
|
-
|
|
339
|
-
## Open questions
|
|
340
|
-
`;
|
|
341
|
-
|
|
342
|
-
/** ADR: an architecture decision record, starting in the Proposed state. */
|
|
343
|
-
const ADR_TEMPLATE = `
|
|
344
|
-
# {{title}}
|
|
345
|
-
|
|
346
|
-
## Status
|
|
347
|
-
|
|
348
|
-
Proposed
|
|
349
|
-
|
|
350
|
-
## Context
|
|
351
|
-
|
|
352
|
-
## Decision
|
|
353
|
-
|
|
354
|
-
## Consequences
|
|
355
|
-
`;
|
|
356
|
-
|
|
357
|
-
/** Runbook: an operational procedure with prerequisites, steps, and rollback. */
|
|
358
|
-
const RUNBOOK_TEMPLATE = `
|
|
359
|
-
# {{title}}
|
|
360
|
-
|
|
361
|
-
## Purpose
|
|
362
|
-
|
|
363
|
-
## Prerequisites
|
|
364
|
-
|
|
365
|
-
## Steps
|
|
366
|
-
|
|
367
|
-
## Rollback
|
|
368
|
-
`;
|
|
369
|
-
|
|
370
|
-
/** Epic: a body of work that groups stories. */
|
|
371
|
-
const EPIC_TEMPLATE = `
|
|
372
|
-
# {{title}}
|
|
373
|
-
|
|
374
|
-
## Goal
|
|
375
|
-
|
|
376
|
-
## Scope
|
|
377
|
-
|
|
378
|
-
## Stories
|
|
379
|
-
`;
|
|
380
|
-
|
|
381
|
-
/**
|
|
382
|
-
* Story: a unit of deliverable work with acceptance criteria. Ships the empty
|
|
383
|
-
* `<!-- lore:tasks:begin -->`/`<!-- lore:tasks:end -->` managed block (LORE-59) so a
|
|
384
|
-
* freshly-created Story is immediately `lore sync`-able once linked to a task, with no
|
|
385
|
-
* hand-authored markup step — `lore sync`/`managed-block.ts` still fail loud (exit `6`) for
|
|
386
|
-
* any doc whose block is totally absent, e.g. one where the markers were hand-deleted.
|
|
387
|
-
*/
|
|
388
|
-
const STORY_TEMPLATE = `
|
|
389
|
-
# {{title}}
|
|
390
|
-
|
|
391
|
-
## Goal
|
|
392
|
-
|
|
393
|
-
## Acceptance criteria
|
|
394
|
-
|
|
395
|
-
## Tasks
|
|
396
|
-
|
|
397
|
-
<!-- lore:tasks:begin -->
|
|
398
|
-
<!-- lore:tasks:end -->
|
|
399
|
-
|
|
400
|
-
## Notes
|
|
401
|
-
`;
|
|
402
|
-
|
|
403
|
-
/**
|
|
404
|
-
* The built-in body template content lore ships for the six story-convention types — the
|
|
405
|
-
* zero-config fallback when no `.lore/templates/<type>.md` is present. Keyed by canonical type
|
|
406
|
-
* name (a plain string map, **independent of the active profile**): a custom-profile type lore
|
|
407
|
-
* ships no body for falls back to {@link GENERIC_TEMPLATE}, and the project supplies its own
|
|
408
|
-
* template file. Decoupling this from the profile is why {@link builtinTemplateFor} tests
|
|
409
|
-
* membership here rather than asking whether the type is profile-known.
|
|
410
|
-
*/
|
|
411
|
-
const BUILTIN_TEMPLATES: Readonly<Record<string, string>> = Object.freeze({
|
|
412
|
-
Reference: REFERENCE_TEMPLATE,
|
|
413
|
-
Spec: SPEC_TEMPLATE,
|
|
414
|
-
ADR: ADR_TEMPLATE,
|
|
415
|
-
Runbook: RUNBOOK_TEMPLATE,
|
|
416
|
-
Epic: EPIC_TEMPLATE,
|
|
417
|
-
Story: STORY_TEMPLATE,
|
|
418
|
-
});
|
|
419
|
-
|
|
420
|
-
/**
|
|
421
|
-
* The fallback body for an **unknown** (producer-extension) type — the lenient shape OKF
|
|
422
|
-
* tolerates (cli-surface §new). lore validates the concept against the unknown-type floor (a
|
|
423
|
-
* non-empty `type`) and writes it without an editor modeline (no schema exists for it).
|
|
424
|
-
*/
|
|
425
|
-
const GENERIC_TEMPLATE = `
|
|
426
|
-
# {{title}}
|
|
427
|
-
|
|
428
|
-
Describe this {{type}} here.
|
|
429
|
-
`;
|