daftari 1.16.0 → 1.17.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.
Files changed (65) hide show
  1. package/CHANGELOG.md +73 -0
  2. package/dist/backfill/apply.d.ts +14 -0
  3. package/dist/backfill/apply.d.ts.map +1 -0
  4. package/dist/backfill/apply.js +111 -0
  5. package/dist/backfill/apply.js.map +1 -0
  6. package/dist/backfill/derive.d.ts +25 -0
  7. package/dist/backfill/derive.d.ts.map +1 -0
  8. package/dist/backfill/derive.js +142 -0
  9. package/dist/backfill/derive.js.map +1 -0
  10. package/dist/backfill/index.d.ts +2 -0
  11. package/dist/backfill/index.d.ts.map +1 -0
  12. package/dist/backfill/index.js +232 -0
  13. package/dist/backfill/index.js.map +1 -0
  14. package/dist/backfill/plan.d.ts +19 -0
  15. package/dist/backfill/plan.d.ts.map +1 -0
  16. package/dist/backfill/plan.js +157 -0
  17. package/dist/backfill/plan.js.map +1 -0
  18. package/dist/backfill/types.d.ts +19 -0
  19. package/dist/backfill/types.d.ts.map +1 -0
  20. package/dist/backfill/types.js +10 -0
  21. package/dist/backfill/types.js.map +1 -0
  22. package/dist/cli.d.ts.map +1 -1
  23. package/dist/cli.js +9 -0
  24. package/dist/cli.js.map +1 -1
  25. package/dist/curation/lint.d.ts +3 -0
  26. package/dist/curation/lint.d.ts.map +1 -1
  27. package/dist/curation/lint.js +5 -0
  28. package/dist/curation/lint.js.map +1 -1
  29. package/dist/curation/staged-actions.d.ts +68 -0
  30. package/dist/curation/staged-actions.d.ts.map +1 -0
  31. package/dist/curation/staged-actions.js +394 -0
  32. package/dist/curation/staged-actions.js.map +1 -0
  33. package/dist/index.d.ts.map +1 -1
  34. package/dist/index.js +11 -1
  35. package/dist/index.js.map +1 -1
  36. package/dist/search/reindex.d.ts.map +1 -1
  37. package/dist/search/reindex.js +6 -0
  38. package/dist/search/reindex.js.map +1 -1
  39. package/dist/server.d.ts.map +1 -1
  40. package/dist/server.js +2 -0
  41. package/dist/server.js.map +1 -1
  42. package/dist/storage/index-db.d.ts +19 -0
  43. package/dist/storage/index-db.d.ts.map +1 -1
  44. package/dist/storage/index-db.js +56 -0
  45. package/dist/storage/index-db.js.map +1 -1
  46. package/dist/tools/curation.d.ts +2 -1
  47. package/dist/tools/curation.d.ts.map +1 -1
  48. package/dist/tools/curation.js +18 -4
  49. package/dist/tools/curation.js.map +1 -1
  50. package/dist/tools/staged-actions.d.ts +18 -0
  51. package/dist/tools/staged-actions.d.ts.map +1 -0
  52. package/dist/tools/staged-actions.js +275 -0
  53. package/dist/tools/staged-actions.js.map +1 -0
  54. package/dist/tools/write.d.ts.map +1 -1
  55. package/dist/tools/write.js +50 -2
  56. package/dist/tools/write.js.map +1 -1
  57. package/dist/utils/config.d.ts +1 -0
  58. package/dist/utils/config.d.ts.map +1 -1
  59. package/dist/utils/config.js +32 -0
  60. package/dist/utils/config.js.map +1 -1
  61. package/dist/utils/git.d.ts +6 -0
  62. package/dist/utils/git.d.ts.map +1 -1
  63. package/dist/utils/git.js +34 -0
  64. package/dist/utils/git.js.map +1 -1
  65. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -7,6 +7,79 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.17.1] - 2026-06-08
11
+
12
+ ### Fixed
13
+
14
+ - **Frontmatter writes are now non-destructive** ([#113]). A tool-mediated write
15
+ no longer silently drops frontmatter fields that are absent from the write
16
+ payload. Two parts: (1) `serializeDocument` preserves any field a document
17
+ already carries that is neither built-in nor a declared schema extension —
18
+ undeclared custom fields now round-trip untyped instead of being stripped,
19
+ which fixes the same loss on `vault_append` / `vault_promote` / `vault_deprecate`
20
+ and `daftari backfill` (the path that dropped fields across 197 files in a
21
+ single run); and (2) on the `vault_write` update path, the document's existing
22
+ frontmatter is merged under the payload — every existing field (built-in,
23
+ declared extension, or undeclared) is preserved, the payload wins per key, and
24
+ an explicit `null` in the payload removes a key (opt-in deletion). The create
25
+ path is unchanged. This makes the 1.17.0 changelog claim that existing
26
+ frontmatter is "preserved field-by-field" actually hold. Critical-priority:
27
+ 1.17.0 shipped with this bug.
28
+
29
+ [#113]: https://github.com/mavaali/daftari/issues/113
30
+
31
+ ## [1.17.0] - 2026-06-07
32
+
33
+ ### Added
34
+
35
+ - **`daftari backfill` git-driven frontmatter migration** (cortex consolidation
36
+ loop §11.1). A CLI command that adopts an existing wiki into Daftari without a
37
+ manual migration sprint: it walks the vault, derives frontmatter defaults
38
+ deterministically (no LLM calls) from git history and body conventions, and
39
+ writes them per-folder on human ratification. Two-step plan/apply:
40
+ `daftari backfill --plan [--scope <folder>]` derives proposals and stages them
41
+ to `.daftari/backfill-plan.jsonl` (modifying no markdown), and
42
+ `daftari backfill --apply --scope <folder> [--yes]` writes the proposals for
43
+ one folder and commits them in a single commit (honoring the vault's
44
+ `auto_commit` setting — with `auto_commit: false` the files are written but
45
+ the caller owns git, matching the other write tools). `--scope` is required on
46
+ apply so a whole-vault write can never happen by accident. Derivation: `title`
47
+ from the first H1 (else the filename), `created`/`updated`/`updated_by` from
48
+ git (`--diff-filter=A` first-add, last-commit, author through an optional
49
+ `backfill.identity_map` in `.daftari/config.yaml`), `collection` from the
50
+ parent folder, and `status: canonical` / `confidence: medium` /
51
+ `provenance: direct` / `domain: accumulation` defaults — explicitly suggested,
52
+ ratified by a human, never asserted. Existing frontmatter is preserved
53
+ field-by-field; a doc whose frontmatter already validates is reported
54
+ conformant and skipped. The plan is transient: backfill never stages or
55
+ commits it (apply stages only the doc paths), the apply commit is the durable
56
+ audit trail, and `.daftari/backfill-plan.jsonl` is added to the `daftari
57
+ --init` .gitignore template (a `--plan` run also prints a reminder to gitignore
58
+ it on wikis not scaffolded by Daftari). CLI-only for v1 — no MCP tool. See
59
+ [docs/superpowers/specs/2026-06-06-cortex-consolidation-loop-design-direction.md](docs/superpowers/specs/2026-06-06-cortex-consolidation-loop-design-direction.md)
60
+ §11.1.
61
+ - **Staged-action queue + `vault_ratify`** (cortex loop §11.2). A persistent
62
+ queue of proposed vault changes awaiting human ratification — the foundation
63
+ for the consolidation loop's "always-stage" tier. Two new MCP tools:
64
+ `vault_stage_action` (producer; normally the curation loop, exposed for
65
+ testing and future callers) records a proposed `promote` / `deprecate` /
66
+ `supersede` / `merge` / `confidence-up` action with a rationale, a proposed
67
+ diff, and a TTL (default 14 days); `vault_ratify` (consumer) lets a human
68
+ `approve` or `reject` one pending action. On approve, it dispatches to the
69
+ existing write path — `promote` → `vault_promote`, `deprecate` →
70
+ `vault_deprecate` (both auto-commit). `supersede` / `merge` / `confidence-up`
71
+ are staged only in v1 (their write tools are deferred to §11.4); approving
72
+ one returns `applied: false` with `deferred_to: "§11.4"` and a
73
+ `ratified-pending-tool` status. Storage mirrors the rest of Daftari: an
74
+ append-only canonical log at `.daftari/staged-actions.jsonl` (the source of
75
+ truth) plus a derived `staged_actions` table in the ephemeral
76
+ `.daftari/index.db`, rebuilt from the jsonl on reindex and startup.
77
+ `vault_lint` gains a "Staged actions" section listing pending actions
78
+ soonest-to-expire first, and expires actions past their TTL as a housekeeping
79
+ sweep on each invocation. See
80
+ [docs/superpowers/specs/2026-06-06-cortex-consolidation-loop-design-direction.md](docs/superpowers/specs/2026-06-06-cortex-consolidation-loop-design-direction.md)
81
+ §11.2.
82
+
10
83
  ## [1.16.0] - 2026-06-02
11
84
 
12
85
  ### Added
@@ -0,0 +1,14 @@
1
+ import { type Result } from "../frontmatter/types.js";
2
+ export interface SkippedDoc {
3
+ path: string;
4
+ reason: string;
5
+ }
6
+ export interface ApplyResult {
7
+ scope: string;
8
+ applied: string[];
9
+ unchanged: string[];
10
+ skipped: SkippedDoc[];
11
+ commit: string | null;
12
+ }
13
+ export declare function applyPlan(vaultRoot: string, scope: string, agent: string): Promise<Result<ApplyResult, Error>>;
14
+ //# sourceMappingURL=apply.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"apply.d.ts","sourceRoot":"","sources":["../../src/backfill/apply.ts"],"names":[],"mappings":"AAkBA,OAAO,EAAM,KAAK,MAAM,EAAE,MAAM,yBAAyB,CAAC;AAU1D,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAC;IAEd,OAAO,EAAE,MAAM,EAAE,CAAC;IAElB,SAAS,EAAE,MAAM,EAAE,CAAC;IAEpB,OAAO,EAAE,UAAU,EAAE,CAAC;IAEtB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;CACvB;AAkCD,wBAAsB,SAAS,CAC7B,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC,MAAM,CAAC,WAAW,EAAE,KAAK,CAAC,CAAC,CAwErC"}
@@ -0,0 +1,111 @@
1
+ // Plan application for `daftari backfill` (§11.1).
2
+ //
3
+ // `applyPlan` reads the plan, takes only the entries under the given scope
4
+ // (per-folder ratification), and writes each doc's proposed frontmatter through
5
+ // the same primitives the write tools use: validate, serialize, then one git
6
+ // commit for the whole scope. The body is taken from the file on disk at apply
7
+ // time, so a body edited between plan and apply is preserved; only frontmatter
8
+ // is filled.
9
+ //
10
+ // --scope is required (the CLI enforces it) so a whole-vault write can never
11
+ // happen by accident. The run is idempotent: a doc whose on-disk content
12
+ // already equals the proposed serialization is left untouched, so re-applying
13
+ // an already-applied folder is a no-op (and never produces an empty commit).
14
+ import { writeFile } from "node:fs/promises";
15
+ import { recordProvenance } from "../curation/provenance.js";
16
+ import { parseDocument } from "../frontmatter/parser.js";
17
+ import { validateFrontmatter } from "../frontmatter/schema.js";
18
+ import { ok } from "../frontmatter/types.js";
19
+ import { readFile, resolveVaultPath } from "../storage/local.js";
20
+ import { serializeDocument } from "../tools/write.js";
21
+ import { loadConfig } from "../utils/config.js";
22
+ import { commit } from "../utils/git.js";
23
+ import { planPath, readPlan } from "./plan.js";
24
+ // Serializes one plan entry against the file's current body. Returns the new
25
+ // file text, or a reason it cannot be written.
26
+ function renderEntry(entry, currentText, extensions) {
27
+ const parsed = parseDocument(currentText);
28
+ if (!parsed.ok)
29
+ return parsed;
30
+ // Guard: never write frontmatter the validator would reject. A non-conformant
31
+ // doc whose *present* field is itself malformed (so preservation carries the
32
+ // bad value through) is reported, not written.
33
+ const { report } = validateFrontmatter(entry.proposed);
34
+ if (!report.valid) {
35
+ const summary = report.issues.map((i) => `${i.field}: ${i.message}`).join("; ");
36
+ return { ok: false, error: new Error(`proposed frontmatter is invalid: ${summary}`) };
37
+ }
38
+ // Preserve any config-extension fields present on disk by passing the current
39
+ // raw frontmatter through to the serializer.
40
+ const text = serializeDocument(entry.proposed, parsed.value.content, extensions, parsed.value.raw);
41
+ return ok(text);
42
+ }
43
+ // Applies all plan entries under `scope`. Writes only changed docs and commits
44
+ // them in a single commit authored by `agent`.
45
+ export async function applyPlan(vaultRoot, scope, agent) {
46
+ const plan = await readPlan(planPath(vaultRoot));
47
+ if (!plan.ok)
48
+ return plan;
49
+ const config = loadConfig(vaultRoot);
50
+ if (!config.ok)
51
+ return config;
52
+ const extensions = config.value.schemaExtensions;
53
+ const inScope = plan.value.filter((e) => e.scope === scope);
54
+ const applied = [];
55
+ const unchanged = [];
56
+ const skipped = [];
57
+ for (const entry of inScope) {
58
+ const resolved = resolveVaultPath(vaultRoot, entry.path);
59
+ if (!resolved.ok) {
60
+ skipped.push({ path: entry.path, reason: resolved.error.message });
61
+ continue;
62
+ }
63
+ const existing = await readFile(resolved.value);
64
+ if (!existing.ok) {
65
+ skipped.push({ path: entry.path, reason: existing.error.message });
66
+ continue;
67
+ }
68
+ const rendered = renderEntry(entry, existing.value, extensions);
69
+ if (!rendered.ok) {
70
+ skipped.push({ path: entry.path, reason: rendered.error.message });
71
+ continue;
72
+ }
73
+ // Idempotence: identical bytes → no write, no stage, no commit churn.
74
+ if (rendered.value === existing.value) {
75
+ unchanged.push(entry.path);
76
+ continue;
77
+ }
78
+ try {
79
+ await writeFile(resolved.value, rendered.value, "utf-8");
80
+ }
81
+ catch (e) {
82
+ const reason = e instanceof Error ? e.message : String(e);
83
+ skipped.push({ path: entry.path, reason: `write failed: ${reason}` });
84
+ continue;
85
+ }
86
+ applied.push(entry.path);
87
+ }
88
+ // One commit for the whole scope. Skipped entirely when nothing changed (no
89
+ // empty commits) or when the vault is configured with auto_commit: false.
90
+ let commitHash = null;
91
+ if (applied.length > 0 && config.value.autoCommit) {
92
+ const message = `vault_backfill: ${scope} — ${applied.length} ` +
93
+ `${applied.length === 1 ? "doc" : "docs"} frontmatter backfilled by ${agent}`;
94
+ const committed = await commit(vaultRoot, applied, message, agent);
95
+ if (!committed.ok)
96
+ return committed;
97
+ commitHash = committed.value.hash;
98
+ }
99
+ // Advisory provenance, per applied doc. Best-effort: a log failure does not
100
+ // fail the backfill (the commit is the durable record).
101
+ for (const path of applied) {
102
+ await recordProvenance(vaultRoot, {
103
+ tool: "vault_backfill",
104
+ file: path,
105
+ agent,
106
+ action: "backfill",
107
+ });
108
+ }
109
+ return ok({ scope, applied, unchanged, skipped, commit: commitHash });
110
+ }
111
+ //# sourceMappingURL=apply.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"apply.js","sourceRoot":"","sources":["../../src/backfill/apply.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,25 @@
1
+ import type { Frontmatter } from "../frontmatter/types.js";
2
+ import type { FileGitMeta } from "../utils/git.js";
3
+ import type { DerivationMap, DocClassification } from "./types.js";
4
+ export declare function slugify(value: string): string;
5
+ export declare function firstH1(body: string): string | null;
6
+ export declare function titleFromFilename(relPath: string): string;
7
+ export declare function parseQuestionSection(body: string, heading: string): string[];
8
+ export declare function mapIdentity(author: string, identityMap: Record<string, string>): string;
9
+ export declare function classifyDoc(raw: Record<string, unknown>): DocClassification;
10
+ export interface DeriveInputs {
11
+ relPath: string;
12
+ body: string;
13
+ raw: Record<string, unknown>;
14
+ coerced: Frontmatter;
15
+ git: FileGitMeta;
16
+ mtimeDate: string;
17
+ identityMap: Record<string, string>;
18
+ invoker: string;
19
+ }
20
+ export interface DerivedFrontmatter {
21
+ proposed: Frontmatter;
22
+ derivation: DerivationMap;
23
+ }
24
+ export declare function deriveProposed(input: DeriveInputs): DerivedFrontmatter;
25
+ //# sourceMappingURL=derive.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"derive.d.ts","sourceRoot":"","sources":["../../src/backfill/derive.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAC3D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AACnD,OAAO,KAAK,EAAE,aAAa,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAInE,wBAAgB,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAM7C;AAID,wBAAgB,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAMnD;AAID,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAKzD;AAKD,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,CAqB5E;AAID,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,MAAM,CAIvF;AAeD,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,iBAAiB,CAG3E;AAED,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IAEb,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAG7B,OAAO,EAAE,WAAW,CAAC;IACrB,GAAG,EAAE,WAAW,CAAC;IAEjB,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAGpC,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,kBAAkB;IACjC,QAAQ,EAAE,WAAW,CAAC;IACtB,UAAU,EAAE,aAAa,CAAC;CAC3B;AAKD,wBAAgB,cAAc,CAAC,KAAK,EAAE,YAAY,GAAG,kBAAkB,CA6EtE"}
@@ -0,0 +1,142 @@
1
+ // Deterministic frontmatter derivation for `daftari backfill` (§11.1).
2
+ //
3
+ // No LLM calls: every value comes from git metadata, body conventions, the
4
+ // path, or a fixed default. The contract is "suggest, don't assert" — adopted
5
+ // docs are proposed as canonical/medium/direct, but a human ratifies per folder
6
+ // before anything is written. Existing frontmatter is never overwritten: a
7
+ // present field is preserved verbatim, only missing fields are filled.
8
+ import { validateFrontmatter } from "../frontmatter/schema.js";
9
+ // kebab-case a free-form string: lowercase, non-alphanumerics → single hyphen,
10
+ // trimmed. Used for collection names and the identity fallback slug.
11
+ export function slugify(value) {
12
+ return value
13
+ .toLowerCase()
14
+ .replace(/[^a-z0-9]+/g, "-")
15
+ .replace(/-+/g, "-")
16
+ .replace(/^-+|-+$/g, "");
17
+ }
18
+ // The first ATX H1 (`# Title`) in the body, or null. Only a single leading `#`
19
+ // counts — `##` and deeper are sub-headings, not the document title.
20
+ export function firstH1(body) {
21
+ for (const line of body.split(/\r?\n/)) {
22
+ const m = line.match(/^#\s+(.+?)\s*#*\s*$/);
23
+ if (m)
24
+ return m[1].trim();
25
+ }
26
+ return null;
27
+ }
28
+ // Title-cased title derived from a filename when the body has no H1. Strips the
29
+ // `.md`, splits the basename on `-`/`_`, and capitalizes each word.
30
+ export function titleFromFilename(relPath) {
31
+ const base = (relPath.split("/").pop() ?? relPath).replace(/\.md$/i, "");
32
+ const words = base.split(/[-_]+/).filter((w) => w.length > 0);
33
+ if (words.length === 0)
34
+ return base;
35
+ return words.map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
36
+ }
37
+ // Bullet items under a `## <heading>` section, in order. Collection stops at the
38
+ // next heading of any level. Placeholder bullets (empty, or fully parenthical
39
+ // like "(none yet)") are dropped — they are scaffolding, not real questions.
40
+ export function parseQuestionSection(body, heading) {
41
+ const lines = body.split(/\r?\n/);
42
+ const target = heading.toLowerCase();
43
+ const out = [];
44
+ let inSection = false;
45
+ for (const line of lines) {
46
+ const headingMatch = line.match(/^#{1,6}\s+(.+?)\s*#*\s*$/);
47
+ if (headingMatch) {
48
+ const text = headingMatch[1].trim().toLowerCase();
49
+ inSection = text === target;
50
+ continue;
51
+ }
52
+ if (!inSection)
53
+ continue;
54
+ const bullet = line.match(/^\s*[-*]\s+(.*\S)\s*$/);
55
+ if (!bullet)
56
+ continue;
57
+ const item = bullet[1].trim();
58
+ if (item.length === 0)
59
+ continue;
60
+ if (item.startsWith("(") && item.endsWith(")"))
61
+ continue;
62
+ out.push(item);
63
+ }
64
+ return out;
65
+ }
66
+ // Maps a git author name to a Daftari identity: an explicit identity_map entry
67
+ // wins; otherwise the default `human:<slug>` fallback.
68
+ export function mapIdentity(author, identityMap) {
69
+ const mapped = identityMap[author];
70
+ if (mapped)
71
+ return mapped;
72
+ return `human:${slugify(author) || "unknown"}`;
73
+ }
74
+ // A frontmatter field is "present" — and therefore preserved — when the raw
75
+ // YAML carries a non-null, non-empty-string value for it. Empty arrays count as
76
+ // present (the author wrote `[]` deliberately).
77
+ function isPresent(raw, field) {
78
+ const v = raw[field];
79
+ if (v === undefined || v === null)
80
+ return false;
81
+ if (typeof v === "string" && v.length === 0)
82
+ return false;
83
+ return true;
84
+ }
85
+ // Whether a document needs backfilling. A doc whose existing frontmatter
86
+ // already validates against the built-in schema is conformant and skipped;
87
+ // otherwise it is `missing` (no frontmatter at all) or `partial`.
88
+ export function classifyDoc(raw) {
89
+ if (validateFrontmatter(raw).report.valid)
90
+ return "conformant";
91
+ return Object.keys(raw).length === 0 ? "missing" : "partial";
92
+ }
93
+ // Builds the full proposed frontmatter for a non-conformant doc plus a per-field
94
+ // derivation map. Present fields are preserved (coerced value, label
95
+ // "preserved"); missing fields are derived from git / body / path / defaults.
96
+ export function deriveProposed(input) {
97
+ const { relPath, body, raw, coerced, git, mtimeDate, identityMap, invoker } = input;
98
+ const derivation = {};
99
+ // Resolves one field: if present in raw, preserve the coerced value; else use
100
+ // the derived value. Records the chosen source label either way.
101
+ function resolve(field, derivedValue, derivedLabel) {
102
+ if (isPresent(raw, field)) {
103
+ derivation[field] = "preserved";
104
+ return coerced[field];
105
+ }
106
+ derivation[field] = derivedLabel;
107
+ return derivedValue;
108
+ }
109
+ // title ← H1, else filename.
110
+ const h1 = firstH1(body);
111
+ const title = resolve("title", h1 ?? titleFromFilename(relPath), h1 ? "body-h1" : "filename");
112
+ // collection ← first path component, kebab-cased.
113
+ const folder = relPath.split("/")[0] ?? "";
114
+ const collection = resolve("collection", slugify(folder), "parent-folder");
115
+ // created/updated ← git, else file mtime.
116
+ const created = resolve("created", git.created ?? mtimeDate, git.created ? "git-first-commit" : "file-mtime");
117
+ const updated = resolve("updated", git.updated ?? mtimeDate, git.updated ? "git-last-commit" : "file-mtime");
118
+ // updated_by ← git author mapped through identity config, else invoker.
119
+ const updatedBy = resolve("updated_by", git.author ? mapIdentity(git.author, identityMap) : invoker, git.author ? "git-author + identity-map" : "invoker-fallback");
120
+ // questions ← body sections, else empty.
121
+ const qAnswered = parseQuestionSection(body, "Questions Answered");
122
+ const qRaised = parseQuestionSection(body, "Questions Raised");
123
+ const proposed = {
124
+ title,
125
+ domain: resolve("domain", "accumulation", "default"),
126
+ collection,
127
+ status: resolve("status", "canonical", "default"),
128
+ confidence: resolve("confidence", "medium", "default"),
129
+ created,
130
+ updated,
131
+ updated_by: updatedBy,
132
+ provenance: resolve("provenance", "direct", "default"),
133
+ sources: resolve("sources", [], "empty"),
134
+ superseded_by: resolve("superseded_by", null, "null"),
135
+ ttl_days: resolve("ttl_days", null, "null"),
136
+ tags: resolve("tags", [], "empty"),
137
+ questions_answered: resolve("questions_answered", qAnswered, qAnswered.length > 0 ? "body-section" : "empty"),
138
+ questions_raised: resolve("questions_raised", qRaised, qRaised.length > 0 ? "body-section" : "empty"),
139
+ };
140
+ return { proposed, derivation };
141
+ }
142
+ //# sourceMappingURL=derive.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"derive.js","sourceRoot":"","sources":["../../src/backfill/derive.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export declare function runBackfill(argv: string[]): Promise<number>;
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/backfill/index.ts"],"names":[],"mappings":"AA+IA,wBAAsB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CA8GjE"}
@@ -0,0 +1,232 @@
1
+ // Top-level entry for `daftari backfill` (§11.1) — git-driven frontmatter
2
+ // migration for adopting an existing wiki into Daftari.
3
+ //
4
+ // Two-step, plan/apply:
5
+ // daftari backfill --plan [--scope <folder>] derive + stage to a plan file
6
+ // daftari backfill --apply --scope <folder> write the plan under one folder
7
+ //
8
+ // --scope is REQUIRED on --apply so a whole-vault write can never happen by
9
+ // accident. The plan does not modify any markdown; the apply commits one folder
10
+ // at a time (per-folder human ratification).
11
+ //
12
+ // Exit codes:
13
+ // 0 — success
14
+ // 1 — usage error (bad flags, missing --scope on apply, no mode)
15
+ // 2 — runtime / config error
16
+ import { userInfo } from "node:os";
17
+ import { resolve } from "node:path";
18
+ import { createInterface } from "node:readline/promises";
19
+ import { loadConfig } from "../utils/config.js";
20
+ import { applyPlan } from "./apply.js";
21
+ import { slugify } from "./derive.js";
22
+ import { generatePlan } from "./plan.js";
23
+ const HELP = `daftari backfill — derive frontmatter for an existing wiki from git history.
24
+
25
+ Usage:
26
+ daftari backfill --plan [--scope <folder>] [--vault <path>]
27
+ daftari backfill --apply --scope <folder> [--yes] [--vault <path>]
28
+
29
+ Modes:
30
+ --plan Walk the vault (or one folder), derive proposed
31
+ frontmatter, and write .daftari/backfill-plan.jsonl.
32
+ Modifies no markdown file. Idempotent — overwrites the
33
+ plan on each run.
34
+ --apply Write the plan's proposals for docs under --scope only,
35
+ then auto-commit them in one commit. --scope is
36
+ REQUIRED. Prompts for confirmation unless --yes.
37
+
38
+ Flags:
39
+ --scope <folder> First path component to act on (e.g. specs). Optional
40
+ on --plan (filters the walk), required on --apply.
41
+ --vault <path> Vault root (default: current directory).
42
+ --agent <identity> Acting identity for the apply COMMIT and provenance —
43
+ the migrator running the adoption (default:
44
+ human:<your-username>). Distinct from each doc's
45
+ 'updated_by' FIELD, which is derived from the doc's git
46
+ author through backfill.identity_map (original
47
+ authorship, not the migrator).
48
+ --yes Skip the apply confirmation prompt.
49
+ --help, -h Show this help.
50
+
51
+ The frontmatter 'updated_by' field records who originally authored a doc (its
52
+ git author, mapped via .daftari/config.yaml backfill.identity_map). The --agent
53
+ identity records who ran the migration — it authors the commit, not the field.
54
+ `;
55
+ function readArg(argv, flag) {
56
+ for (let i = 0; i < argv.length; i++) {
57
+ if (argv[i] === flag)
58
+ return argv[i + 1];
59
+ const prefix = `${flag}=`;
60
+ const a = argv[i];
61
+ if (a?.startsWith(prefix))
62
+ return a.slice(prefix.length);
63
+ }
64
+ return undefined;
65
+ }
66
+ // Default acting identity: human:<os-username>, slugified. Falls back to
67
+ // human:cli when the username is unavailable.
68
+ function defaultAgent() {
69
+ try {
70
+ const name = userInfo().username;
71
+ const slug = slugify(name);
72
+ return `human:${slug || "cli"}`;
73
+ }
74
+ catch {
75
+ return "human:cli";
76
+ }
77
+ }
78
+ function renderSummary(summary, planFile) {
79
+ const lines = [];
80
+ lines.push(`Backfill plan written to ${planFile}`);
81
+ lines.push("");
82
+ lines.push(` missing frontmatter: ${summary.missing}`);
83
+ lines.push(` partial frontmatter: ${summary.partial}`);
84
+ lines.push(` already conformant: ${summary.conformant}`);
85
+ if (summary.rootSkipped > 0) {
86
+ lines.push(` root-level (no folder): ${summary.rootSkipped} (skipped — backfill is per-folder)`);
87
+ }
88
+ lines.push("");
89
+ lines.push(` ${summary.planned} doc(s) planned across ${Object.keys(summary.byScope).length} folder(s):`);
90
+ for (const scope of Object.keys(summary.byScope).sort()) {
91
+ lines.push(` ${scope}: ${summary.byScope[scope]}`);
92
+ }
93
+ lines.push("");
94
+ if (summary.planned > 0) {
95
+ lines.push("Ratify a folder with:");
96
+ for (const scope of Object.keys(summary.byScope).sort()) {
97
+ lines.push(` daftari backfill --apply --scope ${scope}`);
98
+ }
99
+ lines.push("");
100
+ lines.push("The plan is transient — backfill never commits it (the apply commit is the");
101
+ lines.push("audit trail). If this vault wasn't scaffolded by `daftari --init`, add");
102
+ lines.push("`.daftari/backfill-plan.jsonl` to .gitignore so it can't be committed by hand.");
103
+ }
104
+ else {
105
+ lines.push("Nothing to backfill.");
106
+ }
107
+ return `${lines.join("\n")}\n`;
108
+ }
109
+ // Throttled stderr progress for the plan walk: a heartbeat every 50 docs (and a
110
+ // final newline) when the vault is large enough to matter. Returns the
111
+ // onProgress callback, or undefined to stay silent on small vaults.
112
+ function planProgress() {
113
+ let lastShown = 0;
114
+ return (done, total) => {
115
+ if (total < 50)
116
+ return;
117
+ if (done === total) {
118
+ process.stderr.write(`\rbackfill: scanned ${total}/${total} docs\n`);
119
+ return;
120
+ }
121
+ if (done - lastShown >= 50) {
122
+ lastShown = done;
123
+ process.stderr.write(`\rbackfill: scanned ${done}/${total} docs`);
124
+ }
125
+ };
126
+ }
127
+ async function confirm(prompt) {
128
+ const rl = createInterface({ input: process.stdin, output: process.stderr });
129
+ try {
130
+ const answer = await rl.question(prompt);
131
+ return /^y(es)?$/i.test(answer.trim());
132
+ }
133
+ finally {
134
+ rl.close();
135
+ }
136
+ }
137
+ export async function runBackfill(argv) {
138
+ if (argv.length === 0 || argv.includes("--help") || argv.includes("-h")) {
139
+ process.stdout.write(HELP);
140
+ return argv.length === 0 ? 1 : 0;
141
+ }
142
+ const wantPlan = argv.includes("--plan");
143
+ const wantApply = argv.includes("--apply");
144
+ if (wantPlan === wantApply) {
145
+ process.stderr.write("daftari backfill: pass exactly one of --plan or --apply\n");
146
+ return 1;
147
+ }
148
+ const vaultRoot = resolve(readArg(argv, "--vault") ?? ".");
149
+ const scope = readArg(argv, "--scope");
150
+ const agent = readArg(argv, "--agent") ?? defaultAgent();
151
+ // An explicitly-passed empty --scope ("" or `--scope=`) is a user error in
152
+ // either mode — reject it at parse time rather than letting it slip through as
153
+ // a no-match filter (plan) or fall to the required-scope check (apply). An
154
+ // omitted --scope (undefined) stays valid: optional on plan, caught below on
155
+ // apply.
156
+ if (scope !== undefined && scope.length === 0) {
157
+ process.stderr.write("daftari backfill: --scope cannot be empty\n");
158
+ return 1;
159
+ }
160
+ const config = loadConfig(vaultRoot);
161
+ if (!config.ok) {
162
+ process.stderr.write(`daftari backfill: ${config.error.message}\n`);
163
+ return 2;
164
+ }
165
+ const identityMap = config.value.backfillIdentityMap;
166
+ if (wantPlan) {
167
+ const result = await generatePlan(vaultRoot, {
168
+ scope,
169
+ identityMap,
170
+ invoker: agent,
171
+ onProgress: planProgress(),
172
+ });
173
+ if (!result.ok) {
174
+ process.stderr.write(`daftari backfill: ${result.error.message}\n`);
175
+ return 2;
176
+ }
177
+ process.stdout.write(renderSummary(result.value.summary, result.value.planPath));
178
+ return 0;
179
+ }
180
+ // --apply
181
+ if (scope === undefined || scope.length === 0) {
182
+ process.stderr.write("daftari backfill: --apply requires --scope <folder> (per-folder ratification; " +
183
+ "whole-vault apply is intentionally not supported)\n");
184
+ return 1;
185
+ }
186
+ if (!argv.includes("--yes")) {
187
+ // A non-interactive apply (piped stdin, CI) would block forever on the
188
+ // prompt. Refuse with an actionable message instead of hanging.
189
+ if (!process.stdin.isTTY) {
190
+ process.stderr.write("daftari backfill: --apply needs confirmation but stdin is not a TTY — " +
191
+ "re-run with --yes to apply non-interactively\n");
192
+ return 1;
193
+ }
194
+ const proceed = await confirm(`Apply backfilled frontmatter to docs under '${scope}' and commit as ${agent}? [y/N] `);
195
+ if (!proceed) {
196
+ process.stderr.write("daftari backfill: aborted\n");
197
+ return 0;
198
+ }
199
+ }
200
+ const result = await applyPlan(vaultRoot, scope, agent);
201
+ if (!result.ok) {
202
+ process.stderr.write(`daftari backfill: ${result.error.message}\n`);
203
+ return 2;
204
+ }
205
+ const r = result.value;
206
+ // No applied, no unchanged, no skipped means the plan had no entry under this
207
+ // scope — either a mistyped folder or a folder already fully backfilled (a
208
+ // re-plan drops conformant docs). Either way nothing was written, so this is
209
+ // an idempotent no-op: exit 0 (a CI loop re-applying every folder must not
210
+ // fail here) with a message that names the likely typo case.
211
+ if (r.applied.length === 0 && r.unchanged.length === 0 && r.skipped.length === 0) {
212
+ process.stdout.write(`No planned docs under '${scope}' — nothing to apply ` +
213
+ "(already backfilled, or check the folder name against the plan summary).\n");
214
+ return 0;
215
+ }
216
+ const out = [];
217
+ out.push(`Backfill applied to '${scope}':`);
218
+ out.push(` written: ${r.applied.length}`);
219
+ out.push(` unchanged: ${r.unchanged.length}`);
220
+ if (r.skipped.length > 0) {
221
+ out.push(` skipped: ${r.skipped.length}`);
222
+ for (const s of r.skipped)
223
+ out.push(` ${s.path}: ${s.reason}`);
224
+ }
225
+ if (r.commit)
226
+ out.push(` commit: ${r.commit}`);
227
+ else if (r.applied.length === 0)
228
+ out.push(" (no changes — already applied)");
229
+ process.stdout.write(`${out.join("\n")}\n`);
230
+ return 0;
231
+ }
232
+ //# sourceMappingURL=index.js.map