get-tbd 0.2.2 → 0.3.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 (86) hide show
  1. package/README.md +38 -6
  2. package/dist/bin.mjs +4036 -1074
  3. package/dist/bin.mjs.map +1 -1
  4. package/dist/cli.mjs +2505 -1585
  5. package/dist/cli.mjs.map +1 -1
  6. package/dist/{config-BJz1m9eN.mjs → config-B1w3pAkY.mjs} +142 -279
  7. package/dist/config-B1w3pAkY.mjs.map +1 -0
  8. package/dist/config-DXhifxOw.mjs +3 -0
  9. package/dist/doc-cache-CamtXfi4.mjs +1035 -0
  10. package/dist/doc-cache-CamtXfi4.mjs.map +1 -0
  11. package/dist/doc-cache-CiBwpDTf.mjs +3 -0
  12. package/dist/doc-fork-BMjqzfAs.mjs +3 -0
  13. package/dist/doc-fork-CoPi1G1N.mjs +304 -0
  14. package/dist/doc-fork-CoPi1G1N.mjs.map +1 -0
  15. package/dist/docref-C7g0sjvL.mjs +167 -0
  16. package/dist/docref-C7g0sjvL.mjs.map +1 -0
  17. package/dist/docs/README.md +38 -6
  18. package/dist/docs/SKILL.md +21 -5
  19. package/dist/docs/guidelines/backward-compatibility-rules.md +1 -0
  20. package/dist/docs/guidelines/bun-monorepo-patterns.md +1 -0
  21. package/dist/docs/guidelines/cli-agent-skill-patterns.md +228 -52
  22. package/dist/docs/guidelines/commit-conventions.md +1 -0
  23. package/dist/docs/guidelines/common-doc-guidelines.md +1 -0
  24. package/dist/docs/guidelines/convex-limits-best-practices.md +1 -0
  25. package/dist/docs/guidelines/convex-rules.md +1 -0
  26. package/dist/docs/guidelines/electron-app-development-patterns.md +1 -0
  27. package/dist/docs/guidelines/error-handling-rules.md +4 -0
  28. package/dist/docs/guidelines/general-coding-rules.md +3 -1
  29. package/dist/docs/guidelines/general-comment-rules.md +3 -1
  30. package/dist/docs/guidelines/general-eng-agent-principles.md +127 -0
  31. package/dist/docs/guidelines/general-tdd-guidelines.md +7 -0
  32. package/dist/docs/guidelines/general-testing-rules.md +5 -0
  33. package/dist/docs/guidelines/golden-testing-guidelines.md +1 -0
  34. package/dist/docs/guidelines/pnpm-monorepo-patterns.md +1 -0
  35. package/dist/docs/guidelines/python-cli-patterns.md +5 -0
  36. package/dist/docs/guidelines/python-modern-guidelines.md +4 -0
  37. package/dist/docs/guidelines/python-rules.md +7 -0
  38. package/dist/docs/guidelines/release-notes-guidelines.md +1 -0
  39. package/dist/docs/guidelines/supply-chain-hardening.md +3 -2
  40. package/dist/docs/guidelines/tbd-sync-troubleshooting.md +25 -2
  41. package/dist/docs/guidelines/typescript-cli-tool-rules.md +4 -3
  42. package/dist/docs/guidelines/typescript-code-coverage.md +7 -4
  43. package/dist/docs/guidelines/typescript-rules.md +8 -9
  44. package/dist/docs/guidelines/typescript-sorting-patterns.md +2 -1
  45. package/dist/docs/guidelines/typescript-yaml-handling-rules.md +6 -5
  46. package/dist/docs/references/docmap-format.md +64 -0
  47. package/dist/docs/references/docref-format.md +71 -0
  48. package/dist/docs/shortcuts/standard/checkout-third-party-repo.md +17 -4
  49. package/dist/docs/shortcuts/standard/new-shortcut.md +17 -3
  50. package/dist/docs/shortcuts/standard/plan-implementation-with-beads.md +1 -1
  51. package/dist/docs/shortcuts/standard/setup-github-cli.md +4 -1
  52. package/dist/docs/shortcuts/standard/suggest-upstream-improvements.md +69 -0
  53. package/dist/docs/shortcuts/standard/sync-failure-recovery.md +1 -1
  54. package/dist/docs/shortcuts/standard/welcome-user.md +28 -0
  55. package/dist/docs/shortcuts/system/shortcut-explanation.md +16 -1
  56. package/dist/docs/shortcuts/system/skill-baseline.md +21 -5
  57. package/dist/docs/tbd-design.md +144 -3
  58. package/dist/docs/tbd-docs.md +169 -5
  59. package/dist/docs/tbd-prime.md +0 -1
  60. package/dist/docs/templates/qa-playbook.md +3 -1
  61. package/dist/docs/templates/research-brief.md +2 -2
  62. package/dist/fork-manifest-ByU7U2do.mjs +253 -0
  63. package/dist/fork-manifest-ByU7U2do.mjs.map +1 -0
  64. package/dist/fork-manifest-C7lGRq-6.mjs +3 -0
  65. package/dist/{id-mapping-mtoSP9Qt.mjs → id-mapping-D0iitY-F.mjs} +1 -1
  66. package/dist/{id-mapping-687_UEsy.mjs → id-mapping-DAIeLKzm.mjs} +8 -200
  67. package/dist/id-mapping-DAIeLKzm.mjs.map +1 -0
  68. package/dist/index.d.mts +90 -13
  69. package/dist/index.mjs +3 -3
  70. package/dist/lockfile-BR0laSDy.mjs +198 -0
  71. package/dist/lockfile-BR0laSDy.mjs.map +1 -0
  72. package/dist/paths-C1DpnZJW.mjs +405 -0
  73. package/dist/paths-C1DpnZJW.mjs.map +1 -0
  74. package/dist/{schemas-f0EcuAVu.mjs → schemas-lCwRk30L.mjs} +19 -6
  75. package/dist/schemas-lCwRk30L.mjs.map +1 -0
  76. package/dist/{src-BpvcrLnq.mjs → src-CxKOynr1.mjs} +3 -3
  77. package/dist/src-CxKOynr1.mjs.map +1 -0
  78. package/dist/tbd +4036 -1074
  79. package/dist/yaml-utils-BPy991by.mjs.map +1 -1
  80. package/package.json +1 -1
  81. package/dist/config-BJz1m9eN.mjs.map +0 -1
  82. package/dist/config-DlCUMyCG.mjs +0 -3
  83. package/dist/docs/guidelines/general-eng-assistant-rules.md +0 -59
  84. package/dist/id-mapping-687_UEsy.mjs.map +0 -1
  85. package/dist/schemas-f0EcuAVu.mjs.map +0 -1
  86. package/dist/src-BpvcrLnq.mjs.map +0 -1
@@ -156,13 +156,11 @@ The recommended way to initialize tbd and configure agent integrations.
156
156
 
157
157
  ```bash
158
158
  tbd setup --auto # Full setup with auto-detection (recommended)
159
- tbd setup --interactive # Interactive setup with prompts
160
159
  tbd setup --from-beads # Migrate from existing Beads setup
161
160
  ```
162
161
 
163
162
  Options:
164
163
  - `--auto` - Automatic mode: auto-detect prefix, migrate beads if present
165
- - `--interactive` - Interactive mode: prompt for all options
166
164
  - `--from-beads` - Migrate issues from existing Beads setup
167
165
  - `--prefix <name>` - Override auto-detected prefix
168
166
 
@@ -576,6 +574,7 @@ Options:
576
574
 
577
575
  > **Note:** `tbd import --from-beads` is deprecated.
578
576
  > Use `tbd setup --auto` or `tbd setup --from-beads` instead for migrating from Beads.
577
+
579
578
  - `--validate` - Validate existing import against Beads source
580
579
 
581
580
  ### beads
@@ -674,12 +673,23 @@ and skips reinstallation.
674
673
 
675
674
  ### Documentation Commands
676
675
 
677
- Built-in documentation viewers:
676
+ Managed docs (the `tbd docs` group):
677
+
678
+ ```bash
679
+ tbd docs # Status overview of managed docs
680
+ tbd docs list # All docs across kinds, with state markers
681
+ tbd docs show <name> # Read any doc by name (kind-agnostic)
682
+ tbd docs show tbd-docs # The CLI manual (alias: tbd docs manual)
683
+ tbd docs show tbd-docs --sections # List the manual's sections
684
+ tbd docs show tbd-docs --section <name> # Read one manual section
685
+ tbd docs sync # Refresh the gitignored docs cache
686
+ tbd docs fork / unfork / update / diff / status # Forked docs (see below)
687
+ ```
688
+
689
+ Other built-in viewers:
678
690
 
679
691
  ```bash
680
692
  tbd readme # Display README (same as GitHub landing page)
681
- tbd docs # Display CLI reference documentation
682
- tbd docs --list # List available documentation sections
683
693
  tbd design # Display design documentation
684
694
  tbd design --list # List design doc sections
685
695
  tbd closing # Display session closing protocol reminder
@@ -714,6 +724,69 @@ On HTTP 403, fetching falls back to `gh api` for authenticated access.
714
724
  User-added shortcuts go to `shortcuts/custom/` (separate from bundled
715
725
  `shortcuts/standard/`).
716
726
 
727
+ ### Managing Docs: Two Modes
728
+
729
+ Every managed doc is served through one search path; where the file lives is a per-doc
730
+ choice between two modes that serve identical content:
731
+
732
+ - **Hidden cache (the default).** Docs live in the gitignored `.tbd/docs/` cache: always
733
+ active, zero repo footprint, refreshed by `tbd docs sync` (and by setup).
734
+ - **Forked.** `tbd docs fork <name>` (or `--all`) copies a doc into `docs/tbd/`, tracked
735
+ in git: visible on GitHub, reviewable in PRs, and editable; your copy shadows the
736
+ cache everywhere the upstream one was served.
737
+ `tbd docs unfork` returns to the cache; `tbd docs update` three-way merges upstream
738
+ changes into your copy after an upgrade.
739
+
740
+ Forking changes nothing about how docs work.
741
+ It only makes them explicit and editable.
742
+ Four update surfaces stay deliberately separate:
743
+
744
+ | Command | Scope | Touches | Modifies tracked files? |
745
+ | --- | --- | --- | --- |
746
+ | `tbd sync` | project data (issues/beads) | sync worktree + `tbd-sync` branch; also refreshes the doc cache and *reports* fork drift | never |
747
+ | `tbd setup --auto` | installation + integrations | skills, hooks, settings, `AGENTS.md`; invokes a docs-cache sync | only generated integration files |
748
+ | `tbd docs sync` | doc cache | gitignored `.tbd/docs/` only | never |
749
+ | `tbd docs update` | your forked docs | fork dir + bases + manifest (offline, against the cache) | **yes, the only doc command that does** |
750
+
751
+ Disambiguation worth stating once: `tbd update <id>` is an issue operation,
752
+ `tbd docs update` a doc operation; the noun scope always disambiguates.
753
+
754
+ ### Forked Docs in Your Repo (docs/tbd/)
755
+
756
+ `tbd docs fork` copies managed docs into `docs/tbd/`, laid out **by kind, flat within
757
+ each kind**, with a generated `README.md` index (regenerated on every
758
+ fork/unfork/update):
759
+
760
+ ```
761
+ docs/tbd/
762
+ ├── README.md # generated index — what this folder is, one line per doc
763
+ ├── guidelines/<name>.md
764
+ ├── shortcuts/<name>.md
765
+ └── templates/<name>.md
766
+ ```
767
+
768
+ Two rules make everything below predictable: **names are identity** (a doc is
769
+ `<kind>/<name>.md`; nested subfolders are not scanned), and **tracking is derived, not
770
+ stored** (the canonical model (copies, invariants, flows) is `tbd-design.md` §2.9; this
771
+ table is its user-facing summary); every doc’s state is recomputed from content hashes
772
+ (your file vs its recorded base vs current upstream), so no git operation can
773
+ desynchronize tbd from the folder.
774
+ Whatever you or your agent do to these files, `tbd docs status` gives a defined answer:
775
+
776
+ | You (or your agent)… | State | What happens / what to do |
777
+ | --- | --- | --- |
778
+ | Edit a forked file | `customized` | Served as-is; `tbd docs update` three-way merges upstream changes in |
779
+ | Delete a forked file | `missing` | Serving falls back to upstream; restore with `tbd docs fork <name> --force` or finalize with `tbd docs unfork <name>` |
780
+ | Rename a forked file | `missing` + `local` | A rename is delete + add: finalize the old name (`unfork`), keep the new file as `local` |
781
+ | Add a new `.md` file | `local` | Served with top precedence; nothing to update or unfork (no upstream) |
782
+ | Move a file into a subfolder | invisible | Subfolders are not scanned; keep files at `<kind>/<name>.md` |
783
+ | Delete `.tbd/doc-forks/` (the manifest) | all `local` | Files keep being served; re-fork with `--force` to re-establish update tracking (overwrites with upstream; re-apply edits after) |
784
+ | Commit / pull / merge / revert any of it | recomputed | States derive from content, so collaborators see the same answers from the same files |
785
+
786
+ Awareness without surprise mutations: `tbd sync` prints a one-line notice when forked
787
+ docs are stale, conflicted, or missing, and `tbd docs status` shows the full picture,
788
+ but only the explicit `tbd docs update` ever modifies tracked files.
789
+
717
790
  ### uninstall
718
791
 
719
792
  Remove tbd from a repository.
@@ -1022,8 +1095,32 @@ sync:
1022
1095
  branch: tbd-sync # Sync branch name
1023
1096
  remote: origin # Remote name
1024
1097
  auto_sync: true # Auto-sync after writes
1098
+
1099
+ docs_cache:
1100
+ files: # Docs synced into the cache: destination -> docref
1101
+ guidelines/python-rules.md: internal:guidelines/python-rules.md
1102
+ guidelines/my-team-rules.md: github:my-org/docs@main//rules.md
1103
+ lookup_path: # Search paths for doc lookup (earlier wins)
1104
+ - .tbd/docs/shortcuts/system
1105
+ - .tbd/docs/shortcuts/standard
1025
1106
  ```
1026
1107
 
1108
+ `docs_cache.files` values, like the fork manifest’s `source` values in
1109
+ `.tbd/doc-forks/forks.yml`, are **docrefs**: one URI-like address grammar (`internal:…`,
1110
+ anchored local paths, URLs, `github:owner/repo@ref//path`). For the full grammar see
1111
+ `tbd docs show docref-format`; for the docmap structure that doc listings and their
1112
+ `--json` output follow, see `tbd docs show docmap-format`.
1113
+
1114
+ Two further `docs_cache` keys:
1115
+
1116
+ - `docs_cache.local_dirs`: an ordered list of `./`-prefixed local docrefs naming extra
1117
+ in-repo doc directories, served between the fork dir and the cache.
1118
+ Docs found there are first-class for reading (`list`, `show`, the per-kind readers,
1119
+ with a `(serving local doc: …)` note) and report state `local`; they are not forkable
1120
+ or updatable; they already live in the repo.
1121
+ - `docs_cache.fork_dir`: reserved in the f05 format era but **planned, not yet read**:
1122
+ the fork-dir location is currently fixed at `docs/tbd/`.
1123
+
1027
1124
  ## Priority Scale
1028
1125
 
1029
1126
  | Value | Alias | Meaning |
@@ -1118,6 +1215,73 @@ ls "$(git rev-parse --path-format=absolute --git-common-dir)/tbd/data-sync-workt
1118
1215
  ls "$(git rev-parse --path-format=absolute --git-common-dir)/tbd/data-sync-worktree/.tbd/data-sync/issues/" | sort
1119
1216
  ```
1120
1217
 
1218
+ ### Aborting a Format Upgrade
1219
+
1220
+ Upgrading tbd can bump the repository format (`tbd_format` in `.tbd/config.yml`, e.g.
1221
+ f04 → f05). The bump happens automatically on the first command after upgrading, and
1222
+ older tbd versions then refuse the repository until they are upgraded.
1223
+ If an upgrade hits unexpected bugs, you can cleanly abort and return to the previous
1224
+ version. This is everything a format upgrade can touch:
1225
+
1226
+ | State | Location | In git? | Written by | Revert |
1227
+ | --- | --- | --- | --- | --- |
1228
+ | Project config | `.tbd/config.yml` | tracked | the migration (format stamp) | `git checkout -- .tbd/config.yml`, or `git revert` the bump commit |
1229
+ | Agent surfaces | `AGENTS.md`, `.claude/`, `.agents/`, `.codex/` | tracked | only `tbd setup --auto` (marker refresh) | `git checkout --` those paths |
1230
+ | Shared layout stamp | `$GIT_COMMON_DIR/tbd/layout.yml` | machine-local, not in git | the migration (re-stamp) | delete it; it regenerates from whatever the config says |
1231
+ | Forked docs (f05) | `docs/tbd/`, `.tbd/doc-forks/` | tracked once committed | only `tbd docs fork` | `git checkout --`/`git revert` if committed; delete if never committed |
1232
+ | Docs cache | `.tbd/docs/` | gitignored | doc sync (unchanged by migration) | none needed; always safe to delete and re-sync |
1233
+ | Issue data | `tbd-sync` branch + `$GIT_COMMON_DIR/tbd/data-sync-worktree/` | git branch | **never touched by migration** | none needed; the worktree re-materializes from the branch |
1234
+
1235
+ **Abort recipe** (works from any state, including a crash mid-upgrade):
1236
+
1237
+ ```bash
1238
+ # 1. Restore the tracked files (or `git revert` the format-bump commit):
1239
+ git checkout -- .tbd/config.yml
1240
+ git checkout -- AGENTS.md .claude .agents .codex # only if `tbd setup --auto` ran
1241
+
1242
+ # 2. Delete the machine-local format stamp (regenerates from the config):
1243
+ rm "$(git rev-parse --path-format=absolute --git-common-dir)/tbd/layout.yml"
1244
+
1245
+ # 3. Only if docs were forked and never committed:
1246
+ rm -rf docs/tbd .tbd/doc-forks
1247
+ ```
1248
+
1249
+ After this, the previous tbd version works again, and re-running the upgrade later is
1250
+ safe; the migration is idempotent from any of these states.
1251
+
1252
+ Reverting `.tbd/config.yml` is enough to drop the format gate even if forks were already
1253
+ committed: compatibility is decided only by `tbd_format` in the config, not by the
1254
+ presence of `docs/tbd/` or `.tbd/doc-forks/`. Committed fork files simply become inert
1255
+ `local` docs under the older version; harmless to leave in place, so step 3 is only for
1256
+ cleanup, never required to abort.
1257
+
1258
+ Notes:
1259
+
1260
+ - **The migration never writes issue data**, so the recipe above cannot lose issues; it
1261
+ touches only the two stamps and tracked files.
1262
+ A bigger hammer also exists: deleting the entire `$GIT_COMMON_DIR/tbd/` directory is
1263
+ recoverable (layout and the data-sync worktree re-materialize from the config and the
1264
+ `tbd-sync` branch on the next command, or via `tbd doctor --fix`); **but only for
1265
+ synced data**. Issue changes made with `--no-sync` since the last `tbd sync` live as
1266
+ uncommitted files inside that worktree and would be lost, so run `tbd sync` first if
1267
+ you must delete it. This is why the recipe deletes only `layout.yml`, never the whole
1268
+ directory.
1269
+ - **Interrupted upgrades self-heal.** If the process dies between the two stamp writes
1270
+ (layout updated but not config, or config but not layout), the next command with the
1271
+ new version completes the migration; the abort recipe above also works from either
1272
+ partial state.
1273
+ - **Quiesce other tbd processes first.** The same self-healing re-stamp that completes
1274
+ an interrupted upgrade can also undo an abort.
1275
+ Any concurrent `tbd` write (another worktree, a background agent, an editor hook)
1276
+ re-stamps `layout.yml` from whatever `.tbd/config.yml` currently says.
1277
+ If you delete `layout.yml` while the config is still on the new format, or before the
1278
+ config revert in step 1 has landed, the next write recreates the stamp and reopens the
1279
+ migration. Stop other agents and worktrees, do step 1 (revert the config) before step 2
1280
+ (delete the stamp), and the abort sticks.
1281
+ - Teammates each migrate their own machine-local stamp automatically; only the
1282
+ `.tbd/config.yml` change is shared (via your branch), so reverting that commit is the
1283
+ team-wide rollback.
1284
+
1121
1285
  ### Performance
1122
1286
 
1123
1287
  For large repositories with many issues:
@@ -109,7 +109,6 @@ tbd dep add <tests-id> <feature-id> # Tests depend on feature
109
109
  ## Setup Commands
110
110
 
111
111
  - `tbd setup --auto` - Non-interactive setup with smart defaults (for agents/scripts)
112
- - `tbd setup --interactive` - Interactive setup with prompts (for humans)
113
112
  - `tbd setup --from-beads` - Migrate from Beads to tbd
114
113
 
115
114
  ## Quick Reference
@@ -14,6 +14,7 @@ Manual QA playbook for [brief description of what is being tested].
14
14
  > This is a kind of “manual test”: it is not a strict end-to-end integration test,
15
15
  > because it is too costly or pass/fail is not clear.
16
16
  > Goals:
17
+ >
17
18
  > - Document expected behavior that are difficult to capture fully in unit, integration,
18
19
  > or end-to-end/golden tests.
19
20
  > - Make it possible for an agent to follow the instructions and evaluate if things are
@@ -54,9 +55,10 @@ Manual QA playbook for [brief description of what is being tested].
54
55
 
55
56
  * * *
56
57
 
57
- ## Related Documentation Read for Context
58
+ ## Related Documentation: Read for Context
58
59
 
59
60
  > Include links to relevant documentation that provides context for this test, e.g.
61
+ >
60
62
  > - [spec-name.md](path/to/spec) - [Brief description]
61
63
  > - [architecture-doc.md](path/to/arch) - [Brief description]
62
64
  > - [other-qa-playbook.md](path/to/qa) - [Brief description]
@@ -46,7 +46,7 @@ author: Joshua Levy (github.com/jlevy) with LLM assistance
46
46
  > This section is for the “so what?”—conclusions that aren’t obvious from the individual
47
47
  > findings alone.
48
48
 
49
- ## Comparison Matrix *(optional for comparative research)*
49
+ ## Comparison Matrix *(optional, for comparative research)*
50
50
 
51
51
  > When evaluating multiple options or technologies, a structured comparison table is
52
52
  > often the most useful artifact.
@@ -103,7 +103,7 @@ author: Joshua Levy (github.com/jlevy) with LLM assistance
103
103
  - [ ] Action item 1
104
104
  - [ ] Action item 2
105
105
 
106
- ## Methodology *(optional for complex or high-stakes research)*
106
+ ## Methodology *(optional, for complex or high-stakes research)*
107
107
 
108
108
  > How the research was conducted: what searches were run, what sources were
109
109
  > cross-referenced, what information was not found or remains uncertain.
@@ -0,0 +1,253 @@
1
+ import { s as stringifyYaml } from "./yaml-utils-BPy991by.mjs";
2
+ import { P as resolveSharedTbdPaths } from "./paths-C1DpnZJW.mjs";
3
+ import { n as withLockfile } from "./lockfile-BR0laSDy.mjs";
4
+ import { n as isDocRef } from "./docref-C7g0sjvL.mjs";
5
+ import { z } from "zod";
6
+ import { parse } from "yaml";
7
+ import { mkdir, readFile, rm } from "node:fs/promises";
8
+ import { dirname, join } from "node:path";
9
+ import { writeFile } from "atomically";
10
+ import { createHash } from "node:crypto";
11
+
12
+ //#region src/file/fork-manifest.ts
13
+ /**
14
+ * Fork manifest and base snapshots for the forkable-docs workflow.
15
+ *
16
+ * All fork state lives under one committed directory, `.tbd/doc-forks/`:
17
+ * forks.yml : the manifest (provenance, hashes, revisions)
18
+ * base/<kind>/<name>.md : verbatim base snapshots (the merge bases)
19
+ *
20
+ * The base of every fork is a stored snapshot of the upstream content the fork
21
+ * was created from. Together with `base_hash`, this makes "customized?",
22
+ * "stale vs upstream?", and three-way merging cheap, exact, offline operations.
23
+ *
24
+ * The hashing and state computation here are pure functions (see
25
+ * {@link hashContent} and {@link computeForkStatus}); only the read/write helpers
26
+ * touch the filesystem.
27
+ */
28
+ /** Directory (repo-relative under `.tbd/`) holding all fork state. */
29
+ const DOC_FORKS_DIR = ".tbd/doc-forks";
30
+ /** Manifest filename within {@link DOC_FORKS_DIR}. */
31
+ const FORKS_FILE = "forks.yml";
32
+ /** Subdirectory within {@link DOC_FORKS_DIR} holding base snapshots. */
33
+ const BASE_SUBDIR = "base";
34
+ /** Doc kinds that can be forked. */
35
+ const FORK_KINDS = [
36
+ "guideline",
37
+ "shortcut",
38
+ "template",
39
+ "reference"
40
+ ];
41
+ /**
42
+ * A safe doc name: no path separators, no `..`, no leading dot, no NUL.
43
+ * Names are used to build filesystem paths (and are a doc's identity), so a
44
+ * crafted manifest must not be able to escape the fork dir (e.g. via a
45
+ * `../../../../victim` name through `unfork --force`). Allows the punctuation
46
+ * real doc names use (letters, digits, `.`, `_`, `-`).
47
+ */
48
+ const SAFE_DOC_NAME = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
49
+ function isSafeDocName(name) {
50
+ return SAFE_DOC_NAME.test(name) && !name.includes("..") && !name.endsWith(".md");
51
+ }
52
+ const ForkEntrySchema = z.object({
53
+ name: z.string().min(1).refine(isSafeDocName, { message: "invalid doc name (no path separators, \"..\", or leading dot)" }),
54
+ kind: z.enum(FORK_KINDS),
55
+ path: z.string().min(1),
56
+ source: z.string().min(1).refine(isDocRef, { message: "source must be a valid docref" }),
57
+ base_hash: z.string().min(1),
58
+ tbd_version: z.string().optional(),
59
+ source_revision: z.string().optional(),
60
+ source_tag: z.string().optional(),
61
+ conflicted: z.boolean().optional()
62
+ });
63
+ const ForkManifestSchema = z.object({ forks: z.array(ForkEntrySchema).default([]) });
64
+ /** An empty manifest (no forks). */
65
+ function emptyManifest() {
66
+ return { forks: [] };
67
+ }
68
+ /** Normalize line endings to LF so hashes are stable across platforms. */
69
+ function normalizeLineEndings(content) {
70
+ return content.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
71
+ }
72
+ /** sha256: hash of the LF-normalized content. */
73
+ function hashContent(content) {
74
+ return `sha256:${createHash("sha256").update(normalizeLineEndings(content), "utf8").digest("hex")}`;
75
+ }
76
+ /**
77
+ * The labels tbd writes into its three-way merge conflict markers. Detection of
78
+ * an *unresolved* conflict keys off these specific labels (not generic marker
79
+ * lines) so a forked doc that legitimately contains conflict-marker examples
80
+ * (e.g. a git tutorial, or our own golden-testing guideline) is not stuck
81
+ * `conflicted` forever after one unrelated `update --merge`.
82
+ */
83
+ const CONFLICT_LABELS = {
84
+ ours: "ours (your fork)",
85
+ base: "base (fork point)",
86
+ theirs: "theirs (upstream)"
87
+ };
88
+ /** Whether `content` still carries tbd's own unresolved conflict markers. */
89
+ function hasUnresolvedConflict(content) {
90
+ return content.includes(`<<<<<<< ${CONFLICT_LABELS.ours}`) && content.includes(`>>>>>>> ${CONFLICT_LABELS.theirs}`);
91
+ }
92
+ /**
93
+ * Loose semver comparison on major.minor.patch (prerelease ignored). Returns
94
+ * null when either version is unparseable; callers must not guard on a version
95
+ * they cannot parse (treat null as "do not block").
96
+ */
97
+ function compareVersionsLoose(a, b) {
98
+ const parse = (v) => {
99
+ const m = /^(\d+)\.(\d+)\.(\d+)/.exec(v.trim());
100
+ return m ? [
101
+ Number(m[1]),
102
+ Number(m[2]),
103
+ Number(m[3])
104
+ ] : null;
105
+ };
106
+ const pa = parse(a);
107
+ const pb = parse(b);
108
+ if (!pa || !pb) return null;
109
+ for (let i = 0; i < 3; i++) {
110
+ const x = pa[i];
111
+ const y = pb[i];
112
+ if (x < y) return -1;
113
+ if (x > y) return 1;
114
+ }
115
+ return 0;
116
+ }
117
+ /**
118
+ * Run `fn` while holding the doc-forks manifest lock, serializing the
119
+ * read-modify-write of `forks.yml` across concurrent fork/unfork/update so
120
+ * entries are not lost to last-writer-wins. The lock lives in the machine-local
121
+ * git common dir (never committed), alongside the data-sync lock.
122
+ */
123
+ async function withForkManifestLock(tbdRoot, fn) {
124
+ const paths = await resolveSharedTbdPaths(tbdRoot);
125
+ await mkdir(paths.sharedLocksDir, { recursive: true });
126
+ return withLockfile(join(paths.sharedLocksDir, "doc-forks.lock"), fn);
127
+ }
128
+ /**
129
+ * Compute a doc's fork status from hashes and flags. Pure and total; every
130
+ * combination of inputs maps to exactly one {@link ForkStatus}.
131
+ */
132
+ function computeForkStatus(input) {
133
+ const none = {
134
+ customized: false,
135
+ stale: false,
136
+ conflicted: false,
137
+ orphaned: false
138
+ };
139
+ if (!input.inManifest) return {
140
+ state: input.forkFileExists ? "local" : "upstream",
141
+ ...none
142
+ };
143
+ if (!input.forkFileExists) return {
144
+ state: "missing",
145
+ ...none
146
+ };
147
+ const customized = input.forkHash !== input.baseHash;
148
+ const orphaned = input.cacheHash === void 0;
149
+ const stale = !orphaned && input.cacheHash !== input.baseHash;
150
+ const conflicted = Boolean(input.conflictedFlag && input.markersPresent);
151
+ let state;
152
+ if (conflicted) state = "conflicted";
153
+ else if (orphaned) state = "orphaned";
154
+ else if (customized) state = "customized";
155
+ else if (stale) state = "stale";
156
+ else state = "forked";
157
+ return {
158
+ state,
159
+ customized,
160
+ stale,
161
+ conflicted,
162
+ orphaned
163
+ };
164
+ }
165
+ /** Find a fork entry by name, optionally constrained to a kind. */
166
+ function findFork(manifest, name, kind) {
167
+ return manifest.forks.find((f) => f.name === name && (kind === void 0 || f.kind === kind));
168
+ }
169
+ /** Return a new manifest with `entry` inserted or replaced (matched by kind+name). */
170
+ function upsertFork(manifest, entry) {
171
+ const forks = manifest.forks.filter((f) => !(f.name === entry.name && f.kind === entry.kind));
172
+ forks.push(entry);
173
+ forks.sort((a, b) => a.kind.localeCompare(b.kind) || a.name.localeCompare(b.name));
174
+ return { forks };
175
+ }
176
+ /** Return a new manifest with the matching entry removed. */
177
+ function removeFork(manifest, name, kind) {
178
+ return { forks: manifest.forks.filter((f) => !(f.name === name && (kind === void 0 || f.kind === kind))) };
179
+ }
180
+ function forksDir(tbdRoot) {
181
+ return join(tbdRoot, DOC_FORKS_DIR);
182
+ }
183
+ function forksFilePath(tbdRoot) {
184
+ return join(forksDir(tbdRoot), FORKS_FILE);
185
+ }
186
+ function baseFilePath(tbdRoot, kind, name) {
187
+ return join(forksDir(tbdRoot), BASE_SUBDIR, kind, `${name}.md`);
188
+ }
189
+ function isNotFound(err) {
190
+ return err?.code === "ENOENT";
191
+ }
192
+ /** The outer manifest shape, before per-entry validation. */
193
+ const ForkManifestEnvelopeSchema = z.object({ forks: z.array(z.unknown()).default([]) });
194
+ /**
195
+ * Read the fork manifest, returning an empty manifest if none exists.
196
+ *
197
+ * Parsing is tolerant per entry: a malformed or unsafe entry (bad name/kind,
198
+ * path-traversal attempt) is dropped with a warning rather than aborting the
199
+ * whole read. This both fails closed on a crafted entry (it is never returned,
200
+ * so commands never act on it; no out-of-tree deletes) and keeps one corrupt
201
+ * entry from taking down status/update for every other fork.
202
+ */
203
+ async function readForkManifest(tbdRoot) {
204
+ let content;
205
+ try {
206
+ content = await readFile(forksFilePath(tbdRoot), "utf-8");
207
+ } catch (err) {
208
+ if (isNotFound(err)) return emptyManifest();
209
+ throw err;
210
+ }
211
+ const envelope = ForkManifestEnvelopeSchema.parse(parse(content) ?? { forks: [] });
212
+ const forks = [];
213
+ let dropped = 0;
214
+ for (const raw of envelope.forks) {
215
+ const parsed = ForkEntrySchema.safeParse(raw);
216
+ if (parsed.success) forks.push(parsed.data);
217
+ else dropped++;
218
+ }
219
+ if (dropped > 0) process.stderr.write(`• Ignored ${dropped} invalid fork manifest entr${dropped === 1 ? "y" : "ies"} in ${FORKS_FILE} (bad name/kind or unsafe path). Run 'tbd doctor' to review.\n`);
220
+ return { forks };
221
+ }
222
+ /** Write the fork manifest (creating `.tbd/doc-forks/` as needed). */
223
+ async function writeForkManifest(tbdRoot, manifest) {
224
+ await mkdir(forksDir(tbdRoot), { recursive: true });
225
+ const yaml = stringifyYaml(manifest, {
226
+ lineWidth: 0,
227
+ sortMapEntries: false
228
+ });
229
+ await writeFile(forksFilePath(tbdRoot), yaml);
230
+ }
231
+ /** Read a base snapshot's content, or null if it is absent. */
232
+ async function readBaseContent(tbdRoot, kind, name) {
233
+ try {
234
+ return await readFile(baseFilePath(tbdRoot, kind, name), "utf-8");
235
+ } catch (err) {
236
+ if (isNotFound(err)) return null;
237
+ throw err;
238
+ }
239
+ }
240
+ /** Write a base snapshot verbatim (creating parent dirs as needed). */
241
+ async function writeBaseContent(tbdRoot, kind, name, content) {
242
+ const path = baseFilePath(tbdRoot, kind, name);
243
+ await mkdir(dirname(path), { recursive: true });
244
+ await writeFile(path, content);
245
+ }
246
+ /** Remove a base snapshot if present. */
247
+ async function removeBaseContent(tbdRoot, kind, name) {
248
+ await rm(baseFilePath(tbdRoot, kind, name), { force: true });
249
+ }
250
+
251
+ //#endregion
252
+ export { withForkManifestLock as C, upsertFork as S, writeForkManifest as T, normalizeLineEndings as _, FORK_KINDS as a, removeBaseContent as b, compareVersionsLoose as c, findFork as d, forksDir as f, isSafeDocName as g, hashContent as h, FORKS_FILE as i, computeForkStatus as l, hasUnresolvedConflict as m, CONFLICT_LABELS as n, ForkEntrySchema as o, forksFilePath as p, DOC_FORKS_DIR as r, baseFilePath as s, BASE_SUBDIR as t, emptyManifest as u, readBaseContent as v, writeBaseContent as w, removeFork as x, readForkManifest as y };
253
+ //# sourceMappingURL=fork-manifest-ByU7U2do.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fork-manifest-ByU7U2do.mjs","names":["parseYaml"],"sources":["../src/file/fork-manifest.ts"],"sourcesContent":["/**\n * Fork manifest and base snapshots for the forkable-docs workflow.\n *\n * All fork state lives under one committed directory, `.tbd/doc-forks/`:\n * forks.yml : the manifest (provenance, hashes, revisions)\n * base/<kind>/<name>.md : verbatim base snapshots (the merge bases)\n *\n * The base of every fork is a stored snapshot of the upstream content the fork\n * was created from. Together with `base_hash`, this makes \"customized?\",\n * \"stale vs upstream?\", and three-way merging cheap, exact, offline operations.\n *\n * The hashing and state computation here are pure functions (see\n * {@link hashContent} and {@link computeForkStatus}); only the read/write helpers\n * touch the filesystem.\n */\n\nimport { createHash } from 'node:crypto';\nimport { readFile, mkdir, rm } from 'node:fs/promises';\nimport { dirname, join } from 'node:path';\n\nimport { writeFile } from 'atomically';\nimport { parse as parseYaml } from 'yaml';\nimport { z } from 'zod';\n\nimport { stringifyYaml } from '../utils/yaml-utils.js';\nimport { withLockfile } from '../utils/lockfile.js';\nimport { resolveSharedTbdPaths } from '../lib/paths.js';\nimport { isDocRef } from '../docref/index.js';\n\n/** Directory (repo-relative under `.tbd/`) holding all fork state. */\nexport const DOC_FORKS_DIR = '.tbd/doc-forks';\n/** Manifest filename within {@link DOC_FORKS_DIR}. */\nexport const FORKS_FILE = 'forks.yml';\n/** Subdirectory within {@link DOC_FORKS_DIR} holding base snapshots. */\nexport const BASE_SUBDIR = 'base';\n\n/** Doc kinds that can be forked. */\nexport const FORK_KINDS = ['guideline', 'shortcut', 'template', 'reference'] as const;\nexport type ForkKind = (typeof FORK_KINDS)[number];\n\n// =============================================================================\n// Schema\n// =============================================================================\n\n/**\n * A safe doc name: no path separators, no `..`, no leading dot, no NUL.\n * Names are used to build filesystem paths (and are a doc's identity), so a\n * crafted manifest must not be able to escape the fork dir (e.g. via a\n * `../../../../victim` name through `unfork --force`). Allows the punctuation\n * real doc names use (letters, digits, `.`, `_`, `-`).\n */\nconst SAFE_DOC_NAME = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;\nexport function isSafeDocName(name: string): boolean {\n return SAFE_DOC_NAME.test(name) && !name.includes('..') && !name.endsWith('.md');\n}\n\nexport const ForkEntrySchema = z.object({\n /** Doc name (e.g. \"python-rules\"). Constrained so it cannot escape the fork dir. */\n name: z.string().min(1).refine(isSafeDocName, {\n message: 'invalid doc name (no path separators, \"..\", or leading dot)',\n }),\n /** Doc kind: must be one of the known fork kinds. */\n kind: z.enum(FORK_KINDS),\n /** Repo-relative path of the forked file (e.g. \"docs/tbd/guidelines/python-rules.md\"). */\n path: z.string().min(1),\n /** Provenance docref the fork was created from (docref-everywhere rule). */\n source: z.string().min(1).refine(isDocRef, { message: 'source must be a valid docref' }),\n /** sha256: of the LF-normalized base content. */\n base_hash: z.string().min(1),\n /** tbd version when the base was last set. */\n tbd_version: z.string().optional(),\n /** Upstream commit at base time (git-hosted sources only). */\n source_revision: z.string().optional(),\n /** Exact/matching tag at base time, when one exists. */\n source_tag: z.string().optional(),\n /** Set by `update --merge` when it leaves conflict markers; auto-clears. */\n conflicted: z.boolean().optional(),\n});\n\nexport type ForkEntry = z.infer<typeof ForkEntrySchema>;\n\nexport const ForkManifestSchema = z.object({\n forks: z.array(ForkEntrySchema).default([]),\n});\n\nexport type ForkManifest = z.infer<typeof ForkManifestSchema>;\n\n/** An empty manifest (no forks). */\nexport function emptyManifest(): ForkManifest {\n return { forks: [] };\n}\n\n// =============================================================================\n// Hashing (pure)\n// =============================================================================\n\n/** Normalize line endings to LF so hashes are stable across platforms. */\nexport function normalizeLineEndings(content: string): string {\n return content.replace(/\\r\\n/g, '\\n').replace(/\\r/g, '\\n');\n}\n\n/** sha256: hash of the LF-normalized content. */\nexport function hashContent(content: string): string {\n const hex = createHash('sha256').update(normalizeLineEndings(content), 'utf8').digest('hex');\n return `sha256:${hex}`;\n}\n\n/**\n * Whether `content` still contains git conflict markers. Requires all three\n * standard markers so prose that merely discusses one marker is not flagged.\n */\nexport function hasConflictMarkers(content: string): boolean {\n return /^<{7}/m.test(content) && /^={7}\\s*$/m.test(content) && /^>{7}/m.test(content);\n}\n\n/**\n * The labels tbd writes into its three-way merge conflict markers. Detection of\n * an *unresolved* conflict keys off these specific labels (not generic marker\n * lines) so a forked doc that legitimately contains conflict-marker examples\n * (e.g. a git tutorial, or our own golden-testing guideline) is not stuck\n * `conflicted` forever after one unrelated `update --merge`.\n */\nexport const CONFLICT_LABELS = {\n ours: 'ours (your fork)',\n base: 'base (fork point)',\n theirs: 'theirs (upstream)',\n} as const;\n\n/** Whether `content` still carries tbd's own unresolved conflict markers. */\nexport function hasUnresolvedConflict(content: string): boolean {\n return (\n content.includes(`<<<<<<< ${CONFLICT_LABELS.ours}`) &&\n content.includes(`>>>>>>> ${CONFLICT_LABELS.theirs}`)\n );\n}\n\n/**\n * Loose semver comparison on major.minor.patch (prerelease ignored). Returns\n * null when either version is unparseable; callers must not guard on a version\n * they cannot parse (treat null as \"do not block\").\n */\nexport function compareVersionsLoose(a: string, b: string): -1 | 0 | 1 | null {\n const parse = (v: string): [number, number, number] | null => {\n const m = /^(\\d+)\\.(\\d+)\\.(\\d+)/.exec(v.trim());\n return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : null;\n };\n const pa = parse(a);\n const pb = parse(b);\n if (!pa || !pb) return null;\n for (let i = 0; i < 3; i++) {\n const x = pa[i]!;\n const y = pb[i]!;\n if (x < y) return -1;\n if (x > y) return 1;\n }\n return 0;\n}\n\n/**\n * Run `fn` while holding the doc-forks manifest lock, serializing the\n * read-modify-write of `forks.yml` across concurrent fork/unfork/update so\n * entries are not lost to last-writer-wins. The lock lives in the machine-local\n * git common dir (never committed), alongside the data-sync lock.\n */\nexport async function withForkManifestLock<T>(tbdRoot: string, fn: () => Promise<T>): Promise<T> {\n const paths = await resolveSharedTbdPaths(tbdRoot);\n await mkdir(paths.sharedLocksDir, { recursive: true });\n return withLockfile(join(paths.sharedLocksDir, 'doc-forks.lock'), fn);\n}\n\n// =============================================================================\n// State computation (pure)\n// =============================================================================\n\nexport type DocState =\n | 'upstream'\n | 'forked'\n | 'customized'\n | 'stale'\n | 'conflicted'\n | 'local'\n | 'missing'\n | 'orphaned';\n\n/** Inputs for {@link computeForkStatus}; all comparisons are by hash. */\nexport interface ForkStatusInput {\n /** Whether a manifest entry exists for this doc. */\n inManifest: boolean;\n /** Whether the forked file is present on disk. */\n forkFileExists: boolean;\n /** sha256: of the current forked file (if present). */\n forkHash?: string;\n /** Recorded `base_hash` from the manifest entry. */\n baseHash?: string;\n /** sha256: of the current upstream/cache content; undefined if the source is gone. */\n cacheHash?: string;\n /** Manifest `conflicted` flag. */\n conflictedFlag?: boolean;\n /** Whether conflict markers are present in the current file. */\n markersPresent?: boolean;\n}\n\n/**\n * Computed lifecycle status of a doc. `state` is the headline for display; the\n * booleans are orthogonal modifiers (`customized` and `stale` can combine).\n */\nexport interface ForkStatus {\n state: DocState;\n customized: boolean;\n stale: boolean;\n conflicted: boolean;\n orphaned: boolean;\n}\n\n/**\n * Compute a doc's fork status from hashes and flags. Pure and total; every\n * combination of inputs maps to exactly one {@link ForkStatus}.\n */\nexport function computeForkStatus(input: ForkStatusInput): ForkStatus {\n const none = { customized: false, stale: false, conflicted: false, orphaned: false };\n\n if (!input.inManifest) {\n // A file in the fork dir with no manifest entry is a hand-authored local doc;\n // otherwise the doc is simply served from upstream via the cache.\n return { state: input.forkFileExists ? 'local' : 'upstream', ...none };\n }\n\n if (!input.forkFileExists) {\n return { state: 'missing', ...none };\n }\n\n const customized = input.forkHash !== input.baseHash;\n const orphaned = input.cacheHash === undefined;\n const stale = !orphaned && input.cacheHash !== input.baseHash;\n const conflicted = Boolean(input.conflictedFlag && input.markersPresent);\n\n // Headline state for display. `customized` and `stale` can combine; the\n // renderer composes \"customized, stale\" from state + the stale modifier.\n let state: DocState;\n if (conflicted) {\n state = 'conflicted';\n } else if (orphaned) {\n state = 'orphaned';\n } else if (customized) {\n state = 'customized';\n } else if (stale) {\n state = 'stale';\n } else {\n state = 'forked';\n }\n\n return { state, customized, stale, conflicted, orphaned };\n}\n\n// =============================================================================\n// Manifest helpers (pure)\n// =============================================================================\n\n/** Find a fork entry by name, optionally constrained to a kind. */\nexport function findFork(\n manifest: ForkManifest,\n name: string,\n kind?: string,\n): ForkEntry | undefined {\n return manifest.forks.find((f) => f.name === name && (kind === undefined || f.kind === kind));\n}\n\n/** Return a new manifest with `entry` inserted or replaced (matched by kind+name). */\nexport function upsertFork(manifest: ForkManifest, entry: ForkEntry): ForkManifest {\n const forks = manifest.forks.filter((f) => !(f.name === entry.name && f.kind === entry.kind));\n forks.push(entry);\n forks.sort((a, b) => a.kind.localeCompare(b.kind) || a.name.localeCompare(b.name));\n return { forks };\n}\n\n/** Return a new manifest with the matching entry removed. */\nexport function removeFork(manifest: ForkManifest, name: string, kind?: string): ForkManifest {\n return {\n forks: manifest.forks.filter(\n (f) => !(f.name === name && (kind === undefined || f.kind === kind)),\n ),\n };\n}\n\n// =============================================================================\n// Paths\n// =============================================================================\n\nexport function forksDir(tbdRoot: string): string {\n return join(tbdRoot, DOC_FORKS_DIR);\n}\n\nexport function forksFilePath(tbdRoot: string): string {\n return join(forksDir(tbdRoot), FORKS_FILE);\n}\n\nexport function baseFilePath(tbdRoot: string, kind: string, name: string): string {\n return join(forksDir(tbdRoot), BASE_SUBDIR, kind, `${name}.md`);\n}\n\n// =============================================================================\n// Filesystem I/O\n// =============================================================================\n\nfunction isNotFound(err: unknown): boolean {\n return (err as NodeJS.ErrnoException | undefined)?.code === 'ENOENT';\n}\n\n/** The outer manifest shape, before per-entry validation. */\nconst ForkManifestEnvelopeSchema = z.object({\n forks: z.array(z.unknown()).default([]),\n});\n\n/**\n * Read the fork manifest, returning an empty manifest if none exists.\n *\n * Parsing is tolerant per entry: a malformed or unsafe entry (bad name/kind,\n * path-traversal attempt) is dropped with a warning rather than aborting the\n * whole read. This both fails closed on a crafted entry (it is never returned,\n * so commands never act on it; no out-of-tree deletes) and keeps one corrupt\n * entry from taking down status/update for every other fork.\n */\nexport async function readForkManifest(tbdRoot: string): Promise<ForkManifest> {\n let content: string;\n try {\n content = await readFile(forksFilePath(tbdRoot), 'utf-8');\n } catch (err) {\n if (isNotFound(err)) {\n return emptyManifest();\n }\n throw err;\n }\n const envelope = ForkManifestEnvelopeSchema.parse(parseYaml(content) ?? { forks: [] });\n const forks: ForkEntry[] = [];\n let dropped = 0;\n for (const raw of envelope.forks) {\n const parsed = ForkEntrySchema.safeParse(raw);\n if (parsed.success) {\n forks.push(parsed.data);\n } else {\n dropped++;\n }\n }\n if (dropped > 0) {\n process.stderr.write(\n `• Ignored ${dropped} invalid fork manifest entr${dropped === 1 ? 'y' : 'ies'} ` +\n `in ${FORKS_FILE} (bad name/kind or unsafe path). Run 'tbd doctor' to review.\\n`,\n );\n }\n return { forks };\n}\n\n/** Write the fork manifest (creating `.tbd/doc-forks/` as needed). */\nexport async function writeForkManifest(tbdRoot: string, manifest: ForkManifest): Promise<void> {\n await mkdir(forksDir(tbdRoot), { recursive: true });\n const yaml = stringifyYaml(manifest, { lineWidth: 0, sortMapEntries: false });\n await writeFile(forksFilePath(tbdRoot), yaml);\n}\n\n/** Read a base snapshot's content, or null if it is absent. */\nexport async function readBaseContent(\n tbdRoot: string,\n kind: string,\n name: string,\n): Promise<string | null> {\n try {\n return await readFile(baseFilePath(tbdRoot, kind, name), 'utf-8');\n } catch (err) {\n if (isNotFound(err)) {\n return null;\n }\n throw err;\n }\n}\n\n/** Write a base snapshot verbatim (creating parent dirs as needed). */\nexport async function writeBaseContent(\n tbdRoot: string,\n kind: string,\n name: string,\n content: string,\n): Promise<void> {\n const path = baseFilePath(tbdRoot, kind, name);\n await mkdir(dirname(path), { recursive: true });\n await writeFile(path, content);\n}\n\n/** Remove a base snapshot if present. */\nexport async function removeBaseContent(\n tbdRoot: string,\n kind: string,\n name: string,\n): Promise<void> {\n await rm(baseFilePath(tbdRoot, kind, name), { force: true });\n}\n"],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ import { C as withForkManifestLock, S as upsertFork, T as writeForkManifest, _ as normalizeLineEndings, a as FORK_KINDS, b as removeBaseContent, c as compareVersionsLoose, d as findFork, f as forksDir, g as isSafeDocName, h as hashContent, i as FORKS_FILE, l as computeForkStatus, m as hasUnresolvedConflict, n as CONFLICT_LABELS, o as ForkEntrySchema, p as forksFilePath, r as DOC_FORKS_DIR, s as baseFilePath, t as BASE_SUBDIR, u as emptyManifest, v as readBaseContent, w as writeBaseContent, x as removeFork, y as readForkManifest } from "./fork-manifest-ByU7U2do.mjs";
2
+
3
+ export { DOC_FORKS_DIR, FORKS_FILE, findFork, hasUnresolvedConflict, hashContent, readForkManifest, removeBaseContent, removeFork, withForkManifestLock, writeForkManifest };
@@ -1,3 +1,3 @@
1
- import { a as hasShortId, c as parseIdMappingFromYaml, d as resolveToInternalId, f as saveIdMapping, i as generateUniqueShortId, l as reconcileMappings, n as calculateOptimalLength, o as loadIdMapping, r as createShortIdMapping, s as mergeIdMappings, t as addIdMapping, u as resolveIdMappingConflicts } from "./id-mapping-687_UEsy.mjs";
1
+ import { a as hasShortId, c as parseIdMappingFromYaml, d as resolveToInternalId, f as saveIdMapping, i as generateUniqueShortId, l as reconcileMappings, n as calculateOptimalLength, o as loadIdMapping, r as createShortIdMapping, s as mergeIdMappings, t as addIdMapping, u as resolveIdMappingConflicts } from "./id-mapping-DAIeLKzm.mjs";
2
2
 
3
3
  export { loadIdMapping, mergeIdMappings, parseIdMappingFromYaml, reconcileMappings, resolveIdMappingConflicts, saveIdMapping };