@opum-ai/lore 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/README.md +33 -22
  2. package/bin/lore.cjs +35 -7
  3. package/package.json +17 -17
  4. package/src/adapters/backlog.ts +0 -1084
  5. package/src/adapters/git.ts +0 -221
  6. package/src/cli.ts +0 -667
  7. package/src/commands/agent.ts +0 -301
  8. package/src/commands/agents.ts +0 -302
  9. package/src/commands/args.ts +0 -209
  10. package/src/commands/changed.ts +0 -70
  11. package/src/commands/check.ts +0 -1031
  12. package/src/commands/codex-bridge.ts +0 -49
  13. package/src/commands/concurrency.ts +0 -48
  14. package/src/commands/context.ts +0 -292
  15. package/src/commands/discover.ts +0 -89
  16. package/src/commands/explorer.ts +0 -253
  17. package/src/commands/export.ts +0 -93
  18. package/src/commands/fswrite.ts +0 -928
  19. package/src/commands/graph.ts +0 -291
  20. package/src/commands/help.ts +0 -151
  21. package/src/commands/impact.ts +0 -59
  22. package/src/commands/init.ts +0 -583
  23. package/src/commands/instructions.ts +0 -91
  24. package/src/commands/link.ts +0 -929
  25. package/src/commands/new.ts +0 -476
  26. package/src/commands/orphans.ts +0 -457
  27. package/src/commands/path.ts +0 -67
  28. package/src/commands/provenance.ts +0 -68
  29. package/src/commands/query.ts +0 -312
  30. package/src/commands/reconcile-shared.ts +0 -280
  31. package/src/commands/rename.ts +0 -585
  32. package/src/commands/replace.ts +0 -320
  33. package/src/commands/scaffold.ts +0 -346
  34. package/src/commands/schema.ts +0 -293
  35. package/src/commands/snapshot.ts +0 -130
  36. package/src/commands/supersede.ts +0 -400
  37. package/src/commands/sync.ts +0 -371
  38. package/src/commands/tasks.ts +0 -271
  39. package/src/commands/traversal.ts +0 -151
  40. package/src/commands/validate.ts +0 -226
  41. package/src/config.ts +0 -598
  42. package/src/core/agent-bridge.ts +0 -287
  43. package/src/core/agent-context.ts +0 -498
  44. package/src/core/agent-profile.ts +0 -447
  45. package/src/core/bundle.ts +0 -893
  46. package/src/core/check.ts +0 -853
  47. package/src/core/codex-bridge.ts +0 -100
  48. package/src/core/concept.ts +0 -597
  49. package/src/core/consumer-scaffold.ts +0 -433
  50. package/src/core/context.ts +0 -271
  51. package/src/core/explorer-contract.ts +0 -441
  52. package/src/core/explorer-qualification.ts +0 -58
  53. package/src/core/explorer.ts +0 -518
  54. package/src/core/finding.ts +0 -31
  55. package/src/core/graph.ts +0 -201
  56. package/src/core/indexes.ts +0 -436
  57. package/src/core/instructions.ts +0 -209
  58. package/src/core/ladybug-driver.ts +0 -1795
  59. package/src/core/ladybug-lifecycle.ts +0 -1178
  60. package/src/core/ladybug-native.ts +0 -95
  61. package/src/core/ladybug-source.ts +0 -667
  62. package/src/core/links.ts +0 -681
  63. package/src/core/log.ts +0 -253
  64. package/src/core/managed-block.ts +0 -540
  65. package/src/core/manifest.ts +0 -718
  66. package/src/core/order.ts +0 -13
  67. package/src/core/profile.ts +0 -1007
  68. package/src/core/projection.ts +0 -195
  69. package/src/core/query.ts +0 -542
  70. package/src/core/reconcile.ts +0 -236
  71. package/src/core/replace.ts +0 -419
  72. package/src/core/retrieval.ts +0 -213
  73. package/src/core/rewrite.ts +0 -940
  74. package/src/core/scaffold.ts +0 -255
  75. package/src/core/schema.ts +0 -366
  76. package/src/core/snapshot-runtime.ts +0 -52
  77. package/src/core/snapshot-store.ts +0 -287
  78. package/src/core/snapshot.ts +0 -711
  79. package/src/core/template.ts +0 -429
  80. package/src/core/traversal.ts +0 -487
  81. package/src/core/validate.ts +0 -517
  82. package/src/core/workspace-contract.ts +0 -473
  83. package/src/core/workspace-projection.ts +0 -365
  84. package/src/core/workspace-retrieval.ts +0 -196
  85. package/src/core/workspace-source.ts +0 -174
  86. package/src/errors.ts +0 -697
  87. package/src/meta.ts +0 -7
  88. package/src/output.ts +0 -589
  89. package/src/scripts/upstream-backlog-watch.ts +0 -288
  90. package/src/state.ts +0 -390
@@ -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
- `;