artshelf 0.10.2 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +57 -0
- package/README.md +5 -0
- package/SPEC.md +169 -3
- package/dist/src/commands/index.js +4 -0
- package/dist/src/commands/put.js +1 -1
- package/dist/src/commands/reconcile.js +48 -0
- package/dist/src/commands/shared.js +17 -0
- package/dist/src/ledger.js +245 -188
- package/dist/src/locks.js +73 -0
- package/dist/src/provenance.js +142 -0
- package/dist/src/reconcile.js +332 -0
- package/dist/src/registry.js +3 -41
- package/dist/src/shared/help-text.js +26 -0
- package/docs/reference.html +26 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -79,6 +79,63 @@
|
|
|
79
79
|
cache, `ARTSHELF_NO_UPDATE_CHECK_TTL_MS` overrides the no-update/failed TTL
|
|
80
80
|
(falling back to `ARTSHELF_UPDATE_CHECK_TTL_MS` for compatibility), and a
|
|
81
81
|
non-numeric TTL value falls back to the default instead of disabling expiry.
|
|
82
|
+
- Made concurrent ledger and registry writes safe: ledger mutations now take the
|
|
83
|
+
same cross-process advisory lock as the registry (extracted into a shared
|
|
84
|
+
`withPathLock` helper in `src/locks.ts`), and ledger appends and rewrites commit
|
|
85
|
+
through a unique temp file and an atomic rename, so overlapping `put`,
|
|
86
|
+
`resolve`, and cleanup runs no longer drop records or leave a partially written
|
|
87
|
+
ledger.
|
|
88
|
+
- Hardened `cleanup --execute` to reject unsafe plan ids and bind the loaded plan
|
|
89
|
+
to the request before any filesystem mutation: the plan's `planId` must match
|
|
90
|
+
the requested id, its `ledgerPath` must match the executing ledger, and its
|
|
91
|
+
entries must be well-formed, so mismatched or malformed plans are refused before
|
|
92
|
+
moving files or writing a receipt — the plan-id-bound posture trash purge
|
|
93
|
+
already enforces.
|
|
94
|
+
- Added path provenance to new ledger records: each record now captures a
|
|
95
|
+
`provenance` block (root class, root-relative path, basename, path kind, and an
|
|
96
|
+
optional byte-size fingerprint) so a later reconcile can rebuild a moved
|
|
97
|
+
artifact's path after a root rename without a daemon, watcher, or shell hook.
|
|
98
|
+
Provenance is additive and backward compatible — records written before it
|
|
99
|
+
simply omit the field and still validate, read, list, find, and get as legacy
|
|
100
|
+
rows, while `validate` reports a malformed provenance block only when the field
|
|
101
|
+
is present.
|
|
102
|
+
- Added the approval-gated `artshelf reconcile` command for ledger/registry
|
|
103
|
+
housekeeping that never creates, moves, or deletes files. `--dry-run`
|
|
104
|
+
classifies recorded-path drift into a reviewed plan (`remap`, `resolve-missing`,
|
|
105
|
+
`resolve-stale-trash`, or `blocked`), writing and registering an Artshelf-owned
|
|
106
|
+
plan only when actionable entries exist and reusing a matching plan id
|
|
107
|
+
otherwise, and `--all` previews every registered ledger as dry-run only.
|
|
108
|
+
`--execute` applies exactly one reviewed `--plan-id` against one explicit
|
|
109
|
+
`--ledger`, refuses missing, unknown, or mismatched plans and entries whose live
|
|
110
|
+
state drifted since review, stamps the reconcile audit trail (`previousPath`,
|
|
111
|
+
`reconcilePlanId`, `reconcileReceiptPath`, `reconciledAt`, `reconcileReason`) on
|
|
112
|
+
every touched row, and writes an Artshelf-owned reconcile receipt.
|
|
113
|
+
|
|
114
|
+
## [0.12.0](https://github.com/calvinnwq/artshelf/compare/artshelf-v0.11.0...artshelf-v0.12.0) (2026-06-15)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
### Features
|
|
118
|
+
|
|
119
|
+
* **ledger:** add path-provenance foundation for NGX-436 ([f0bf797](https://github.com/calvinnwq/artshelf/commit/f0bf797223e5032e326842cc1dd8fcb47130ed3e))
|
|
120
|
+
* **ledger:** add provenance validation distinguishing legacy from malformed rows (NGX-436) ([ce5128a](https://github.com/calvinnwq/artshelf/commit/ce5128a85bcc6353f73c3b47bd9a433918236ee0))
|
|
121
|
+
* **reconcile:** add path-provenance foundation and approval-gated reconcile command ([ad4bcec](https://github.com/calvinnwq/artshelf/commit/ad4bcec7839e004a0f7ca3cc9a8ecebb0caaac0f))
|
|
122
|
+
* **reconcile:** add read-only classification engine for NGX-437 ([3245738](https://github.com/calvinnwq/artshelf/commit/3245738c8d7b3e3dfd95c49fae414596b35c22e1))
|
|
123
|
+
* **reconcile:** add reconcile dry-run plan layer for NGX-437 ([ddc8881](https://github.com/calvinnwq/artshelf/commit/ddc8881713d26f1e43575b3097e49558c22cc2e7))
|
|
124
|
+
* **reconcile:** add reconcile execute layer with audit trail and stale-state refusals (NGX-437) ([50a12d4](https://github.com/calvinnwq/artshelf/commit/50a12d49cdf0552b675bfaa75a0220c886bd64e5))
|
|
125
|
+
* **reconcile:** wire reconcile CLI command with integration tests (NGX-437) ([0ea033b](https://github.com/calvinnwq/artshelf/commit/0ea033b73e96910de87a832e5ada835bc603ff12))
|
|
126
|
+
|
|
127
|
+
## [0.11.0](https://github.com/calvinnwq/artshelf/compare/artshelf-v0.10.2...artshelf-v0.11.0) (2026-06-14)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
### Features
|
|
131
|
+
|
|
132
|
+
* **ledger:** add cross-process advisory file lock and unique temp paths for atomic writes (NGX-428) ([0f553e4](https://github.com/calvinnwq/artshelf/commit/0f553e485737cf96390d451f4ae92f52e1abbf2a))
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
### Bug Fixes
|
|
136
|
+
|
|
137
|
+
* **cleanup:** reject unsafe plan-ids and mismatched plans before filesystem mutation (NGX-426) ([79debb7](https://github.com/calvinnwq/artshelf/commit/79debb7c3610984a969adea7f93b27ca08150647))
|
|
138
|
+
* **ledger:** make ledger writes atomic and concurrency-safe and reject unsafe cleanup plans ([ac98c4e](https://github.com/calvinnwq/artshelf/commit/ac98c4eaf917b695e166f2ca7c40b6759d6e5f53))
|
|
82
139
|
|
|
83
140
|
## [0.10.2](https://github.com/calvinnwq/artshelf/compare/artshelf-v0.10.1...artshelf-v0.10.2) (2026-06-13)
|
|
84
141
|
|
package/README.md
CHANGED
|
@@ -113,6 +113,9 @@ destructive deletion.
|
|
|
113
113
|
- **No fresh-plan-then-execute shortcut** — review the plan, then run that plan.
|
|
114
114
|
- **Trash before delete** — `cleanup=delete` stays refused; physical deletion
|
|
115
115
|
needs its own reviewed trash purge. No silent deletion, ever.
|
|
116
|
+
- **Durable, concurrency-safe writes** — ledger and registry mutations take a
|
|
117
|
+
cross-process lock and commit atomically, so overlapping commands never lose
|
|
118
|
+
records or leave a half-written ledger.
|
|
116
119
|
- **`--json` on every command**, so agents can act on structured output.
|
|
117
120
|
- **`--agent` on `review`/`status`/`doctor`**, a compact, token-efficient
|
|
118
121
|
decision packet for agents, while the default render stays human-scannable.
|
|
@@ -138,6 +141,8 @@ artshelf doctor
|
|
|
138
141
|
artshelf update [--json]
|
|
139
142
|
artshelf cleanup --dry-run [--all]
|
|
140
143
|
artshelf cleanup --execute --plan-id <id> [--ledger <path>] [--json]
|
|
144
|
+
artshelf reconcile --dry-run [--all] [--ledger <path>] [--json]
|
|
145
|
+
artshelf reconcile --execute --plan-id <id> --ledger <path> [--json]
|
|
141
146
|
artshelf trash list [--all] [--ledger <path>] [--json]
|
|
142
147
|
artshelf trash purge --older-than <ttl> --dry-run [--ledger <path>] [--json]
|
|
143
148
|
artshelf trash purge --execute --plan-id <id> [--ledger <path>] [--json]
|
package/SPEC.md
CHANGED
|
@@ -423,8 +423,15 @@ artshelf cleanup --execute --plan-id <id> [--ledger <path>] --json
|
|
|
423
423
|
|
|
424
424
|
Rules:
|
|
425
425
|
|
|
426
|
-
- Requires `--plan-id
|
|
426
|
+
- Requires `--plan-id`, and refuses an unsafe plan id (anything outside
|
|
427
|
+
`[A-Za-z0-9_-]`, such as a value containing path separators or `..`) before
|
|
428
|
+
touching the filesystem.
|
|
427
429
|
- Refuses to generate a fresh live cleanup set during execute.
|
|
430
|
+
- Binds the loaded plan to the request before any mutation: the plan file's
|
|
431
|
+
`planId` must match the requested id, its `ledgerPath` must match the executing
|
|
432
|
+
ledger, and its entries must be well-formed. A mismatched or malformed plan is
|
|
433
|
+
refused without moving files or writing a receipt, mirroring the live-record
|
|
434
|
+
re-checks `trash purge --execute` performs.
|
|
428
435
|
- Writes a cleanup receipt and appends or refreshes an Artshelf-owned ledger record
|
|
429
436
|
for that receipt with `owner=artshelf`, `kind=run-artifact`, `ttl=30d`,
|
|
430
437
|
`cleanup=review`, and labels including `artshelf`, `cleanup-receipt`, and the
|
|
@@ -519,6 +526,76 @@ Rules:
|
|
|
519
526
|
- Keeps the record visible through `list` and `list --status resolved`.
|
|
520
527
|
- Refuses records that are already `resolved`; the original reason is preserved.
|
|
521
528
|
|
|
529
|
+
### `artshelf reconcile`
|
|
530
|
+
|
|
531
|
+
Approval-gated ledger/registry housekeeping that turns recorded-path drift into a
|
|
532
|
+
reviewed plan and then applies exactly one reviewed plan id. Reconcile is **not**
|
|
533
|
+
cleanup: it never creates, moves, or deletes files. It only rewrites drifted ledger
|
|
534
|
+
paths and resolves rows that can no longer be acted on, mirroring the cleanup
|
|
535
|
+
dry-run/execute boundary.
|
|
536
|
+
|
|
537
|
+
```bash
|
|
538
|
+
artshelf reconcile --dry-run [--ledger <path>] [--json]
|
|
539
|
+
artshelf reconcile --dry-run --all [--registry <path>] [--json]
|
|
540
|
+
artshelf reconcile --execute --plan-id <id> --ledger <path> [--json]
|
|
541
|
+
```
|
|
542
|
+
|
|
543
|
+
Dry-run classifies each drifted record into one finding category:
|
|
544
|
+
|
|
545
|
+
- `remap`: the recorded path is gone, but provenance reconstructs the artifact under
|
|
546
|
+
the current ledger/repo root (for example after a `shelf` -> `artshelf` or
|
|
547
|
+
`.shelf` -> `.artshelf` rename) and the basename plus optional file fingerprint
|
|
548
|
+
still match. The path can be safely rewritten to the reconstructed location.
|
|
549
|
+
- `resolve-missing`: an `active` or `review-required` record's path is gone and no
|
|
550
|
+
safe remap target was found (external path, legacy row, or nothing matches). The
|
|
551
|
+
row can be resolved after review.
|
|
552
|
+
- `resolve-stale-trash`: an already-`trashed` record's trash target is gone. The
|
|
553
|
+
ledger row is resolved ledger-only; the filesystem is never touched.
|
|
554
|
+
- `blocked`: a candidate exists at the reconstructed location but its name or
|
|
555
|
+
fingerprint does not match, or evidence is otherwise ambiguous or unsafe. Blocked
|
|
556
|
+
findings are surfaced for review and never auto-applied.
|
|
557
|
+
|
|
558
|
+
`registry-remap` is reserved in the finding taxonomy for a future registry pass that
|
|
559
|
+
updates a registered ledger whose path moved; the current dry-run classifies drift
|
|
560
|
+
within a single ledger's records and does not yet emit `registry-remap`.
|
|
561
|
+
|
|
562
|
+
Dry-run rules:
|
|
563
|
+
|
|
564
|
+
- Read-only except for reviewed plan artifact creation/reuse. It classifies drift
|
|
565
|
+
and, when actionable entries exist, persists the plan to
|
|
566
|
+
`<ledger-dir>/reconcile-plans/<id>.json` and registers an Artshelf-owned plan
|
|
567
|
+
record (`owner=artshelf`, `kind=run-artifact`, `ttl=14d`, `cleanup=trash`, labels
|
|
568
|
+
including `artshelf`, `reconcile-plan`, and the plan id).
|
|
569
|
+
- A no-op dry-run (only blocked or no findings) reports `planId=not-created`,
|
|
570
|
+
`planPath=null`, and writes no plan file. A later dry-run whose actionable entries
|
|
571
|
+
match an existing plan reuses that plan id and refreshes its plan artifact.
|
|
572
|
+
- `--all` is dry-run only and previews every registered ledger after the registry
|
|
573
|
+
validates. There is no global execute.
|
|
574
|
+
|
|
575
|
+
Execute rules:
|
|
576
|
+
|
|
577
|
+
- Requires `--plan-id` and one explicit `--ledger`. It binds to one reviewed plan id
|
|
578
|
+
and refuses a missing, unknown, or id/ledger-mismatched plan before any mutation.
|
|
579
|
+
There is no `reconcile --execute --all` and no fresh-plan-then-execute.
|
|
580
|
+
- Before applying each entry it re-classifies the live ledger and refuses entries
|
|
581
|
+
whose live state has drifted since review (record gone, status changed, remap
|
|
582
|
+
target vanished, or path reappeared), skipping them instead of mutating stale rows.
|
|
583
|
+
- A `remap` rewrites the record `path` and recomputes its provenance for the new
|
|
584
|
+
location while keeping the row's status; every resolve category archives the row
|
|
585
|
+
ledger-only as `resolved`.
|
|
586
|
+
- Preserves audit provenance on every touched row (`previousPath`, the rewritten
|
|
587
|
+
`path` for a remap, `reconcilePlanId`, `reconcileReceiptPath`, `reconciledAt`, and
|
|
588
|
+
`reconcileReason`), and writes a reconcile receipt to
|
|
589
|
+
`<ledger-dir>/reconcile-receipts/<id>.json` registered as an Artshelf-owned
|
|
590
|
+
artifact (`ttl=30d`, `cleanup=review`, labels including `artshelf`,
|
|
591
|
+
`reconcile-receipt`, and the plan id).
|
|
592
|
+
- Never creates or deletes filesystem artifacts. Reconcile is ledger/registry
|
|
593
|
+
bookkeeping only, and `doctor`, `status`, `review`, and `validate` never perform
|
|
594
|
+
silent reconcile edits.
|
|
595
|
+
|
|
596
|
+
JSON output is deterministic (findings preserve ledger order) so agents can render a
|
|
597
|
+
decision packet and approve a specific plan id.
|
|
598
|
+
|
|
522
599
|
## Ledger Storage
|
|
523
600
|
|
|
524
601
|
V1 supports two scopes:
|
|
@@ -532,6 +609,16 @@ Default behavior:
|
|
|
532
609
|
- Otherwise write user-global.
|
|
533
610
|
- Allow `--ledger <path>` for explicit tests and unusual workflows.
|
|
534
611
|
|
|
612
|
+
Write durability:
|
|
613
|
+
|
|
614
|
+
- Every mutation of a ledger or the registry runs under a cross-process advisory
|
|
615
|
+
lock keyed on the target file, so overlapping `artshelf` processes serialize
|
|
616
|
+
their writes instead of racing. The lock is re-entrant within a process and
|
|
617
|
+
reclaims a stale lock left by a crashed holder.
|
|
618
|
+
- Ledger writes — both single-record appends and full rewrites — land through a
|
|
619
|
+
unique temp file and an atomic rename, so an interrupted write cannot truncate
|
|
620
|
+
the ledger or lose already-recorded entries.
|
|
621
|
+
|
|
535
622
|
V1 also supports a user-level registry of known ledgers:
|
|
536
623
|
|
|
537
624
|
- registry: `~/.artshelf/ledgers.json`
|
|
@@ -642,6 +729,69 @@ the purge provenance:
|
|
|
642
729
|
}
|
|
643
730
|
```
|
|
644
731
|
|
|
732
|
+
Records touched by `artshelf reconcile --execute` carry the reconcile audit trail so a
|
|
733
|
+
remap or resolve stays traceable to the reviewed plan that produced it:
|
|
734
|
+
|
|
735
|
+
```json
|
|
736
|
+
{
|
|
737
|
+
"previousPath": "/old-absolute/path/build/out.txt",
|
|
738
|
+
"reconcilePlanId": "reconcile_20260601_062000_ab12",
|
|
739
|
+
"reconcileReceiptPath": "/absolute/path/.artshelf/reconcile-receipts/reconcile_20260601_062000_ab12.json",
|
|
740
|
+
"reconciledAt": "2026-06-01T06:20:00Z",
|
|
741
|
+
"reconcileReason": "recorded path is missing; reconstructed at the current root"
|
|
742
|
+
}
|
|
743
|
+
```
|
|
744
|
+
|
|
745
|
+
`previousPath` preserves the path the row held before the action; for a `remap` the new
|
|
746
|
+
location is the rewritten `path`, while resolve categories leave `path` and set
|
|
747
|
+
`status=resolved`. These fields are additive and absent on records reconcile never
|
|
748
|
+
touched.
|
|
749
|
+
|
|
750
|
+
### Path provenance
|
|
751
|
+
|
|
752
|
+
New records carry a `provenance` block alongside the absolute `path`. The absolute
|
|
753
|
+
path is still the audit record of where the artifact lived; provenance adds the data
|
|
754
|
+
a future reconcile needs to reason about an artifact that moved because its root was
|
|
755
|
+
renamed (for example `shelf` -> `artshelf` or `.shelf` -> `.artshelf`). Capturing it
|
|
756
|
+
at write time is what lets reconcile remap paths later **without** Artshelf running as
|
|
757
|
+
a daemon, watcher, or shell hook.
|
|
758
|
+
|
|
759
|
+
```json
|
|
760
|
+
{
|
|
761
|
+
"provenance": {
|
|
762
|
+
"root": "repo",
|
|
763
|
+
"rootPath": "/absolute/path/to/repo",
|
|
764
|
+
"relativePath": "build/out.txt",
|
|
765
|
+
"basename": "out.txt",
|
|
766
|
+
"pathKind": "file",
|
|
767
|
+
"fingerprint": { "byteSize": 1024 }
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
```
|
|
771
|
+
|
|
772
|
+
- `root` is `repo`, `ledger`, or `external`. Ledger-owned paths (`trash/`, `plans/`,
|
|
773
|
+
`receipts/`) classify as `ledger`; other paths inside the repo classify as `repo`;
|
|
774
|
+
anything else is `external`.
|
|
775
|
+
- `rootPath` and `relativePath` are the matched root and the POSIX path beneath it.
|
|
776
|
+
The relative path is what survives a root rename, so a reconcile can rebuild the
|
|
777
|
+
current absolute path from the current root. `external` paths cannot be rebuilt, so
|
|
778
|
+
both fields are `null`.
|
|
779
|
+
- `basename`, `pathKind`, and the optional file `fingerprint` (byte size only) are
|
|
780
|
+
cheap matching hints for disambiguating rename candidates.
|
|
781
|
+
|
|
782
|
+
Provenance is additive and backward compatible. Records written before provenance
|
|
783
|
+
existed simply omit the field; they are treated as **legacy records with missing
|
|
784
|
+
provenance, not malformed data**, and continue to validate, read, list, find, and get
|
|
785
|
+
normally. `artshelf validate` only inspects provenance when the field is present: a
|
|
786
|
+
present-but-structurally-invalid block (bad `root`, missing reconstruct data on a
|
|
787
|
+
`repo`/`ledger` root, reconstruct data on an `external` root, non-numeric fingerprint)
|
|
788
|
+
is reported as an error, while an absent block is not.
|
|
789
|
+
|
|
790
|
+
Provenance only records evidence. It never moves, deletes, or rewrites artifacts, and
|
|
791
|
+
capturing it does not change any path. Acting on provenance to remap a ledger remains
|
|
792
|
+
an explicit, approval-gated reconcile step — never an automatic side effect of `put`,
|
|
793
|
+
`doctor`, `status`, `review`, or `validate`.
|
|
794
|
+
|
|
645
795
|
## Cleanup Safety Model
|
|
646
796
|
|
|
647
797
|
Cleanup execution is intentionally boring and approval-only. Five boundaries
|
|
@@ -784,6 +934,8 @@ human review.
|
|
|
784
934
|
- CLI can find existing records by path/owner/label/status and get records by id.
|
|
785
935
|
- CLI can mark records manually resolved with a required reason.
|
|
786
936
|
- CLI validates ledger shape.
|
|
937
|
+
- Concurrent ledger and registry writes are serialized with a cross-process lock
|
|
938
|
+
and committed atomically, so overlapping commands do not lose records.
|
|
787
939
|
- CLI reports machine and registry health through `artshelf doctor`, exiting
|
|
788
940
|
non-zero when the registry or a registered ledger is broken.
|
|
789
941
|
- CLI reports a read-only daily dashboard through `artshelf status`, with
|
|
@@ -795,11 +947,23 @@ human review.
|
|
|
795
947
|
entries; no-op dry-runs do not write plan files.
|
|
796
948
|
- Cleanup dry-run and execute register the plan/receipt artifacts that Artshelf
|
|
797
949
|
creates.
|
|
798
|
-
- Cleanup execute refuses to run without a plan id
|
|
950
|
+
- Cleanup execute refuses to run without a plan id, and refuses an unsafe,
|
|
951
|
+
mismatched, or malformed plan before moving files or writing a receipt.
|
|
799
952
|
- Cleanup execute writes a receipt.
|
|
800
953
|
- CLI can list trashed records (single ledger or `--all`) and purge them through
|
|
801
954
|
an approval-first, ledger-scoped dry-run/execute boundary that writes a purge
|
|
802
955
|
receipt; purge refuses `--all` and never deletes without a reviewed plan id.
|
|
956
|
+
- New records capture path provenance (root class, root-relative path, basename,
|
|
957
|
+
path kind, and an optional byte-size fingerprint); provenance is additive and
|
|
958
|
+
backward compatible, so legacy records without it still validate and read, and
|
|
959
|
+
`validate` reports a malformed provenance block only when the field is present.
|
|
960
|
+
- CLI can reconcile drifted recorded paths through `artshelf reconcile` without
|
|
961
|
+
ever creating, moving, or deleting files: `--dry-run` classifies drift into a
|
|
962
|
+
reviewed plan (`remap`, `resolve-missing`, `resolve-stale-trash`, `blocked`) and
|
|
963
|
+
`--all` previews every registered ledger as dry-run only, while `--execute`
|
|
964
|
+
applies one reviewed plan id against one explicit ledger, refuses `--all`,
|
|
965
|
+
mismatched plans, and entries whose live state drifted since review, and writes
|
|
966
|
+
the reconcile audit trail and receipt.
|
|
803
967
|
- Package includes the deterministic `ArtshelfReviewReport` schema, canonical
|
|
804
968
|
example, and portable renderer script for agent-rendered review reports.
|
|
805
969
|
- All core commands support `--json`.
|
|
@@ -807,7 +971,9 @@ human review.
|
|
|
807
971
|
JSON decision packet for agents that takes precedence over `--json`.
|
|
808
972
|
- Tests cover record/list/find/get/status-filter/due/validate/resolve/registry,
|
|
809
973
|
`artshelf doctor`, the `artshelf status` dashboard, `--all` review, stale-registry,
|
|
810
|
-
dry-run, global-dry-run, execute-plan,
|
|
974
|
+
dry-run, global-dry-run, execute-plan, cleanup plan-id validation, concurrent
|
|
975
|
+
ledger writes, trash list/purge, path provenance validation, and reconcile
|
|
976
|
+
dry-run/execute behavior.
|
|
811
977
|
|
|
812
978
|
## Deferred
|
|
813
979
|
|
|
@@ -8,6 +8,7 @@ import { handleGet } from "./get.js";
|
|
|
8
8
|
import { handleLedgers } from "./ledgers.js";
|
|
9
9
|
import { handleList } from "./list.js";
|
|
10
10
|
import { handlePut } from "./put.js";
|
|
11
|
+
import { handleReconcile } from "./reconcile.js";
|
|
11
12
|
import { handleResolve } from "./resolve.js";
|
|
12
13
|
import { handleReview } from "./review.js";
|
|
13
14
|
import { handleStatus } from "./status.js";
|
|
@@ -43,6 +44,9 @@ export async function runCommand(parsed) {
|
|
|
43
44
|
case "cleanup":
|
|
44
45
|
status = handleCleanup(parsed, normalizeLedgerPath(stringFlag(parsed, "ledger")), boolFlag(parsed, "json"));
|
|
45
46
|
break;
|
|
47
|
+
case "reconcile":
|
|
48
|
+
status = handleReconcile(parsed, normalizeLedgerPath(stringFlag(parsed, "ledger")), boolFlag(parsed, "json"));
|
|
49
|
+
break;
|
|
46
50
|
case "trash":
|
|
47
51
|
status = handleTrash(parsed, normalizeLedgerPath(stringFlag(parsed, "ledger")), boolFlag(parsed, "json"));
|
|
48
52
|
break;
|
package/dist/src/commands/put.js
CHANGED
|
@@ -16,7 +16,7 @@ export function handlePut(parsed, ledgerPath, json) {
|
|
|
16
16
|
cleanup: stringFlag(parsed, "cleanup"),
|
|
17
17
|
owner: stringFlag(parsed, "owner"),
|
|
18
18
|
labels: arrayFlag(parsed, "label")
|
|
19
|
-
});
|
|
19
|
+
}, ledgerPath);
|
|
20
20
|
const registryPath = normalizeRegistryPath(stringFlag(parsed, "registry"));
|
|
21
21
|
appendPreparedRecord(ledgerPath, record);
|
|
22
22
|
let ledger;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { createReconcilePlan, executeReconcilePlan } from "../reconcile.js";
|
|
2
|
+
import { normalizeRegistryPath } from "../registry.js";
|
|
3
|
+
import { printJson } from "../renderers/json.js";
|
|
4
|
+
import { boolFlag, requiredStringFlag, stringFlag } from "../shared/flags.js";
|
|
5
|
+
import { printReconcilePlan, printReconcilePlans, printRegisteredLedgerValidation, validateRegisteredLedgersOrThrow } from "./shared.js";
|
|
6
|
+
// `artshelf reconcile` (NGX-437): approval-gated ledger/registry housekeeping that
|
|
7
|
+
// converts path drift into a reviewed plan, then applies one reviewed plan id. This
|
|
8
|
+
// command layer enforces the safety envelope around the reconcile domain functions:
|
|
9
|
+
// dry-run and execute are mutually exclusive, `--all` is dry-run only (no global
|
|
10
|
+
// execute), and execute always binds to one explicit `--ledger` plus `--plan-id`.
|
|
11
|
+
// Reconcile never touches the filesystem; it is ledger bookkeeping, not cleanup.
|
|
12
|
+
export function handleReconcile(parsed, ledgerPath, json) {
|
|
13
|
+
const dryRun = boolFlag(parsed, "dry-run");
|
|
14
|
+
const execute = boolFlag(parsed, "execute");
|
|
15
|
+
if (dryRun && execute)
|
|
16
|
+
throw new Error("reconcile accepts either --dry-run or --execute, not both");
|
|
17
|
+
if (boolFlag(parsed, "all") && execute) {
|
|
18
|
+
throw new Error("reconcile --all is dry-run only; execute requires an explicit --ledger and reviewed --plan-id");
|
|
19
|
+
}
|
|
20
|
+
if (dryRun) {
|
|
21
|
+
if (boolFlag(parsed, "all")) {
|
|
22
|
+
const registryPath = normalizeRegistryPath(stringFlag(parsed, "registry"));
|
|
23
|
+
const { ok, results } = validateRegisteredLedgersOrThrow(registryPath);
|
|
24
|
+
if (!ok)
|
|
25
|
+
return printRegisteredLedgerValidation(registryPath, results, json);
|
|
26
|
+
const plans = results.map(({ ledger }) => ({ ledger, plan: createReconcilePlan(ledger.path) }));
|
|
27
|
+
if (json)
|
|
28
|
+
return printJson({ ok: true, registryPath, plans });
|
|
29
|
+
printReconcilePlans(plans);
|
|
30
|
+
process.stdout.write(`registry: ${registryPath}\n`);
|
|
31
|
+
return 0;
|
|
32
|
+
}
|
|
33
|
+
const plan = createReconcilePlan(ledgerPath);
|
|
34
|
+
if (json)
|
|
35
|
+
return printJson({ ok: true, plan });
|
|
36
|
+
printReconcilePlan(plan, ledgerPath);
|
|
37
|
+
return 0;
|
|
38
|
+
}
|
|
39
|
+
if (execute) {
|
|
40
|
+
const planId = requiredStringFlag(parsed, "plan-id");
|
|
41
|
+
const receipt = executeReconcilePlan(ledgerPath, planId);
|
|
42
|
+
if (json)
|
|
43
|
+
return printJson({ ok: true, receipt });
|
|
44
|
+
process.stdout.write(`receipt ${receipt.planId}: ${receipt.results.length} results\nreceipt: ${receipt.receiptPath}\nledger: ${ledgerPath}\n`);
|
|
45
|
+
return 0;
|
|
46
|
+
}
|
|
47
|
+
throw new Error("reconcile requires --dry-run or --execute");
|
|
48
|
+
}
|
|
@@ -103,6 +103,23 @@ export function printPlan(plan, ledgerPath) {
|
|
|
103
103
|
process.stdout.write(`plan ${plan.planId}: ${plan.entries.length} entries, ${plan.skipped.length} skipped\n`);
|
|
104
104
|
process.stdout.write(`plan: ${plan.planPath ?? "not created"}\nledger: ${ledgerPath}\n`);
|
|
105
105
|
}
|
|
106
|
+
export function printReconcilePlan(plan, ledgerPath) {
|
|
107
|
+
process.stdout.write(`plan ${plan.planId}: ${plan.entries.length} entries, ${plan.blocked.length} blocked\n`);
|
|
108
|
+
for (const entry of plan.entries) {
|
|
109
|
+
const target = entry.proposedPath ? `${entry.currentPath} -> ${entry.proposedPath}` : entry.currentPath;
|
|
110
|
+
process.stdout.write(`${entry.category} ${entry.id} ${entry.field} ${target} :: ${entry.reason}\n`);
|
|
111
|
+
}
|
|
112
|
+
for (const blocked of plan.blocked) {
|
|
113
|
+
process.stdout.write(`blocked ${blocked.id} ${blocked.field} ${blocked.currentPath} :: ${blocked.reason}\n`);
|
|
114
|
+
}
|
|
115
|
+
process.stdout.write(`plan: ${plan.planPath ?? "not created"}\nledger: ${ledgerPath}\n`);
|
|
116
|
+
}
|
|
117
|
+
export function printReconcilePlans(results) {
|
|
118
|
+
for (const result of results) {
|
|
119
|
+
process.stdout.write(`plan ${result.plan.planId} [${result.ledger.name}]: ${result.plan.entries.length} entries, ${result.plan.blocked.length} blocked\n`);
|
|
120
|
+
process.stdout.write(`plan: ${result.plan.planPath ?? "not created"}\nledger: ${result.ledger.path}\n`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
106
123
|
export function printTrashListEntries(results) {
|
|
107
124
|
const total = results.reduce((count, result) => count + result.entries.length, 0);
|
|
108
125
|
if (total === 0) {
|