@opum-ai/lore 0.1.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.
- package/LICENSE +21 -0
- package/README.md +306 -0
- package/bin/lore.cjs +109 -0
- package/package.json +67 -0
- package/src/adapters/backlog.ts +1084 -0
- package/src/adapters/git.ts +221 -0
- package/src/cli.ts +667 -0
- package/src/commands/agent.ts +301 -0
- package/src/commands/agents.ts +302 -0
- package/src/commands/args.ts +209 -0
- package/src/commands/changed.ts +70 -0
- package/src/commands/check.ts +1031 -0
- package/src/commands/codex-bridge.ts +49 -0
- package/src/commands/concurrency.ts +48 -0
- package/src/commands/context.ts +292 -0
- package/src/commands/discover.ts +89 -0
- package/src/commands/explorer.ts +253 -0
- package/src/commands/export.ts +93 -0
- package/src/commands/fswrite.ts +928 -0
- package/src/commands/graph.ts +291 -0
- package/src/commands/help.ts +151 -0
- package/src/commands/impact.ts +59 -0
- package/src/commands/init.ts +583 -0
- package/src/commands/instructions.ts +91 -0
- package/src/commands/link.ts +929 -0
- package/src/commands/new.ts +476 -0
- package/src/commands/orphans.ts +457 -0
- package/src/commands/path.ts +67 -0
- package/src/commands/provenance.ts +68 -0
- package/src/commands/query.ts +312 -0
- package/src/commands/reconcile-shared.ts +280 -0
- package/src/commands/rename.ts +585 -0
- package/src/commands/replace.ts +320 -0
- package/src/commands/scaffold.ts +346 -0
- package/src/commands/schema.ts +293 -0
- package/src/commands/snapshot.ts +130 -0
- package/src/commands/supersede.ts +400 -0
- package/src/commands/sync.ts +371 -0
- package/src/commands/tasks.ts +271 -0
- package/src/commands/traversal.ts +151 -0
- package/src/commands/validate.ts +226 -0
- package/src/config.ts +598 -0
- package/src/core/agent-bridge.ts +287 -0
- package/src/core/agent-context.ts +498 -0
- package/src/core/agent-profile.ts +447 -0
- package/src/core/bundle.ts +893 -0
- package/src/core/check.ts +853 -0
- package/src/core/codex-bridge.ts +100 -0
- package/src/core/concept.ts +597 -0
- package/src/core/consumer-scaffold.ts +433 -0
- package/src/core/context.ts +271 -0
- package/src/core/explorer-contract.ts +441 -0
- package/src/core/explorer-qualification.ts +58 -0
- package/src/core/explorer.ts +518 -0
- package/src/core/finding.ts +31 -0
- package/src/core/graph.ts +201 -0
- package/src/core/indexes.ts +436 -0
- package/src/core/instructions.ts +209 -0
- package/src/core/ladybug-driver.ts +1795 -0
- package/src/core/ladybug-lifecycle.ts +1178 -0
- package/src/core/ladybug-native.ts +95 -0
- package/src/core/ladybug-source.ts +667 -0
- package/src/core/links.ts +681 -0
- package/src/core/log.ts +253 -0
- package/src/core/managed-block.ts +540 -0
- package/src/core/manifest.ts +718 -0
- package/src/core/order.ts +13 -0
- package/src/core/profile.ts +1007 -0
- package/src/core/projection.ts +195 -0
- package/src/core/query.ts +542 -0
- package/src/core/reconcile.ts +236 -0
- package/src/core/replace.ts +419 -0
- package/src/core/retrieval.ts +213 -0
- package/src/core/rewrite.ts +940 -0
- package/src/core/scaffold.ts +255 -0
- package/src/core/schema.ts +366 -0
- package/src/core/snapshot-runtime.ts +52 -0
- package/src/core/snapshot-store.ts +287 -0
- package/src/core/snapshot.ts +711 -0
- package/src/core/template.ts +429 -0
- package/src/core/traversal.ts +487 -0
- package/src/core/validate.ts +517 -0
- package/src/core/workspace-contract.ts +473 -0
- package/src/core/workspace-projection.ts +365 -0
- package/src/core/workspace-retrieval.ts +196 -0
- package/src/core/workspace-source.ts +174 -0
- package/src/errors.ts +697 -0
- package/src/meta.ts +7 -0
- package/src/output.ts +589 -0
- package/src/scripts/upstream-backlog-watch.ts +288 -0
- package/src/state.ts +390 -0
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* scaffold.ts — the **pure** description of an empty, conformant OKF bundle.
|
|
3
|
+
*
|
|
4
|
+
* This module is the byte-source for `lore init` (lore-design §3.1): it computes,
|
|
5
|
+
* with no filesystem access whatsoever, the exact directories and file contents a
|
|
6
|
+
* fresh bundle needs — the `.lore/` state directory ([ADR-0013](../../docs/adr/0013-lore-state-directory.md)),
|
|
7
|
+
* the Draft-7 JSON Schemas exported from the Zod source of truth
|
|
8
|
+
* ([ADR-0006](../../docs/adr/0006-schema-types-templates.md)), and the reserved
|
|
9
|
+
* root `docs/index.md` that carries `okf_version`
|
|
10
|
+
* ([okf-conformance](../../docs/reference/okf-conformance.md)).
|
|
11
|
+
*
|
|
12
|
+
* Keeping it pure is what makes init testable and deterministic: {@link buildScaffold}
|
|
13
|
+
* returns a {@link ScaffoldPlan} of intended bytes that golden tests pin exactly, and
|
|
14
|
+
* `commands/init.ts` is the thin layer that applies the plan **idempotently** (writing
|
|
15
|
+
* only absent files, never clobbering) — the side effects live there, the bytes live
|
|
16
|
+
* here. The single non-deterministic input, the root index's `timestamp`, enters
|
|
17
|
+
* through an injected option (lore-design §8), so the same options always yield the
|
|
18
|
+
* same plan.
|
|
19
|
+
*
|
|
20
|
+
* Scope (confirmed for LORE-17): an empty bundle only. Full index/log *generation*
|
|
21
|
+
* (`indexes.generateIndexes`, `log.ts` → `log.md`) is M3 / `lore sync` and is **not** done here —
|
|
22
|
+
* init writes a *minimal* root index. Per-type template *content* is `lore new`'s
|
|
23
|
+
* concern (LORE-18, which carries built-in fallbacks); init only ensures the
|
|
24
|
+
* `.lore/templates/` directory exists.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { CONFIG_REL_PATH } from "../config";
|
|
28
|
+
import { type Concept, idFromPath, serializeConcept, serializeConceptWithModeline } from "./concept";
|
|
29
|
+
import { defaultProfile, PROFILE_REL_PATH, type Profile } from "./profile";
|
|
30
|
+
import { emitSchemaFiles, schemaModeline } from "./schema";
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The bundle root — the directory every concept lives under, relative to the repo root.
|
|
34
|
+
* The single source of truth for the `docs/` convention, shared by the scaffolder, the
|
|
35
|
+
* bundle walk, and `lore new`'s output-path computation so they never spell it differently.
|
|
36
|
+
*/
|
|
37
|
+
export const DOCS_DIR = "docs";
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The reserved bundle-root index — the only file that carries `okf_version`. Exported so
|
|
41
|
+
* `core/validate.ts` can recognize it as the one lore-owned reserved file whose frontmatter is
|
|
42
|
+
* always a concept (every other `RESERVED_STEMS` file — sub-directory `index.md`s, `log.md` — is
|
|
43
|
+
* frontmatter-free and so never reaches the profile-driven validator at all): `lore validate`
|
|
44
|
+
* must judge it under the same {@link defaultProfile} `serializeStructuralConcept` wrote it
|
|
45
|
+
* against, never the active project profile (LORE-144).
|
|
46
|
+
*/
|
|
47
|
+
export const ROOT_INDEX_PATH = `${DOCS_DIR}/index.md`;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Reserved file stems that name machine-generated hubs (`index.md`, `log.md`): regenerated
|
|
51
|
+
* wholesale by `lore sync`, never a rename/supersede/link/unlink principal. Shared by
|
|
52
|
+
* `commands/rename.ts`, `commands/supersede.ts`, and `commands/link.ts` — each guards its own
|
|
53
|
+
* concept-id argument(s) against it.
|
|
54
|
+
*/
|
|
55
|
+
export const RESERVED_STEMS: ReadonlySet<string> = new Set(["index", "log"]);
|
|
56
|
+
|
|
57
|
+
/** A single file the scaffold wants to exist, with the exact bytes to write when it is absent. */
|
|
58
|
+
export interface ScaffoldFile {
|
|
59
|
+
/** Repo-relative POSIX path. */
|
|
60
|
+
readonly path: string;
|
|
61
|
+
/** The exact bytes to write — pinned by golden tests, applied verbatim by the command. */
|
|
62
|
+
readonly contents: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* The complete, filesystem-free description of an empty bundle. `dirs` are ensured to
|
|
67
|
+
* exist (idempotent `mkdir -p`, in parent-first order); `files` are created only when
|
|
68
|
+
* absent so a re-run never clobbers user edits (the AC#2 idempotency contract).
|
|
69
|
+
*/
|
|
70
|
+
export interface ScaffoldPlan {
|
|
71
|
+
/** Directories to ensure exist, parents before children. */
|
|
72
|
+
readonly dirs: readonly string[];
|
|
73
|
+
/** Files to create if absent, in a stable order. */
|
|
74
|
+
readonly files: readonly ScaffoldFile[];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Options for {@link buildScaffold}; the timestamp is the one injected determinism seam. */
|
|
78
|
+
export interface ScaffoldOptions {
|
|
79
|
+
/**
|
|
80
|
+
* ISO-8601 datetime (e.g. `2026-06-25T12:00:00Z`) stamped on the generated root
|
|
81
|
+
* index. Injected by the command from its clock so the plan is deterministic and
|
|
82
|
+
* golden-testable (lore-design §8).
|
|
83
|
+
*/
|
|
84
|
+
readonly timestamp: string;
|
|
85
|
+
/**
|
|
86
|
+
* The active profile whose types drive the emitted JSON Schemas and whose `okfVersion` stamps
|
|
87
|
+
* the root index. Defaults to the built-in {@link defaultProfile}, so a zero-config `lore init`
|
|
88
|
+
* scaffolds the six story-convention schemas exactly as before.
|
|
89
|
+
*/
|
|
90
|
+
readonly profile?: Profile;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Build the {@link ScaffoldPlan} for an empty OKF bundle: the `.lore/` state tree, one exported
|
|
95
|
+
* JSON Schema per profile type, and the reserved root `docs/index.md`. Pure — it touches no
|
|
96
|
+
* filesystem and reads no clock; identical `options` always produce identical bytes.
|
|
97
|
+
*/
|
|
98
|
+
export function buildScaffold(options: ScaffoldOptions): ScaffoldPlan {
|
|
99
|
+
const profile = options.profile ?? defaultProfile();
|
|
100
|
+
return {
|
|
101
|
+
dirs: [".lore", ".lore/schemas", ".lore/templates", ".lore/cache", DOCS_DIR],
|
|
102
|
+
files: [
|
|
103
|
+
{ path: CONFIG_REL_PATH, contents: DEFAULT_CONFIG_TOML },
|
|
104
|
+
{ path: PROFILE_REL_PATH, contents: DEFAULT_PROFILE_TOML },
|
|
105
|
+
{ path: ".lore/.gitignore", contents: LORE_GITIGNORE },
|
|
106
|
+
...schemaFiles(profile),
|
|
107
|
+
// Materialize the templates directory without committing to per-type content:
|
|
108
|
+
// `lore new` (LORE-18) owns the template bodies and their override-if-present
|
|
109
|
+
// logic, so init only keeps the directory tracked.
|
|
110
|
+
{ path: ".lore/templates/.gitkeep", contents: "" },
|
|
111
|
+
{ path: ROOT_INDEX_PATH, contents: rootIndexDocument(options.timestamp, profile) },
|
|
112
|
+
],
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* One {@link ScaffoldFile} per profile type, each the generated Draft-7 JSON Schema pretty-printed
|
|
118
|
+
* with a trailing newline, in the profile's type-declaration order so the plan (and its golden) is
|
|
119
|
+
* stable. Delegates to the shared {@link emitSchemaFiles} (the byte contract `lore schema export`
|
|
120
|
+
* reuses), so a scaffolded schema and a re-exported one can never diverge.
|
|
121
|
+
*/
|
|
122
|
+
function schemaFiles(profile: Profile): ScaffoldFile[] {
|
|
123
|
+
return emitSchemaFiles(profile);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* The minimal reserved root index: byte-stable frontmatter (a `Reference` carrying
|
|
128
|
+
* `okf_version: "0.1"` — the sole carrier in the bundle), with the editor modeline
|
|
129
|
+
* spliced in as the **first line inside** the `---` fence via
|
|
130
|
+
* {@link serializeConceptWithModeline} (the shared placement seam in concept.ts).
|
|
131
|
+
*
|
|
132
|
+
* The modeline sits *inside* the frontmatter, not above it, because that is the only
|
|
133
|
+
* placement that both (a) lets lore read the file back as a concept — `parseConcept`
|
|
134
|
+
* needs `---` at byte 0, so an above-fence comment would make `loadBundle` skip the
|
|
135
|
+
* index as a non-concept — and (b) matches every modeline-bearing doc already in this
|
|
136
|
+
* bundle. (Trade-off: js-yaml drops the in-fence comment if the file is ever
|
|
137
|
+
* re-serialized — a documented round-trip limitation in concept.ts that applies to all
|
|
138
|
+
* such docs equally; `init` writes the index once and never rewrites it.)
|
|
139
|
+
*/
|
|
140
|
+
function rootIndexDocument(timestamp: string, profile: Profile): string {
|
|
141
|
+
const concept: Concept = {
|
|
142
|
+
id: idFromPath(ROOT_INDEX_PATH),
|
|
143
|
+
path: ROOT_INDEX_PATH,
|
|
144
|
+
type: ROOT_INDEX_TYPE,
|
|
145
|
+
frontmatter: {
|
|
146
|
+
type: ROOT_INDEX_TYPE,
|
|
147
|
+
title: "Documentation",
|
|
148
|
+
summary: "Root index of this OKF documentation bundle, created by `lore init`.",
|
|
149
|
+
timestamp,
|
|
150
|
+
okf_version: profile.okfVersion,
|
|
151
|
+
},
|
|
152
|
+
body: ROOT_INDEX_BODY,
|
|
153
|
+
};
|
|
154
|
+
// Only `okf_version` above is profile-derived; the serialization choice itself (structural
|
|
155
|
+
// profile, conditional modeline) is shared with every other reserved structural file — see
|
|
156
|
+
// {@link serializeStructuralConcept}.
|
|
157
|
+
return serializeStructuralConcept(concept, profile);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Serialize a lore-owned **reserved structural file** — one with a fixed shape that lore itself
|
|
162
|
+
* generates (the root `docs/index.md` here, `docs/tags.md` in `core/consumer-scaffold.ts`) — never
|
|
163
|
+
* a user concept. Always validated/serialized against the built-in {@link defaultProfile}, never the
|
|
164
|
+
* *active* one, so a custom profile that retypes the concept's `type` (e.g. adds a required field to
|
|
165
|
+
* `Reference`) can never make a scaffold command fail to write its own reserved file. The `$schema`
|
|
166
|
+
* editor modeline is carried only when the *active* `profile` defines `concept.type` — so the schema
|
|
167
|
+
* it points at was actually emitted under `.lore/schemas/` — via {@link serializeConceptWithModeline};
|
|
168
|
+
* otherwise a plain {@link serializeConcept} against the structural profile.
|
|
169
|
+
*/
|
|
170
|
+
export function serializeStructuralConcept(concept: Concept, profile: Profile): string {
|
|
171
|
+
const structural = defaultProfile();
|
|
172
|
+
return profile.types.has(concept.type)
|
|
173
|
+
? serializeConceptWithModeline(concept, schemaModeline(concept.path, concept.type), { profile: structural })
|
|
174
|
+
: serializeConcept(concept, { profile: structural });
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** The reserved root index's `type` — lore's bundle entry point is conventionally a `Reference`. */
|
|
178
|
+
const ROOT_INDEX_TYPE = "Reference";
|
|
179
|
+
|
|
180
|
+
/** The body of the scaffolded root index (after the frontmatter fence). */
|
|
181
|
+
const ROOT_INDEX_BODY = `
|
|
182
|
+
# Documentation
|
|
183
|
+
|
|
184
|
+
This is the root index of an OKF documentation bundle, created by \`lore init\`.
|
|
185
|
+
Add concepts under \`docs/\` and link them from here. This file is the bundle's
|
|
186
|
+
entry point and the only one that carries \`okf_version\`.
|
|
187
|
+
`;
|
|
188
|
+
|
|
189
|
+
/** The committed `.lore/.gitignore`: keep the transient cache out of git (ADR-0013). */
|
|
190
|
+
const LORE_GITIGNORE = `# lore transient cache — recomputable, machine-local, never committed (ADR-0013).
|
|
191
|
+
cache/
|
|
192
|
+
`;
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* The default, fully-commented `.lore/config.toml`. Every setting is shown commented
|
|
196
|
+
* because lore is zero-config: a missing file — or one with every line commented —
|
|
197
|
+
* yields the documented defaults (ADR-0013), so a fresh `init` produces a file that
|
|
198
|
+
* changes nothing until a team uncomments a knob. The committed-token guard in
|
|
199
|
+
* config.ts still applies; the Confluence token is environment-only.
|
|
200
|
+
*/
|
|
201
|
+
const DEFAULT_CONFIG_TOML = `# lore configuration — committed, team-shared knobs for \`lore sync\` / \`lore check\`.
|
|
202
|
+
# Every setting is OPTIONAL: with this file absent, or every line below commented,
|
|
203
|
+
# lore uses the documented defaults. See docs/adr/0013-lore-state-directory.md.
|
|
204
|
+
|
|
205
|
+
# [reconcile]
|
|
206
|
+
# Status roll-up policy applied by \`lore sync\` / \`lore check\`.
|
|
207
|
+
# mode = "task-rollup" # all tasks Done -> done; any In Progress -> in-progress; else todo
|
|
208
|
+
|
|
209
|
+
# [validate]
|
|
210
|
+
# external_links = false # external-link liveness is opt-in only
|
|
211
|
+
# promote_portability = false # keep the portability lint a warning, not an error
|
|
212
|
+
|
|
213
|
+
# [confluence]
|
|
214
|
+
# Non-secret publish target settings (the one-way publish adapter is deferred to v2).
|
|
215
|
+
# The API token is environment-only: set $LORE_CONFLUENCE_TOKEN, never store it here.
|
|
216
|
+
# base_url = "https://yourorg.atlassian.net/wiki"
|
|
217
|
+
# space = "ENG"
|
|
218
|
+
# parent_page_id = "98765"
|
|
219
|
+
# format = "storage" # or "adf"
|
|
220
|
+
`;
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* The default, fully-commented `.lore/profile.toml`. The profile is the declarative source of
|
|
224
|
+
* truth for the type vocabulary (ADR-0006): with this file absent — or every line below
|
|
225
|
+
* commented — lore uses the built-in story-convention profile (Epic/Story/Spec/ADR/Runbook/
|
|
226
|
+
* Reference), so a fresh `init` produces a file that changes nothing until a team defines its own
|
|
227
|
+
* types. It is separate from `config.toml`: config carries operational knobs, the profile carries
|
|
228
|
+
* the type system. See docs/adr/0006-schema-types-templates.md.
|
|
229
|
+
*/
|
|
230
|
+
const DEFAULT_PROFILE_TOML = `# lore profile — committed, declarative type vocabulary for this bundle.
|
|
231
|
+
# OPTIONAL: with this file absent, or every line below commented, lore uses the built-in
|
|
232
|
+
# story-convention profile (Epic/Story/Spec/ADR/Runbook/Reference). Fill it in to define your
|
|
233
|
+
# own types. lore generates its runtime validators + editor JSON Schemas from this file at load.
|
|
234
|
+
|
|
235
|
+
# [profile]
|
|
236
|
+
# name = "my-project" # required once any line below is uncommented
|
|
237
|
+
# okf_version = "0.1" # required; asserted against the bundle-root index.md
|
|
238
|
+
# case = "Title" # type-name casing convention (advisory; powers the did-you-mean hint)
|
|
239
|
+
# resource_base = "" # prefix for the stamped \`resource\` link (empty = none)
|
|
240
|
+
|
|
241
|
+
# [base.fields]
|
|
242
|
+
# Fields every type carries. \`type\` MUST be required (OKF's one hard requirement).
|
|
243
|
+
# type = { required = true }
|
|
244
|
+
# title = {}
|
|
245
|
+
# description = {}
|
|
246
|
+
# tags = { kind = "list" }
|
|
247
|
+
# summary = {}
|
|
248
|
+
# timestamp = { kind = "datetime" }
|
|
249
|
+
|
|
250
|
+
# [[types]]
|
|
251
|
+
# name = "Spec" # the OKF \`type\` value
|
|
252
|
+
# sections = ["Summary", "Design"] # required body headings (## …)
|
|
253
|
+
# template = "spec.md" # template under .lore/templates/
|
|
254
|
+
# fields = { feature = { required = true }, status = { enum = ["draft", "approved"] } }
|
|
255
|
+
`;
|
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* schema.ts — validate concept frontmatter against the **active profile**.
|
|
3
|
+
*
|
|
4
|
+
* Per [ADR-0006](../../docs/adr/0006-schema-types-templates.md) (inverted by LORE-46),
|
|
5
|
+
* the type vocabulary is no longer hand-authored Zod here: it is **data**, loaded and
|
|
6
|
+
* compiled by [profile.ts](./profile.ts) into a {@link Profile} whose per-type Zod
|
|
7
|
+
* validators and editor JSON Schemas are *generated* from a declarative
|
|
8
|
+
* `.lore/profile.toml`. This module is the **consumer** of that compiled profile: it runs
|
|
9
|
+
* the per-type validator, classifies findings into OKF's tiers, and owns the editor
|
|
10
|
+
* schema-file naming + modeline conventions. With no profile passed, every function
|
|
11
|
+
* defaults to the built-in story-convention {@link defaultProfile} — so a caller that
|
|
12
|
+
* does not opt into a custom profile sees exactly the behavior lore shipped before the
|
|
13
|
+
* profile existed.
|
|
14
|
+
*
|
|
15
|
+
* The validation tiers mirror OKF §9 + the active profile
|
|
16
|
+
* ([okf-conformance](../../docs/reference/okf-conformance.md) "How lore checks conformance"):
|
|
17
|
+
*
|
|
18
|
+
* - **ERROR** (throws a `validation` {@link LoreError}, exit 6): unparseable frontmatter
|
|
19
|
+
* (handled upstream in concept.ts), a missing/empty `type`, or a *known* field carrying
|
|
20
|
+
* the wrong type (e.g. `tags` that is not a list).
|
|
21
|
+
* - **WARNING** (recorded on a {@link WarningCollector}, never fatal): an unknown `type`
|
|
22
|
+
* (validated on `type` alone), an extra key on a *known* type, or a missing/over-long
|
|
23
|
+
* `summary`.
|
|
24
|
+
*
|
|
25
|
+
* Two deliberate departures from a naive reading of ADR-0006 are preserved by the
|
|
26
|
+
* generated profile (see [profile.ts](./profile.ts)): known schemas are **loose** (extra
|
|
27
|
+
* keys pass validation and are warned separately, preserving OKF tolerance), and
|
|
28
|
+
* validation **never rewrites** the data — {@link validateFrontmatter} only inspects the
|
|
29
|
+
* frontmatter and returns the resolved `type`, so byte-stable round-tripping
|
|
30
|
+
* ([ADR-0011](../../docs/adr/0011-frontmatter-serialization-stability.md)) is never
|
|
31
|
+
* compromised by coercion, key-stripping, or reordering. Dates stay **ISO strings**, never
|
|
32
|
+
* coerced to `Date` (ADR-0006 §2).
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
import { posix } from "node:path";
|
|
36
|
+
import type { z } from "zod";
|
|
37
|
+
import { LoreError, type WarningCollector } from "../errors";
|
|
38
|
+
import { type CompiledType, defaultProfile, type Profile, slugForTypeName } from "./profile";
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The bundle-root index's path, in the two conventions a caller of {@link validateFrontmatter} may
|
|
42
|
+
* pass for the identical physical file: **bundle-root-relative** (`"index.md"` — every
|
|
43
|
+
* {@link import("./bundle").loadBundle}-backed command (`sync`/`query`/`graph`/`link`/…) reaches
|
|
44
|
+
* this module through {@link import("./concept").parseConcept}/`tryParseConcept`, fed
|
|
45
|
+
* bundle-root-relative paths by `loadBundle`'s own walk) and **repo-relative**
|
|
46
|
+
* (`"docs/index.md"` — `core/validate.ts`'s `validateConceptText` threads this form instead; see
|
|
47
|
+
* its own `ROOT_INDEX_PATH`, LORE-144). Both name the one file `lore init` ever stamps
|
|
48
|
+
* `okf_version` onto, so both must be recognized as "the root" here. This module cannot import
|
|
49
|
+
* `scaffold.ts`'s `ROOT_INDEX_PATH` directly — `scaffold.ts` already imports from this module, and
|
|
50
|
+
* doing so would be a circular import — so the two literal spellings are pinned locally instead.
|
|
51
|
+
*/
|
|
52
|
+
const ROOT_INDEX_PATHS: ReadonlySet<string> = new Set(["index.md", "docs/index.md"]);
|
|
53
|
+
|
|
54
|
+
/** Whether `path` names the bundle-root index, under either convention {@link ROOT_INDEX_PATHS} pins. */
|
|
55
|
+
function isRootIndexPath(path: string | undefined): boolean {
|
|
56
|
+
return path !== undefined && ROOT_INDEX_PATHS.has(path);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Whether `key` is an OKF-reserved key that passes the extra-key warning on a known type. Both
|
|
61
|
+
* `resource` and `okf_version` are legitimate, lore-recognized fields — not stray producer
|
|
62
|
+
* extensions — so flagging either as unknown (as the generic extra-key check otherwise would)
|
|
63
|
+
* would be a false positive on lore's own conformant output, but each is reserved only in the
|
|
64
|
+
* ONE position lore itself ever writes it:
|
|
65
|
+
*
|
|
66
|
+
* - `resource` is the OKF-recommended canonical link `lore new` stamps from the profile's
|
|
67
|
+
* `resource_base` (LORE-47) onto an ordinary concept. **Index files are the exception**: lore
|
|
68
|
+
* never stamps `resource` on an `index.md` (it is a structure page, not a cited concept —
|
|
69
|
+
* LORE-47 AC#4/#5), so a `resource:` hand-authored onto one (`isIndex`) is not lore's
|
|
70
|
+
* recognized output and is warned like any other extra key.
|
|
71
|
+
* - `okf_version` is the bundle-root index's conformance marker (OKF §4) — a whole-bundle
|
|
72
|
+
* conformance property, not a per-concept one. Only the bundle-ROOT index
|
|
73
|
+
* ({@link isRootIndexPath}) — the one file `lore init`'s `serializeStructuralConcept` ever
|
|
74
|
+
* stamps it onto — is exempt. A hand-authored `okf_version` anywhere else (an ordinary
|
|
75
|
+
* concept, or a *sub*-index like `docs/adr/index.md`) is not lore's own output and is warned
|
|
76
|
+
* like any other extra key (LORE-168; previously this was unconditionally exempt everywhere,
|
|
77
|
+
* contradicting the conformance check `docs/reference/okf-conformance.md` documents).
|
|
78
|
+
*/
|
|
79
|
+
function isReservedKey(key: string, isIndex: boolean, isRootIndex: boolean): boolean {
|
|
80
|
+
if (key === "resource") {
|
|
81
|
+
return !isIndex;
|
|
82
|
+
}
|
|
83
|
+
if (key === "okf_version") {
|
|
84
|
+
return isRootIndex;
|
|
85
|
+
}
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** The longest a `summary` should be before lore warns it is no longer a one-liner (ADR-0006 §5). */
|
|
90
|
+
const SUMMARY_SOFT_LIMIT = 200;
|
|
91
|
+
|
|
92
|
+
/** Options for {@link validateFrontmatter}. */
|
|
93
|
+
export interface ValidateOptions {
|
|
94
|
+
/** Sink for advisory warnings (unknown type, extra keys, summary). Absent → warnings are dropped. */
|
|
95
|
+
warnings?: WarningCollector;
|
|
96
|
+
/** The concept's repo-relative path, woven into diagnostics so a finding names its file. */
|
|
97
|
+
path?: string;
|
|
98
|
+
/** The active profile to validate against; defaults to the built-in {@link defaultProfile}. */
|
|
99
|
+
profile?: Profile;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Narrow an arbitrary string to a type the `profile` (default: built-in) validates strictly-by-field. */
|
|
103
|
+
export function isKnownType(type: string, profile: Profile = defaultProfile()): boolean {
|
|
104
|
+
return profile.types.has(type);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Resolve a user-supplied `<type>` token to its canonical spelling in `profile`. `lore new`
|
|
109
|
+
* accepts a type case-insensitively (`story`, `ADR`, `reference`), so a token whose lower-case
|
|
110
|
+
* form names a profile type returns that type's canonical casing (`Story`, `ADR`, `Reference`) —
|
|
111
|
+
* the value lore writes to `type:` and keys its schema by. An **unknown** type is a tolerated OKF
|
|
112
|
+
* producer extension: it is returned **trimmed but otherwise verbatim** (the author's own casing
|
|
113
|
+
* preserved), never folded or rejected.
|
|
114
|
+
*/
|
|
115
|
+
export function canonicalType(input: string, profile: Profile = defaultProfile()): string {
|
|
116
|
+
const trimmed = input.trim();
|
|
117
|
+
return profile.byLowerName.get(trimmed.toLowerCase()) ?? trimmed;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* The **required body sections** (`##` headings) a concept of `type` must carry under `profile` —
|
|
122
|
+
* the per-type tier-2 section contract `lore validate` enforces as an **error** (ADR-0007). An
|
|
123
|
+
* unknown (producer-extension) type yields `[]` (OKF tolerance: lore never imposes a section shape
|
|
124
|
+
* on a type it does not own). The single source the validator reads.
|
|
125
|
+
*/
|
|
126
|
+
export function requiredSectionsFor(type: string, profile: Profile = defaultProfile()): readonly string[] {
|
|
127
|
+
return profile.types.get(type)?.requiredSections ?? [];
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* The bundle sub-directory each story-convention type's concepts conventionally live under,
|
|
132
|
+
* relative to the bundle root (`docs/`). These plural/acronym directory names are **not derivable**
|
|
133
|
+
* from the type by any single rule (the bundle uses the acronym `adr`, the singular `reference`, and
|
|
134
|
+
* the plurals `runbooks`/`specs`), so they are a lore built-in convenience map — independent of the
|
|
135
|
+
* profile, which (by its finalized grammar) carries no per-type directory. A type outside this map
|
|
136
|
+
* (a producer extension, or a custom-profile type) falls back to its LOWER-KEBAB slug.
|
|
137
|
+
*/
|
|
138
|
+
const TYPE_DIRECTORIES: Readonly<Record<string, string>> = Object.freeze({
|
|
139
|
+
Epic: "epics",
|
|
140
|
+
Story: "stories",
|
|
141
|
+
Spec: "specs",
|
|
142
|
+
ADR: "adr",
|
|
143
|
+
Runbook: "runbooks",
|
|
144
|
+
Reference: "reference",
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* The bundle sub-directory a concept of `type` is scaffolded into: the {@link TYPE_DIRECTORIES}
|
|
149
|
+
* convention for a story-convention type, else the type's {@link slugForTypeName LOWER-KEBAB slug}
|
|
150
|
+
* (so `lore new "QA Plan" …` lands under `docs/qa-plan/`, never `docs/qa plan/`). Using the slug —
|
|
151
|
+
* not a bare lower-case — keeps a multi-word/space-containing profile type from yielding an invalid
|
|
152
|
+
* path segment. Returns a path segment relative to the bundle root, never including `docs/` itself.
|
|
153
|
+
* A caller may always override the computed path (`lore new … --out`).
|
|
154
|
+
*/
|
|
155
|
+
export function typeDirectory(type: string): string {
|
|
156
|
+
return TYPE_DIRECTORIES[type] ?? slugForTypeName(type);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Validate a frontmatter object against `options.profile` (default: built-in), **throwing** on an
|
|
161
|
+
* error-tier problem and **recording** warning-tier ones on `options.warnings`. It never mutates
|
|
162
|
+
* `fm` — the caller keeps the verbatim object so byte-stable round-tripping is preserved (ADR-0011).
|
|
163
|
+
* It **returns the resolved type** (the `type` value, trimmed). Behavior by tier:
|
|
164
|
+
*
|
|
165
|
+
* - Missing/empty `type` → throw (`validation`). This is the OKF §9 floor.
|
|
166
|
+
* - Unknown `type` → warn; the type-only floor already passed, so nothing else is checked and
|
|
167
|
+
* every key is preserved (OKF tolerance).
|
|
168
|
+
* - Known `type` with a mistyped field → throw (`validation`) citing the field(s). A `type`
|
|
169
|
+
* carrying surrounding whitespace, or spelled in a different casing than the profile's
|
|
170
|
+
* canonical form (`story` for `Story`), classifies via {@link canonicalType} — so it is
|
|
171
|
+
* looked up and validated against that type's *real* schema — and then fails the schema's
|
|
172
|
+
* `type` literal check loudly here, rather than being silently demoted to an unvalidated
|
|
173
|
+
* unknown type. Only the lookup key is folded; `fm` itself is never rewritten (ADR-0011).
|
|
174
|
+
* - Known `type` with extra keys → one warning per extra key.
|
|
175
|
+
* - Missing or over-long (~{@link SUMMARY_SOFT_LIMIT}-char) `summary` → warn.
|
|
176
|
+
*/
|
|
177
|
+
export function validateFrontmatter(fm: Record<string, unknown>, options: ValidateOptions = {}): string {
|
|
178
|
+
const profile = options.profile ?? defaultProfile();
|
|
179
|
+
const where = options.path ? ` in ${options.path}` : "";
|
|
180
|
+
const type = requireType(fm, where, options.path);
|
|
181
|
+
|
|
182
|
+
const compiled = profile.types.get(canonicalType(type, profile));
|
|
183
|
+
if (compiled === undefined) {
|
|
184
|
+
// Unknown type: the non-empty-`type` floor (OKF §9) is already satisfied, so this is a
|
|
185
|
+
// tolerated producer extension — warn, validate nothing further, leave every key untouched.
|
|
186
|
+
options.warnings?.add(`unknown type "${type}"${where}; validated on \`type\` only`);
|
|
187
|
+
return type;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const result = compiled.schema.safeParse(fm);
|
|
191
|
+
if (!result.success) {
|
|
192
|
+
throw new LoreError(
|
|
193
|
+
"validation",
|
|
194
|
+
`invalid ${type} frontmatter${where}: ${describeIssues(result.error)}`,
|
|
195
|
+
"fix the field(s) named above to match the type's schema, or run `lore validate`",
|
|
196
|
+
{ path: options.path, type, issues: issueList(result.error) },
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const isIndex = options.path !== undefined && posix.basename(options.path) === "index.md";
|
|
201
|
+
const isRootIndex = isRootIndexPath(options.path);
|
|
202
|
+
warnExtraKeys(fm, compiled, where, options.warnings, isIndex, isRootIndex);
|
|
203
|
+
warnSummary(fm.summary, where, options.warnings);
|
|
204
|
+
return type;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Read a non-empty string `type` and return it **trimmed**, or throw the OKF §9 floor error.
|
|
209
|
+
* Trimming the return means a `type` with accidental surrounding whitespace classifies on its real
|
|
210
|
+
* value (so a known type is type-checked, not silently demoted to "unknown"); the frontmatter object
|
|
211
|
+
* keeps the verbatim value. `path` is echoed on the error's `input.path` so a consumer gets a usable path.
|
|
212
|
+
*/
|
|
213
|
+
function requireType(fm: Record<string, unknown>, where: string, path: string | undefined): string {
|
|
214
|
+
const type = fm.type;
|
|
215
|
+
if (type === undefined || type === null) {
|
|
216
|
+
throw new LoreError(
|
|
217
|
+
"validation",
|
|
218
|
+
`frontmatter${where} is missing a \`type\``,
|
|
219
|
+
"every concept needs a `type:` field (OKF §9); add one, e.g. `type: Reference`",
|
|
220
|
+
{ path },
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
if (typeof type !== "string" || type.trim() === "") {
|
|
224
|
+
throw new LoreError(
|
|
225
|
+
"validation",
|
|
226
|
+
`frontmatter${where} has an invalid \`type\` (must be a non-empty string)`,
|
|
227
|
+
'set `type` to a non-empty string; quote it if it looks like a number or date, e.g. `type: "2026"`',
|
|
228
|
+
{ path },
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
return type.trim();
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Warn for each key on a *known*-type concept that its profile type does not declare. Diffs the
|
|
236
|
+
* frontmatter's own keys against the compiled type's {@link CompiledType.declaredFields} (the
|
|
237
|
+
* generated schema is loose, so these extras validated fine) — the tier-3 OKF-tolerance warning,
|
|
238
|
+
* not an error.
|
|
239
|
+
*/
|
|
240
|
+
function warnExtraKeys(
|
|
241
|
+
fm: Record<string, unknown>,
|
|
242
|
+
compiled: CompiledType,
|
|
243
|
+
where: string,
|
|
244
|
+
warnings: WarningCollector | undefined,
|
|
245
|
+
isIndex: boolean,
|
|
246
|
+
isRootIndex: boolean,
|
|
247
|
+
): void {
|
|
248
|
+
if (warnings === undefined) {
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
for (const key of Object.getOwnPropertyNames(fm)) {
|
|
252
|
+
if (!compiled.declaredFields.has(key) && !isReservedKey(key, isIndex, isRootIndex)) {
|
|
253
|
+
warnings.add(`unknown key "${key}"${where}; preserved but not validated`);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/** Warn when `summary` is absent or runs well past the one-liner soft limit (ADR-0006 §5). */
|
|
259
|
+
function warnSummary(summary: unknown, where: string, warnings: WarningCollector | undefined): void {
|
|
260
|
+
if (warnings === undefined) {
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
if (summary === undefined || summary === null) {
|
|
264
|
+
warnings.add(`missing \`summary\`${where}; add a one-line summary for indexes and query snippets`);
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
if (typeof summary === "string") {
|
|
268
|
+
const codePointLength = [...summary].length;
|
|
269
|
+
if (codePointLength > SUMMARY_SOFT_LIMIT) {
|
|
270
|
+
warnings.add(
|
|
271
|
+
`\`summary\`${where} is ${codePointLength} chars; keep it under ~${SUMMARY_SOFT_LIMIT} (one sentence)`,
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Project one Zod issue onto its `{ path, message }` pair — the single source of the dotted-path
|
|
279
|
+
* rendering, so the human message and the JSON envelope's `input.issues` can never spell a nested
|
|
280
|
+
* path two different ways if Zod's issue shape changes.
|
|
281
|
+
*/
|
|
282
|
+
function projectIssue(issue: z.core.$ZodIssue): { path: string; message: string } {
|
|
283
|
+
return { path: issue.path.join("."), message: issue.message };
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/** Flatten Zod issues to a single-line, human "field: reason; field: reason" string. */
|
|
287
|
+
function describeIssues(error: z.ZodError): string {
|
|
288
|
+
return error.issues
|
|
289
|
+
.map((issue) => {
|
|
290
|
+
const { path, message } = projectIssue(issue);
|
|
291
|
+
return `${path || "(root)"}: ${message}`;
|
|
292
|
+
})
|
|
293
|
+
.join("; ");
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/** Project Zod issues onto a plain, JSON-safe array for the error envelope's `input`. */
|
|
297
|
+
function issueList(error: z.ZodError): Array<{ path: string; message: string }> {
|
|
298
|
+
return error.issues.map(projectIssue);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// ── JSON Schema emission + editor modeline (ADR-0006 §3) ───────────────────────
|
|
302
|
+
//
|
|
303
|
+
// The generated profile already carries each type's Draft-7 JSON Schema
|
|
304
|
+
// ({@link CompiledType.jsonSchema}); `lore init` writes one per type under `.lore/schemas/`.
|
|
305
|
+
// This module owns the **filename + modeline conventions** that bind a doc to its schema, kept
|
|
306
|
+
// here beside the validator so the emitter, the modeline writer, and the runtime check can never
|
|
307
|
+
// disagree about what a type's schema is called or where it lives.
|
|
308
|
+
|
|
309
|
+
/** Where lore writes the emitted JSON Schemas, relative to the repo root (ADR-0013). */
|
|
310
|
+
export const SCHEMAS_DIR = ".lore/schemas";
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* The on-disk filename for a type's JSON Schema: its LOWER-KEBAB slug plus `.schema.json`
|
|
314
|
+
* (`Reference` → `reference.schema.json`, `QA Plan` → `qa-plan.schema.json` — AC#7). For the
|
|
315
|
+
* single-word story-convention types the slug equals the lower-cased name, so the filenames are
|
|
316
|
+
* unchanged from before the profile existed (the committed modelines in this bundle still resolve).
|
|
317
|
+
* The single source of the convention, shared by the scaffolder and {@link schemaModeline}.
|
|
318
|
+
*/
|
|
319
|
+
export function schemaFileName(type: string): string {
|
|
320
|
+
return `${slugForTypeName(type)}.schema.json`;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* The editor modeline for a concept at `docPath` of `type` — the comment
|
|
325
|
+
* `# yaml-language-server: $schema=<relative path to .lore/schemas/<slug>.schema.json>` (ADR-0006 §3).
|
|
326
|
+
* The `$schema` path is computed **relative to the document's own directory** with POSIX separators,
|
|
327
|
+
* so it resolves identically on every consumer regardless of how deep the doc sits. Pure and
|
|
328
|
+
* filesystem-free.
|
|
329
|
+
*/
|
|
330
|
+
export function schemaModeline(docPath: string, type: string): string {
|
|
331
|
+
const relDir = posix.relative(posix.dirname(docPath), SCHEMAS_DIR);
|
|
332
|
+
return `# yaml-language-server: $schema=${posix.join(relDir, schemaFileName(type))}`;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/** A single emitted JSON Schema file: its `<dir>`-relative path and the exact bytes to write. */
|
|
336
|
+
export interface SchemaFile {
|
|
337
|
+
/** `<dir>/<slug>.schema.json`, POSIX-joined (`.lore/schemas/reference.schema.json`). */
|
|
338
|
+
readonly path: string;
|
|
339
|
+
/** The type's Draft-7 JSON Schema, two-space pretty-printed with one trailing newline (the byte contract). */
|
|
340
|
+
readonly contents: string;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/** Options for {@link emitSchemaFiles}. */
|
|
344
|
+
export interface EmitSchemaFilesOptions {
|
|
345
|
+
/** Directory the files are placed under (default {@link SCHEMAS_DIR}). */
|
|
346
|
+
readonly dir?: string;
|
|
347
|
+
/** Emit only this one compiled type; default: every type in the profile, in declaration order. */
|
|
348
|
+
readonly only?: CompiledType;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* The emitted JSON Schema files for a `profile` — one {@link SchemaFile} per type (or just
|
|
353
|
+
* `options.only`), each the type's generated Draft-7 JSON Schema pretty-printed with a two-space
|
|
354
|
+
* indent and a single trailing newline (the committed byte contract), placed under `options.dir`.
|
|
355
|
+
* Pure and deterministic: the same profile always yields the same files in the same order. This is
|
|
356
|
+
* the **single source** `lore init` ({@link import("./scaffold").buildScaffold}) and
|
|
357
|
+
* `lore schema export` share, so a scaffolded schema file and a re-exported one are byte-identical.
|
|
358
|
+
*/
|
|
359
|
+
export function emitSchemaFiles(profile: Profile, options: EmitSchemaFilesOptions = {}): SchemaFile[] {
|
|
360
|
+
const dir = options.dir ?? SCHEMAS_DIR;
|
|
361
|
+
const types = options.only ? [options.only] : [...profile.types.values()];
|
|
362
|
+
return types.map((type) => ({
|
|
363
|
+
path: posix.join(dir, schemaFileName(type.name)),
|
|
364
|
+
contents: `${JSON.stringify(type.jsonSchema, null, 2)}\n`,
|
|
365
|
+
}));
|
|
366
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/** Source-loading boundary shared by snapshot/change/provenance commands. */
|
|
2
|
+
|
|
3
|
+
import type { BacklogAdapter } from "../adapters/backlog";
|
|
4
|
+
import type { WarningCollector } from "../errors";
|
|
5
|
+
import { EXPECTED_LADYBUG_STORAGE_VERSION, EXPECTED_LADYBUG_VERSION } from "./ladybug-native";
|
|
6
|
+
import { loadLadybugProjectionSource } from "./ladybug-source";
|
|
7
|
+
import { buildRepositoryRetainedSnapshot, buildWorkspaceRetainedSnapshot, type RetainedSnapshot } from "./snapshot";
|
|
8
|
+
import type { SnapshotScopeSelection } from "./snapshot-store";
|
|
9
|
+
import { workspaceKeyForId } from "./workspace-contract";
|
|
10
|
+
import { loadWorkspaceProjection } from "./workspace-source";
|
|
11
|
+
|
|
12
|
+
export interface SnapshotRuntimeSelection {
|
|
13
|
+
readonly workspace?: string;
|
|
14
|
+
readonly workspaceId?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function loadCurrentRetainedSnapshot(options: {
|
|
18
|
+
readonly root: string;
|
|
19
|
+
readonly selection: SnapshotRuntimeSelection;
|
|
20
|
+
readonly warnings?: WarningCollector;
|
|
21
|
+
readonly adapter?: BacklogAdapter;
|
|
22
|
+
}): Promise<RetainedSnapshot> {
|
|
23
|
+
if (options.selection.workspace !== undefined) {
|
|
24
|
+
const loaded = await loadWorkspaceProjection({
|
|
25
|
+
root: options.root,
|
|
26
|
+
manifestPath: options.selection.workspace,
|
|
27
|
+
warnings: options.warnings,
|
|
28
|
+
});
|
|
29
|
+
return buildWorkspaceRetainedSnapshot(loaded.projection);
|
|
30
|
+
}
|
|
31
|
+
const source = await loadLadybugProjectionSource({
|
|
32
|
+
root: options.root,
|
|
33
|
+
ladybugVersion: EXPECTED_LADYBUG_VERSION,
|
|
34
|
+
ladybugStorageVersion: EXPECTED_LADYBUG_STORAGE_VERSION,
|
|
35
|
+
adapter: options.adapter,
|
|
36
|
+
warnings: options.warnings,
|
|
37
|
+
});
|
|
38
|
+
return buildRepositoryRetainedSnapshot(source);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function resolveSnapshotScope(options: {
|
|
42
|
+
readonly root: string;
|
|
43
|
+
readonly selection: SnapshotRuntimeSelection;
|
|
44
|
+
readonly warnings?: WarningCollector;
|
|
45
|
+
readonly adapter?: BacklogAdapter;
|
|
46
|
+
}): Promise<SnapshotScopeSelection> {
|
|
47
|
+
if (options.selection.workspaceId !== undefined) {
|
|
48
|
+
return { kind: "workspace", scopeKey: workspaceKeyForId(options.selection.workspaceId) };
|
|
49
|
+
}
|
|
50
|
+
const current = await loadCurrentRetainedSnapshot(options);
|
|
51
|
+
return { kind: current.scopeKind, scopeKey: current.scopeKey };
|
|
52
|
+
}
|