Repository navigation
Expand file tree
/
Copy pathDESIGN.txt
More file actions
1099 lines (887 loc) · 53 KB
/
Copy pathDESIGN.txt
File metadata and controls
1099 lines (887 loc) · 53 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# dot design (v0.1.0)
The structural configuration schema is defined in SCHEMA.txt. This file
describes runtime semantics and design boundaries.
### product model
dot is a declarative bootstrap runner for personal development environments
and dotfiles. A user selects one globally named target and, optionally, one
globally unique inline profile node. When the target is omitted, dot infers it
only if exactly one configured target is compatible with the active
PlatformInfo. It derives one effective unresolved manifest and then explicitly
applies it, renders a dry-run, checks its providers, or lists structural
selection data.
dot deliberately prefers locally complete declarations over cross-environment
deduplication. Two targets may repeat the same package, provider, link, or
action. That repetition expresses that the environments are independently
understood and maintained.
dot understands only a small set of domain concepts:
- target and platform compatibility;
- an inline profile inheritance tree;
- provider-backed and manually installed packages;
- provider lifecycle and explicit package install units;
- symbolic-link reconciliation;
- generic command actions and small built-in HTTPS-to-local-path content
materialization;
- side-effect-free dry-run rendering;
- explicit provider-readiness diagnostics.
dot coordinates external providers and user actions, but it is not a package
manager. It does not access package repositories, resolve dependencies,
compare versions, select release artifacts, or implement package installation.
Fetch Content is a deliberately small one-shot materialization capability, not
a download, artifact, or cache manager. Its finite TOML schema is not a
general-purpose configuration or execution DSL.
### configuration acquisition and path context
All source acquisition is owned by `dot-cli`. The CLI acquires, parses, and
statically validates one manifest, then constructs the unchanged
source-independent ConfigFile consumed by `dot-core`. The core has no local
discovery, HTTPS, or Git acquisition behavior. Source selection is not a
cross-file import mechanism, repository search, or schema feature.
Without an explicit source, local discovery checks `.dot.toml` in the absolute
invocation directory, then `~/.config/dot/.dot.toml` on Linux and macOS or
`%APPDATA%\dot\.dot.toml` on Windows. The first filesystem entry that exists
is selected; later load, parse, or validation failure does not fall through.
An explicit local `--config SOURCE` bypasses discovery and may use any
filename.
For either local form, dot first converts the entry path to an absolute path
without following symbolic links; its parent becomes the lexical configuration
directory exposed by `${dot:config_dir}`. Loading eagerly calls
`std::fs::canonicalize` on the entry path before reading the file. The canonical
result's parent becomes `${dot:real_config_dir}`. A broken, dangling, or
otherwise unresolvable entry fails loading for every command. Reading and
subsequent parse or validation diagnostics continue to identify the selected
entry path.
The canonical configuration directory retains the spelling inherited from the
path returned by Rust and the host operating system. In particular, Windows
canonicalization may return a verbatim `\\?\` path; dot does not strip or
normalize that prefix. The retained directories are native paths. Like every
path-to-string resolver, a `dot` path resolver uses `Path::to_str()` without
lossy replacement and fails at resolution time if that exact path is not
Unicode-representable.
An HTTPS `--config SOURCE` is acquired exactly once per invocation and kept
only in memory. Only HTTPS is accepted as a URL scheme. Requests and redirects
remain HTTPS, no more than 10 redirects are allowed, the final status must be
2xx, and the body must be strict UTF-8. No cache, temporary file, conditional
request, or retry exists. A remote source supplies no entry or entity path, so
`${dot:config_dir}`, `${dot:real_config_dir}`, and `${dot:cwd}` are all the
absolute invocation directory.
The paired `--git REPOSITORY --git-worktree PATH` form makes `PATH` absolute
against the invocation directory and uses external Git with direct argv. A
missing worktree is cloned with the remote name forced to `origin`; normal Git
configuration, credential helpers, and transport behavior remain available.
An existing worktree is reused read-only only after dot validates that its
entry is a directory, it is a worktree root, and exactly one raw
repository-local `remote.origin.url` equals `REPOSITORY`. Every resulting
worktree, including a new clone, undergoes those root and origin checks before
dot loads the root `.dot.toml` with the local entry/entity semantics above.
Git acquisition is not a general Git manager: it does not fetch, pull,
checkout, select refs, choose clone depth, inspect dirty state, repair, or clean
up. Clone and validation failures leave any resulting state in place. Git may
rewrite a relative local repository spelling while recording `origin`;
literal validation can therefore fail, and the mismatch diagnostic shows
actual and expected values.
### target
A target is a complete base environment declaration.
- Target ids are globally unique within one configuration.
- The user may select a target by id. If target selection is omitted, dot
selects the target only when exactly one configured target matches the active
platform facts.
- Zero compatible targets and multiple compatible targets are errors when
target selection is omitted.
- A target owns its platform constraint, providers, packages, links, actions,
and nested profiles.
- Selecting a target without a profile uses only the target root declarations.
- Targets do not inherit from, include, reference, or merge with other targets.
- Duplicate declarations across targets are intentional and have no semantic
relationship.
Examples of independent target ids include arch, macos, and windows.
### platform compatibility
The target platform field defines compatibility for both inference and
execution.
- os is required. Known values include windows, macos, and linux.
- arch, distro, distro_family, and environment are optional refinements.
- Known environment values include native, wsl, and container.
- Different fields are combined with AND.
- Multiple values inside one field are combined with OR.
- Missing optional fields do not constrain compatibility.
- An explicitly selected incompatible target fails execution and provider
check before any action or filesystem mutation occurs.
- `list profiles` and `list jobs` may structurally inspect an explicitly
selected incompatible target; omitted-target inference still requires one
compatible target.
- A profile cannot change or weaken its target's platform constraint.
Execution never turns an incompatible target into a collection of skipped
declarations.
Runtime PlatformInfo uses Rust's compiled target `os` and `arch`. On Linux, dot
reads the first available `/etc/os-release` or `/usr/lib/os-release`: `ID`
provides distro and the whitespace-separated `ID_LIKE` values provide
distro_family facts. WSL is detected from its environment variables or kernel
release, and common container markers provide the container fact. Both facts
may be present; native is present only when neither WSL nor container is
detected. Non-Linux platforms currently have no distro or distro_family facts
and are classified as native.
### profile tree
Profiles are nested inline under a target. A profile may contain providers,
packages, links, actions, and more child profiles. It has no parent reference:
its lexical TOML parent is its only parent.
This makes every target profile structure a finite tree rather than a graph.
There is no extends field, cross-target reference, multiple inheritance,
diamond merge, topological sort, or inheritance-cycle detection.
A profile is selected directly by its id, regardless of its depth in the
target's inline tree. Every profile id must therefore be unique within one
target. Its selector-safe grammar excludes slash and `@`, so slash remains only
a path display separator and the reserved `@root` CLI token cannot collide with
a declared profile. dot finds the selected node and derives its unique lexical
path from the target root. Selecting no profile or selecting `@root` means the
target root. Selecting an unknown profile id is an error before side effects.
Exactly one profile node may be selected per invocation.
Any existing profile node may be selected; it does not need to be a leaf.
Sibling profiles are alternatives and do not see one another. A child profile
inherits every ancestor on its unique root path.
### profile merge
dot first constructs one effective manifest entirely in memory. Starting at
the target root, it overlays each selected profile node from parent to child.
providers, packages, links, and actions are four independent maps keyed by
their declaration ids. Their merge rule is identical:
- a new id adds one record;
- an existing id is silently and completely replaced by the deeper record;
- records are atomic and fields are never recursively merged;
- replacement does not emit a warning;
- deletion, disabling, tombstones, and remove lists do not exist.
The deepest declaration on the selected path wins. Ancestor declarations that
were replaced are not interpolated, executed, or displayed by dry-run.
Unselected siblings and descendants do not participate.
The tree should be organized so a declaration lives at the nearest common
ancestor of every profile that needs it. If two unrelated branches need the
same declaration but their common ancestor should not contain it, the user
duplicates that declaration in the two branches. If a child does not want an
ancestor declaration, the tree is structured incorrectly; dot provides no
subtractive inheritance operation.
All TOML records must deserialize according to SCHEMA.txt. Configuration load
then statically validates every declaration in the complete tree, including
unselected and replaced records. It also validates provider references,
package-batch structure, and provider-args requirements against the target root
and every root-to-profile effective merge. Runtime expression values remain
unresolved until an execution scope and exact job closure have been selected.
The effective manifest is the unresolved internal value shared by list jobs,
dry-run, provider check, and apply. It is not a persistent receipt, a public
serialization format, or a separate user-facing IR.
### package
A package is one named installation intent within the effective manifest.
There is no cross-target or cross-profile package identity.
A package has exactly one of two forms:
provider package:
- Declares exactly one provider id and is strongly typed as either single or
batch. The Rust schema represents these as distinct enum variants rather than
one structure with an optional names field.
- A single provider package has no names field. Its package table key is both
its stable logical id and the exact package name passed to the provider.
- A batch provider package has a required non-empty names list. Its package
table key is a stable logical batch id used by profile replacement and
reporting; names contains the exact package names passed to the provider.
Duplicate names within one batch are invalid.
- The two TOML forms remain untagged: no `type` discriminator is required.
Structural deserialization produces the strong Rust enum before planning,
and runtime code never determines the variant by testing whether names is
absent or empty.
- Names that are not valid TOML bare keys can use quoted keys. Batch names are
identifier values and may likewise be quoted as TOML strings.
- provider_args is an optional ordered list of literal argv elements passed to
the provider install action. Its default is an empty list and one value
applies to every name in that install unit.
- The referenced provider must exist in the final effective provider map.
- Each provider package declaration is exactly one execution and report unit.
Single declarations invoke provider install with one name. Batch
declarations invoke it once with the complete names list. dot never coalesces
separate declarations into one install invocation or report unit, even when
provider and provider_args match. Provider-backed units are grouped by
provider only to determine execution order.
- Profile inheritance replaces the complete single or batch record by its
stable table key. Batch names are replaced as one list, never merged or
appended across profile levels.
For example:
```
ripgrep = { provider = "scoop" }
cli-tools = { provider = "scoop", names = ["bat", "fd", "fzf"] }
```
The first declaration installs and reports `ripgrep`. The second installs the
three names together and reports one logical item named `cli-tools`, with its
names shown as detail.
manual package:
- Declares one Command Action install and no provider. Fetch Content is not a
manual-package install form.
- The package table key is a diagnostic id for the desired tool.
- check can make the install idempotent; without check, exec runs on every
apply.
- Rich transfer, architecture selection, verification, extraction, and layout
logic should normally live in an external shell or PowerShell script invoked
by the install action.
dot does not create a manual provider. Manual installation is package-specific
and has no common batching protocol.
A provider package install unit cannot name multiple providers. There is no
all-of, fallback, winner, preferred source, provider-specific names map,
adoption, or inactive assignment. If related content needs two installation
mechanisms, it is represented by two explicit package records.
dot stores no package receipts or installed-state database. Provider install
commands must tolerate already installed package names. Manual packages rely
on their declared check or on an idempotent exec implementation.
### provider
A provider is a reusable installation capability inside one target/profile
tree, such as pacman, paru, apt, brew, scoop, npm, or cargo. Provider ids are
resolved only from the final effective provider map.
A profile may add or completely replace a provider. This allows a specialized
profile to introduce a capability without making it part of sibling profiles.
Every selected provider completes activation, probing, and any ensure lifecycle
before any selected manual or provider-backed package is executed. Complete job
selection uses JobSelection::All and includes every effective provider,
including providers with no packages. Exact job selection uses
JobSelection::Only and includes a provider only when it is required by a
selected provider-backed package. Providers cannot be selected directly.
Provider install is never called with an empty names list.
### provider lifecycle
activate:
- Is a pure environment patch for child processes started by the current dot
process.
- Does not persistently edit the user's shell environment.
probe:
- Takes no package input.
- Exit code 0 means the provider is ready.
- A non-zero exit or a failure to start the probe may be repaired by ensure.
- A failure while preparing the probe or applying activate is a provider error
and does not run ensure.
ensure:
- Takes no package input.
- Runs only when the initial probe fails in an ensureable way.
- Is optional. A failed probe with no ensure makes the provider unavailable.
- A list runs in declaration order and stops at the first non-zero exit.
- If one ensure action or the complete ensure list succeeds, dot reapplies
activate and probes once more.
- The provider is ready only when the second probe succeeds.
install:
- Receives the names and provider_args of one declared provider-package install
unit.
- Runs once for each single or batch declaration assigned to that provider.
One provider can therefore execute multiple installs after being activated,
probed, and ensured only once.
- Exit code 0 succeeds the install unit.
- A non-zero exit fails only that install unit. For a batch, dot does not infer
partial success or retry individual names. Later unrelated install units
still run.
- The declared command owns idempotency for packages already installed.
### provider dependency boundary
- Providers are top-level bootstrap capabilities within the effective
manifest.
- dot does not build or resolve dependencies between providers.
- ensure cannot reference another provider as a semantic dependency.
- An ensure command may invoke arbitrary programs, but their availability is
part of that provider's bootstrap contract.
- dot does not reorder providers to satisfy undeclared dependencies.
- One provider failure does not prevent unrelated providers from being
processed.
### provider environment
- Environment paths and values support built-in interpolation.
- dot does not refresh Windows User or Machine PATH from the registry after
ensure. A provider must declare enough activate.path_prepend information for
its newly installed program to be found by the current dot process.
- On Unix, dot does not source .profile, .bashrc, .zshrc, or other shell files.
- An exec action may declare its own environment patch.
- The effective child environment is built in this order: current dot process
environment, provider activate patch when applicable, then exec-action patch.
- An action variable overrides a provider variable with the same name.
- Action path_prepend entries precede provider PATH entries. path_append
entries are placed at the end of the effective PATH.
- Global and manual-package actions have no implicit provider environment.
### exec action
Exec is the generic process action. Package-manager-specific and installer-
specific knowledge remains in TOML or external scripts rather than Rust code.
- program and args are executed directly without an implicit shell.
- Pipes, redirects, &&, shell expansion, and shell quoting are not interpreted.
- Shell behavior must be requested explicitly by running bash, pwsh, or another
interpreter and supplying its command arguments.
- Exit code 0 means success unless a containing operation defines a more
specific meaning.
- Probe and check commands use captured stdout and stderr. Provider ensure and
install, action exec, and manual-package exec inherit the terminal streams so
the user sees interactive output as it happens.
- Captured output may be stored as report evidence, but the table renderer does
not dump it verbatim and it is not a v0.1.0 output contract.
### actions
A global Action is one strict structural variant with no discriminator:
Command Action or Fetch Content Action. Each variant rejects unknown fields,
and objects that mix the two field sets are rejected. Manual-package install
accepts Command Action only.
Both global variants occupy the same stable action phase and are ordered by
final effective action declaration position. A failure is isolated to that
action: later actions still run, and the deferred link phase still runs after
the complete action phase. Every failure or blocked result contributes to the
aggregate non-zero apply status. Actions have no dependency graph or
topological scheduling beyond this guaranteed serial declaration order.
### command action lifecycle
The same Command Action structure is used by global command actions and
manual-package install.
- check is optional. Without check, exec runs on every apply.
- A check exit code of 0 means the desired state is already satisfied, so exec
is skipped.
- A check exit code of 1 means the desired state is not satisfied. dot runs
exec and then checks once more.
- Any other check exit code means check failed.
- If the post-exec check does not return 0, the action fails even when exec
returned 0.
- dot owns only this control behavior. The user owns check correctness and
action idempotency.
- No action state is persisted.
Global command actions run after package installation. A manual-package
Command Action follows the same lifecycle but its result is reported as that
package's result.
### fetch content action
Fetch Content materializes one resolved resource at one resolved destination.
The fields are semantic resource locators so later versions can add explicit
locator pairs without changing the Action model. The currently supported pair
is deliberately narrow:
- source must be an explicit HTTPS URL with an authority and host;
- URL userinfo is rejected;
- target must be a native local-disk path. A target URL is not supported.
Source and target string expressions are evaluated only when their action is
in the selected execution closure. Resolution is pure planning: it performs no
network request and does not inspect the target. An absolute target remains
absolute. A relative target uses the path context represented by
`${dot:config_dir}`: the selected entry directory for a local or Git source,
or the invocation directory for HTTPS. `${dot:real_config_dir}` explicitly
selects the canonical directory for a local or Git source and the same
invocation directory for HTTPS.
Dry-run resolves the HTTPS source and local target, and its human table
displays that resolved pair, but it performs no network request and no target
inspection. On apply, an action eligible to transfer always fetches its source
fresh. There is no checksum or cache state, conditional request, retry, resume,
decompression, or archive extraction. Richer transfer behavior belongs in a
Command Action or an external script.
Apply classifies the final target component without following a symbolic link:
```
target state on_conflict=error on_conflict=replace
missing create create
regular file reject replace
symbolic link reject replace
directory reject reject
special entry reject reject
```
`replace` is not transactional, even without concurrent target mutation. Its
transfer and flush complete in the staging file first, but commit removes the
existing regular file or symbolic link before installing the staged file. If
that final install fails, the target can be absent; dot does not restore the
original entry.
Thus `replace` permits only an existing regular file or symbolic link. A
directory or other special filesystem entry is always rejected. Missing parent
directories are created when the transfer proceeds.
Content is transferred into a staging file in the target's directory, flushed,
and then committed to the target. Before commit, dot reinspects the final entry
without following a symbolic link. HTTPS-only policy also applies through
redirects, so an insecure downgrade is rejected; at most five redirects are
followed, and only a final 2xx HTTP status is successful.
These target-kind guarantees assume cooperative concurrency: no other writer
mutates the target concurrently. Commit-time reinspection is best effort and
narrows accidental races, but it is not an atomic compare-and-swap with the
following removal or persistence. Concurrent mutation can make the action fail
or can cause it to affect a competing directory entry. dot provides no
cross-platform filesystem-object-identity or rollback guarantee, so the
same-directory staging design must not be described as universally atomic or
race-safe.
The human report detail shows the resolved source URL and target path. A
successful Fetch Content Action is CREATED or REPLACED. A failure records
Fetch-stage evidence; the action detail and evidence together preserve the
source, target, and materialization-stage failure detail. Because no child
process supplied it, dot does not invent an exit code.
### links
Each effective link record is one native symbolic-link intent from source to
target. Link ids are used for map replacement, diagnostics, and result
reporting. They are not referenced by other declarations.
- A relative source is resolved against `${dot:config_dir}`: the selected
entry directory for a local or Git source, or the invocation directory for
HTTPS. It is not implicitly rebased to `${dot:real_config_dir}`. An absolute
source is used as declared.
- target must resolve to an absolute path.
- During apply, source must exist and be a regular file or directory.
- v0.1.0 creates native symbolic links only. It does not fall back to hard
links, directory junctions, copies, or shell commands.
- The implementation determines whether source is a file or directory and uses
the corresponding native Windows link operation.
on_conflict controls an existing target that is not already the desired link:
- The default is replace-link.
- error fails for an incorrect symbolic link.
- replace-link replaces an incorrect or broken symbolic link.
- A regular file or directory always fails. Neither policy replaces ordinary
user data.
on_missing_parent controls an absent target parent directory:
- The default is create, which recursively creates the parent.
- skip treats the link as currently inapplicable and performs no mutation.
The two policies are independent. A link already resolving to the desired
source is satisfied. After creation or replacement, dot reads the link back and
verifies the relationship.
Path identity is based on normalized native paths rather than raw strings:
- source is made absolute and canonicalized after existence is verified;
- target is made absolute, its nearest existing ancestor is canonicalized, and
a nonexistent suffix is appended using lexical normalization;
- the final target component is not followed while inspecting an existing link;
- a relative destination stored in a link is resolved against the target's
parent;
- Windows separator and case equivalence are respected.
All selected link paths are resolved before link mutation starts. Two selected
links that resolve to the same target are a declaration conflict and prevent
the selected link phase from starting. Only this duplicate-target conflict is
a phase error that reports every selected link as blocked. Target
normalization, source preparation, and other per-link failures are isolated,
do not prevent later nonblocked links from being reconciled, and contribute to
the final non-zero result. Link reconciliation uses Rust filesystem APIs rather
than child commands.
The filesystem is authoritative. dot stores no persistent link state. Removing
a link declaration does not remove an existing link. v0.1.0 has no unlink,
garbage collection, or link query operation.
### interpolation
Interpolation uses an OmegaConf-like resolver surface:
${resolver:payload}
The parser treats resolver and payload generically. Resolver names are not
matched ad hoc by schema fields or execution stages. Every built-in resolver is
declared once in a closed, static registry using the same definition shape:
- a fixed name;
- its scalar or string-list output kind;
- the contexts in which it is available;
- a pure payload-validation function;
- a resolution function that reads an explicit resolve context.
Parsing produces a generic resolver call. Validation and evaluation both look
up that call in the same registry, so resolver-name dispatch is not duplicated
and payload validation stays attached to the resolver definition. Unknown
names fail lookup. Adding a resolver is an internal Rust change that requires
one complete static definition and tests.
The registry is an implementation framework, not a configuration extension
point. Users cannot register resolvers from TOML, scripts, plugins, environment
variables, or command-line arguments. v0.1.0 does not use a mutable global
registry, dynamic loading, or trait-object plugins.
v0.1.0 statically defines only these resolver calls:
- ${env:NAME}: read NAME from the current effective environment;
- ${package:names}: the complete package-name list for the current provider
install unit;
- ${package:provider_args}: the provider argument list for that install unit;
- ${dot:config_dir}: the local/Git entry directory, or the absolute invocation
directory for HTTPS;
- ${dot:real_config_dir}: the local/Git canonical entity directory, or the
absolute invocation directory for HTTPS;
- ${dot:cwd}: the absolute invocation directory;
- ${xdg:home}: the current user's home directory;
- ${xdg:config} and ${xdg:config_local}: the standard configuration directory
and its local/non-roaming counterpart;
- ${xdg:data} and ${xdg:data_local}: the standard data directory and its
local/non-roaming counterpart;
- ${xdg:cache}, ${xdg:state}, ${xdg:runtime}, and ${xdg:executable}: the
corresponding standard user directories when the current platform defines
them;
- ${xdg:documents}: the current user's Documents directory when available.
The `dot` resolver describes this invocation of dot. HTTPS supplies no entry or
canonical entity; both configuration-directory values equal `${dot:cwd}` for
that source. The `xdg` resolver is the portable configuration vocabulary for
standard user directories: it follows XDG base and user directories on Linux
and their platform-standard equivalents on Windows and macOS. An `xdg` value
that the current platform does not define is an error when resolved; dot does
not invent a fallback path.
Resolver availability is contextual:
- links, both global Action variants, manual-package Command Actions, provider
activate, probe, and ensure can use env, dot, and xdg;
- provider install can use env, dot, xdg, package:names, and
package:provider_args.
Resolution receives an explicit context containing the current effective
child environment, lexical and canonical configuration directories, captured
invocation directory, a snapshot of standard user directories, and an optional
provider-package install unit.
Environment patches are resolved against the effective environment that exists
immediately before that patch is applied. Later stages therefore observe
successful provider activation changes without introducing implicit global
environment mutation.
package:names and package:provider_args are string lists. Each resolver must
occupy one complete args element, and dot expands it to one or more package-name
argv elements or zero or more provider-argument elements. For a single install
unit package:names contains the table key; for a batch it contains the declared
non-empty names list. package:provider_args preserves declaration order; an
omitted field expands to zero elements. If any package assigned to a provider
has non-empty provider_args, that provider's install action must contain
exactly one ${package:provider_args} element. provider_args values are literal:
v0.1.0 does not interpolate their contents, and interpolation syntax in them
is an error. Scalar interpolation may be a complete string or part of a larger
string. A list value cannot be embedded inside another string.
Every source expression is syntactically and type-promoted during complete
configuration loading. Unknown resolvers, unsupported payloads, malformed
syntax, invalid list-variable placement, and resolvers used in an invalid
source role are errors even in unselected targets, unselected jobs, or replaced
declarations.
Profile merging and exact job selection happen before runtime resolver
evaluation. Only the selected provider/job closure reads environment values,
resolves dot or XDG paths, or injects provider/package context. A missing
environment variable or unavailable path in an unselected job does not block
an exact selection; the same missing value in the selected closure is a
planning error. Values never silently become an empty string.
v0.1.0 intentionally omits resolver defaults, nested interpolation,
configuration-node references, user-defined resolvers, expressions, automatic
type conversion, and caching. Shell substitution such as $() and backticks has
no implicit meaning, and resolved values are never reinterpreted by a shell.
### execution plan and dry-run
The planning pipeline has one direction:
```
whole-configuration static validation
-> unresolved target/profile EffectiveManifest
-> JobSelection validation and provider closure
-> selected runtime resolution
-> one owned ExecutionPlan
-> dry-run projection or apply execution
```
ExecutionPlanner receives the unresolved effective manifest and a
JobSelection. It validates the entire requested selector set before runtime
resolution. JobSelection::All includes every effective provider, package,
action, and link. JobSelection::Only includes the exact selected
package/action/link identities and the minimum provider closure required by
selected provider-backed packages. Providers are internal requirements and
cannot be selected directly. The BTreeSet inside JobSelection::Only is a
membership structure only; its iteration order is not a scheduling source.
The planner resolves only that closure and returns one owned ExecutionPlan.
There is no post-plan selection view and no second selected-plan type. Its sole
job storage is one stable ordered sequence of typed Provider, Package, Action,
and Link jobs. Phase-specific accessors are projections over that sequence, not
parallel aggregate storage. Provider-backed package jobs preserve the
single-versus-batch distinction.
The effective manifest applies the target, then lexical profile ancestors from
outermost to innermost, then the selected profile. A same-id whole-record
replacement removes the ancestor position and takes the deeper declaration's
position. Those final effective declaration positions define the exact stable
phase and subphase order:
```
providers by final effective provider declaration order
manual packages by final effective package declaration order
provider-backed packages grouped by final effective provider declaration order,
then by final effective package declaration order within each group
actions by final effective action declaration order
links by final effective link declaration order
```
Entries<T> and SelectableEntries<T> preserve keyed-table declaration order.
Exact selection filters the final sequence without reordering it; selector
occurrence order does not schedule work. Every selected provider completes its
readiness lifecycle before the manual-package subphase begins, and each ensure
list retains its list order.
ExecutionPlan is an internal Rust data model, not a serialized configuration
format or public compatibility boundary. Runtime observations such as probe or
check results, provider readiness, and filesystem state are execution state and
are not stored in the plan.
Apply passes `&ExecutionPlan` directly to JobRunner, records runtime outcomes,
and builds its CommandReport from the same plan facts. Dry-run projects that
same plan into PLANNED report items without invoking JobRunner. A report is a
view, not another executable plan.
For each selected provider, planning resolves and applies activate only to an
in-memory child environment, then uses that environment to resolve the
provider's probe, ensure, and selected install units. This is ordinary data
transformation and does not start a process or modify the parent process
environment. Global and manual actions resolve against the captured dot process
environment and do not inherit an implicit provider environment.
Provider install commands are fully resolved once per selected Single or Batch
install unit, including list-valued package interpolation. A Single uses the
package key as its item name; a Batch uses its stable logical key and retains
its complete names list. A selected Fetch Content Action resolves its semantic
source and target locators during this same pure planning stage. Its currently
supported HTTPS source is validated without making a request, and its local
target is made absolute by retaining an absolute path or joining a relative
path to `${dot:config_dir}`.
Relative link sources are likewise joined to `${dot:config_dir}`, not
`${dot:real_config_dir}`, and link targets must resolve to absolute paths. A
repository-relative source or Fetch Content target behind a symlinked local or
Git configuration must explicitly use `${dot:real_config_dir}`. On Unix and
macOS, for example:
```
source = "${dot:real_config_dir}/home/.config/nvim"
```
On Windows, canonicalization may yield a verbatim `\\?\` path. A textual `/`
suffix is not a Windows separator in that path form, so the suffix uses
backslashes; a TOML literal string avoids basic-string escape processing:
```
source = '${dot:real_config_dir}\home\.config\nvim'
```
Dry-run does not probe, ensure, or install providers; run manual-package or
global Command Action checks and execs; transfer Fetch Content; inspect Fetch
Content or link target state; or reconcile links. It does not test link-source
existence, canonicalize link paths, inspect conflicts, or modify the
filesystem. Configuration acquisition has nevertheless already completed,
including entry canonicalization for a local or Git source, before dry-run
planning begins. Its output describes selected intent, not runtime outcome: a
provider may be missing, a package may already be installed, a command check
may skip exec, a Fetch Content target may conflict, and a link may already be
satisfied or may fail during apply.
Selected planning is atomic. Every selector is valid and every selected
provider and job is resolved before an ExecutionPlan exists, so apply executes
nothing after a selection or resolution error. Reading selected environment
values and built-in path context is permitted during planning; those
observations do not modify managed state. The ExecutionPlan, CommandReport, and
human table formatting are not stable machine-readable contracts.
### provider check
Provider check is a separate, explicitly active diagnostic command:
dot check providers --target arch --profile laptop
It is intended for developing and debugging provider declarations against the
current environment. Unlike `dot dry-run`, it may execute external probe
commands and therefore cannot guarantee freedom from side effects. The
configuration author owns the observational behavior of every launched probe.
Provider check performs these operations:
1. Parse and statically validate the complete configuration, including every
source expression and every effective root/profile provider reference.
2. Detect the current platform and select the target, requiring exactly one
compatible target when the target is omitted.
3. Resolve the requested inline profile node and its ancestor path.
4. Overlay target and profile nodes from root to the selected node.
5. Independently attempt each effective provider: resolve and apply activate,
then resolve and prepare its probe. A provider-local failure at any of these
stages records NOT_READY before process launch and does not stop the next
provider.
6. When preparation succeeds, execute that provider's probe process at most
once.
7. Build one report item per provider and render its readiness. Exit codes and
captured stdout/stderr remain structured evidence; the current table shows a
compact message or exit code rather than dumping captured streams.
Every effective provider is attempted, including a provider with no assigned
packages. Activation, probe resolution/preparation, process-launch, and probe
result failures do not stop unrelated providers from being attempted.
The result model has two readiness states:
- READY: a prepared probe process exits with code 0;
- NOT_READY: activation, probe resolution/preparation, process launch, or the
executed probe fails.
If no effective providers exist, provider check reports an empty result and
exits 0. Otherwise it exits 0 only when every provider is READY and exits
non-zero when any provider is NOT_READY. Process-start errors retain their
diagnostics even though they map to NOT_READY.
Provider check does not run provider ensure or install, refresh environment
state after an ensure, resolve or inspect package install units, run
manual-package or global Command Action check/exec, transfer Fetch Content, or
inspect/reconcile links. v0.1.0 defines no link, action, package, or aggregate
check command.
Provider check remains an independent probe-only path. It does not construct a
complete ExecutionPlan or invoke JobRunner. All declarations have already
passed static expression promotion, but missing runtime values in ensure,
provider install, manual-package, action, or link fields remain unevaluated.
Runtime activation, interpolation, preparation, process-launch, and probe
failures remain provider-local and do not stop later provider attempts.
### command surface
The v0.1.0 command facade is explicit:
```
dot [--config SOURCE] <COMMAND>
dot --git REPOSITORY --git-worktree PATH <COMMAND>
COMMAND:
apply
dry-run
check providers
list targets [--all]
list profiles
list jobs
```
There is no default operation. `dot` without a subcommand prints help and is an
error. `--config SOURCE` is global, accepts a local path or HTTPS URL, and
conflicts with `--git`. `--git` and `--git-worktree` are global and require one
another. With none of those options, the CLI performs local discovery. Its
typed ConfigRequest is resolved exactly once before operation dispatch; any
acquisition, parse, or static-validation failure therefore occurs before list,
check, dry-run, or apply begins. The acquisition policies and path context are
defined above. Native local discovery is not affected by an injected
PlatformInfo.
The configured-target option `--target TARGET` and the profile option belong to
`apply`, `dry-run`, `check providers`, and `list jobs`. `list profiles` accepts
only the target option. `list targets` accepts only `--all`. Target may be
omitted only when exactly one configured target is compatible. Profile
omission and `--profile @root` both select the target root.
Apply and dry-run additionally accept repeatable exact job options:
```
--job package:ID
--job action:ID
--job link:ID
```
No `--job` occurrences means JobSelection::All. One or more mean
JobSelection::Only. Bare IDs, provider selectors, unknown kinds, malformed
selector identifiers, and duplicate occurrences are errors. Providers are not
selectable.
Development builds of the `dot-cli` package may enable the
`dev-platform-override` feature. Its
global `--platform` inline TOML argument controls compatibility for dry-run,
provider check, target-list labels/filtering, and omitted-target inference in
profile/job lists. Commands, environment, XDG paths, and filesystem
observations still come from the host. Apply ignores the override and always
detects the host platform. Each case prints a warning so simulated platform
facts cannot be mistaken for full platform emulation. Without the feature,
`--platform` is absent from the CLI.
### structural lists and TSV
List commands are a stable machine-facing query surface. They load and
statically validate the complete configuration, then stop at structural,
unresolved data. They may detect host compatibility facts, which can read OS
or container metadata for target labels, filtering, and inference. They do not
inspect managed link or source state, resolve runtime values, or run commands.
Their shared configuration load still completes source acquisition before
inspection, including entry canonicalization for a local or Git source.
`list targets` returns only compatible targets by default; `--all` returns
every target with a compatibility label. `list profiles` returns `@root` and
the target's profile tree. `list jobs` returns the effective merged packages,
actions, and links and never returns provider rows. An explicit incompatible
target is permitted only for structural `list profiles` and `list jobs`;
execution and provider check reject it.
Structural inspection has no stable row-order contract. List rows may naturally
reflect current containers or traversal, but callers must not infer execution
order or depend on target-ID, sibling-ID, or job row order.
Every list command writes UTF-8, headerless TSV with one record per line and a
fixed number of columns:
```
list targets:
TARGET COMPATIBILITY OS ARCH DISTRO DISTRO_FAMILY ENVIRONMENT
list profiles:
PROFILE PATH DEPTH
list jobs:
SELECTOR KIND ID VIA DETAIL
```
The target columns have these stable meanings:
- TARGET is the typed target ID.
- COMPATIBILITY is `compatible` or `incompatible` relative to the active
PlatformInfo.
- OS, ARCH, DISTRO, DISTRO_FAMILY, and ENVIRONMENT are the target's typed
platform constraints. A One value is emitted directly, a Many value is
comma-joined in its configured order, and an absent optional constraint is
an empty field. OS is required by the schema and is therefore never absent.
The profile columns have these stable meanings:
- PROFILE is `@root` for the target root or the typed profile ID for a declared
profile.
- PATH is `<root>` for the target root or the slash-joined typed profile IDs
from the target root to that profile.
- DEPTH is numeric: `0` for the root and otherwise the number of profile IDs in
PATH.
The job columns have these stable meanings:
- SELECTOR is the canonical reusable `package:ID`, `action:ID`, or `link:ID`.
- KIND is `package`, `action`, or `link`, matching SELECTOR.
- ID is the typed package, action, or link ID.
- VIA is the provider ID for a provider-backed package, `manual` for a manual
package, `exec` for a Command Action, `fetch` for a Fetch Content Action, or
`builtin` for a link.
- DETAIL is the effective package name (the job ID) for a provider-backed
Single, the effective provider-backed Batch names comma-joined in configured
order, the deserialized unresolved install or exec program source for a
manual package or Command Action, or the unresolved `source -> target` pair
for a Fetch Content Action or link.
The canonical reusable selector is the first field and is emitted verbatim.
Backslash, tab, carriage return, and newline in later display fields are
escaped as `\\`, `\t`, `\r`, and `\n`. Later fields are stable display
columns with the meanings above, not a reversible serialization of the
complete domain record.
Every typed record and all of its fields are prepared before stdout is written.
Configuration, validation, or scope errors therefore leave stdout empty. A
downstream BrokenPipe is successful termination, which supports consumers that
stop after enough records.
This TSV boundary is intentionally sufficient for fzf and other external
selectors. An external wrapper may pipe list jobs into `fzf --multi`, take each
selected first field, and pass repeated `--job` arguments to apply or dry-run.
dot never invokes, configures, detects, or depends on fzf, and it never reads a
selection from stdin. Keeping selection outside dot leaves apply stdin attached
to the terminal for interactive child commands.
### command reports and rendering
Every successfully constructed command result is represented in memory by one
CommandReport before presentation. It contains:
- the command kind: dry-run, apply, or check providers;
- context: target, optional profile, and PlatformInfo;
- overall status: PLANNED, SUCCESS, or FAILED;
- one logical item per provider or user job in the selected execution closure,
or one item per effective provider for provider check;
- per-item typed subject data, status, and zero or more evidence records;
- command-level diagnostics.
Evidence identifies an execution stage and may carry an exit code, message, or
captured stdout/stderr. Apply item statuses distinguish READY, INSTALLED,
SATISFIED, EXECUTED, CREATED, REPLACED, SKIPPED, NOT_READY, BLOCKED, and FAILED.
Dry-run items are PLANNED. A provider single is one report item whose id is its
package key. A provider batch is one report item whose id is its stable logical
key and whose detail contains its names. A batch outcome is intentionally not
projected onto synthetic per-name rows because dot cannot observe partial
success inside one provider invocation. Aggregate package counts count declared
install units, so one named batch counts as one package item regardless of how
many names it contains.
The current renderer writes a human-readable table with TYPE, ITEM, VIA, STATUS,
and DETAIL columns followed by aggregate counts. It colors status cells only
when stdout is a terminal. Child commands using inherited I/O may write before
the final apply table.