artshelf 0.11.0 → 0.13.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 +52 -0
- package/README.md +2 -0
- package/SPEC.md +163 -10
- package/dist/src/commands/index.js +4 -0
- package/dist/src/commands/put.js +2 -2
- package/dist/src/commands/reconcile.js +48 -0
- package/dist/src/commands/review.js +1 -1
- package/dist/src/commands/shared.js +31 -3
- package/dist/src/ledger.js +22 -7
- package/dist/src/provenance.js +142 -0
- package/dist/src/reconcile.js +335 -0
- package/dist/src/renderers/doctor.js +3 -3
- package/dist/src/renderers/review.js +73 -9
- package/dist/src/renderers/status.js +4 -3
- package/dist/src/shared/help-text.js +28 -1
- package/docs/agent-monitor.html +7 -0
- package/docs/agent-review.html +9 -3
- package/docs/examples/artshelf-review-report.json +74 -19
- package/docs/reference.html +31 -6
- package/docs/schemas/artshelf-review-report.schema.json +318 -73
- package/examples/artshelf-review-report.json +74 -19
- package/package.json +1 -1
- package/schemas/artshelf-review-report.schema.json +318 -73
- package/skills/artshelf/SKILL.md +10 -10
- package/skills/artshelf/examples/artshelf-review-report.json +74 -19
- package/skills/artshelf/schemas/artshelf-review-report.schema.json +318 -73
- package/skills/artshelf/scripts/render-review-report.mjs +4 -3
package/CHANGELOG.md
CHANGED
|
@@ -91,6 +91,58 @@
|
|
|
91
91
|
entries must be well-formed, so mismatched or malformed plans are refused before
|
|
92
92
|
moving files or writing a receipt — the plan-id-bound posture trash purge
|
|
93
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
|
+
- Integrated reconcile findings into `review --agent`, `status --agent`, and
|
|
114
|
+
`doctor --agent` triage: missing-path warnings now route to reconcile dry-run
|
|
115
|
+
guidance before approval, reconciled plans escalate to ready-for-approval, and
|
|
116
|
+
the `ArtshelfReviewReport` schema adds the `reconcile` action type (NGX-438).
|
|
117
|
+
- Moved `artshelf put` registry-warning output from stdout to stderr in human
|
|
118
|
+
mode; `--json` output is unchanged (NGX-429).
|
|
119
|
+
|
|
120
|
+
## [0.13.0](https://github.com/calvinnwq/artshelf/compare/artshelf-v0.12.0...artshelf-v0.13.0) (2026-06-15)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
### Features
|
|
124
|
+
|
|
125
|
+
* **review:** integrate reconcile findings into agent review packets ([878785e](https://github.com/calvinnwq/artshelf/commit/878785e72c4e65bd8e09572525b05cc020d2f1e1))
|
|
126
|
+
* **review:** integrate reconcile findings into agent triage; move put registry-warning to stderr (NGX-438, NGX-429) ([2573470](https://github.com/calvinnwq/artshelf/commit/25734701b439f617a33609ac98c3fae895199640))
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
### Bug Fixes
|
|
130
|
+
|
|
131
|
+
* **review:** include reconcile counts in all-ledger triage ([2eeb2fe](https://github.com/calvinnwq/artshelf/commit/2eeb2fea6eae58bfd652be959f2bb6e28d7cb90f))
|
|
132
|
+
* **review:** keep reconcile approval schema and blocked triage consistent ([0c8925a](https://github.com/calvinnwq/artshelf/commit/0c8925a851023622796f2b8d847fcc89cab3c5f0))
|
|
133
|
+
|
|
134
|
+
## [0.12.0](https://github.com/calvinnwq/artshelf/compare/artshelf-v0.11.0...artshelf-v0.12.0) (2026-06-15)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
### Features
|
|
138
|
+
|
|
139
|
+
* **ledger:** add path-provenance foundation for NGX-436 ([f0bf797](https://github.com/calvinnwq/artshelf/commit/f0bf797223e5032e326842cc1dd8fcb47130ed3e))
|
|
140
|
+
* **ledger:** add provenance validation distinguishing legacy from malformed rows (NGX-436) ([ce5128a](https://github.com/calvinnwq/artshelf/commit/ce5128a85bcc6353f73c3b47bd9a433918236ee0))
|
|
141
|
+
* **reconcile:** add path-provenance foundation and approval-gated reconcile command ([ad4bcec](https://github.com/calvinnwq/artshelf/commit/ad4bcec7839e004a0f7ca3cc9a8ecebb0caaac0f))
|
|
142
|
+
* **reconcile:** add read-only classification engine for NGX-437 ([3245738](https://github.com/calvinnwq/artshelf/commit/3245738c8d7b3e3dfd95c49fae414596b35c22e1))
|
|
143
|
+
* **reconcile:** add reconcile dry-run plan layer for NGX-437 ([ddc8881](https://github.com/calvinnwq/artshelf/commit/ddc8881713d26f1e43575b3097e49558c22cc2e7))
|
|
144
|
+
* **reconcile:** add reconcile execute layer with audit trail and stale-state refusals (NGX-437) ([50a12d4](https://github.com/calvinnwq/artshelf/commit/50a12d49cdf0552b675bfaa75a0220c886bd64e5))
|
|
145
|
+
* **reconcile:** wire reconcile CLI command with integration tests (NGX-437) ([0ea033b](https://github.com/calvinnwq/artshelf/commit/0ea033b73e96910de87a832e5ada835bc603ff12))
|
|
94
146
|
|
|
95
147
|
## [0.11.0](https://github.com/calvinnwq/artshelf/compare/artshelf-v0.10.2...artshelf-v0.11.0) (2026-06-14)
|
|
96
148
|
|
package/README.md
CHANGED
|
@@ -141,6 +141,8 @@ artshelf doctor
|
|
|
141
141
|
artshelf update [--json]
|
|
142
142
|
artshelf cleanup --dry-run [--all]
|
|
143
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]
|
|
144
146
|
artshelf trash list [--all] [--ledger <path>] [--json]
|
|
145
147
|
artshelf trash purge --older-than <ttl> --dry-run [--ledger <path>] [--json]
|
|
146
148
|
artshelf trash purge --execute --plan-id <id> [--ledger <path>] [--json]
|
package/SPEC.md
CHANGED
|
@@ -99,8 +99,9 @@ Defaults:
|
|
|
99
99
|
`put` should refuse to record a path that does not exist unless a future flag
|
|
100
100
|
explicitly supports planned artifacts. After appending the record, `put`
|
|
101
101
|
registers the ledger in the ledger registry. Registry registration is
|
|
102
|
-
best-effort: if it fails, the record remains appended and
|
|
103
|
-
|
|
102
|
+
best-effort: if it fails, the record remains appended and a registry warning is
|
|
103
|
+
printed to stderr in human mode, or surfaced as a `registryError` field in
|
|
104
|
+
`--json` output, so stdout stays machine-clean.
|
|
104
105
|
|
|
105
106
|
### `artshelf ledgers`
|
|
106
107
|
|
|
@@ -253,10 +254,13 @@ included with a `not-created` plan instead of writing a plan file.
|
|
|
253
254
|
|
|
254
255
|
In `--all` mode, review emits an aggregate triage summary on top of the
|
|
255
256
|
per-ledger detail. JSON includes a `summary` block with affected-ledger, due,
|
|
256
|
-
manual-review, missing-path, executable,
|
|
257
|
-
plan ids; JSON also includes the next safe action.
|
|
258
|
-
|
|
259
|
-
|
|
257
|
+
manual-review, missing-path, executable, skipped, and reconcile entry/blocked
|
|
258
|
+
counts plus the preview plan ids; JSON also includes the next safe action. The
|
|
259
|
+
per-ledger human detail appends a `reconcile` count when a ledger has reconcile
|
|
260
|
+
drift. Human output adds a one-line triage count with the same reconcile counts
|
|
261
|
+
and states the same next safe action (repair broken ledgers, dry-run cleanup,
|
|
262
|
+
dry-run reconcile for missing-path or reconcile drift, or nothing to do). Review
|
|
263
|
+
never writes a plan, so
|
|
260
264
|
the next action always points at an explicit follow-up command.
|
|
261
265
|
|
|
262
266
|
`review`, `status`, and `doctor` share three render modes. The default human
|
|
@@ -265,9 +269,13 @@ stays the full, backward-compatible public audit report; and `--agent` emits a c
|
|
|
265
269
|
deterministic single-line JSON decision packet for agents, taking precedence over
|
|
266
270
|
`--json` when both are passed. For `review`, the packet sorts records into
|
|
267
271
|
ready-for-approval, needs-review-first, and blocked groups. Because review is
|
|
268
|
-
read-only and never mints a cleanup plan, the
|
|
269
|
-
|
|
270
|
-
|
|
272
|
+
read-only and never mints a cleanup plan, the exact approval targets it emits are
|
|
273
|
+
`resolve missing` and `reconcile`; the `reconcile` target appears only when a
|
|
274
|
+
prior reviewed reconcile plan still matches the live drift. Cleanup-eligible
|
|
275
|
+
records and reconcile drift without a reviewed plan stay needs-review-first and
|
|
276
|
+
point at `cleanup --dry-run` or `reconcile --dry-run`, which mint the reviewed
|
|
277
|
+
plan id to approve. Blocked or ambiguous reconcile findings surface in the
|
|
278
|
+
blocked group with no approval target.
|
|
271
279
|
|
|
272
280
|
### `artshelf doctor`
|
|
273
281
|
|
|
@@ -526,6 +534,76 @@ Rules:
|
|
|
526
534
|
- Keeps the record visible through `list` and `list --status resolved`.
|
|
527
535
|
- Refuses records that are already `resolved`; the original reason is preserved.
|
|
528
536
|
|
|
537
|
+
### `artshelf reconcile`
|
|
538
|
+
|
|
539
|
+
Approval-gated ledger/registry housekeeping that turns recorded-path drift into a
|
|
540
|
+
reviewed plan and then applies exactly one reviewed plan id. Reconcile is **not**
|
|
541
|
+
cleanup: it never creates, moves, or deletes files. It only rewrites drifted ledger
|
|
542
|
+
paths and resolves rows that can no longer be acted on, mirroring the cleanup
|
|
543
|
+
dry-run/execute boundary.
|
|
544
|
+
|
|
545
|
+
```bash
|
|
546
|
+
artshelf reconcile --dry-run [--ledger <path>] [--json]
|
|
547
|
+
artshelf reconcile --dry-run --all [--registry <path>] [--json]
|
|
548
|
+
artshelf reconcile --execute --plan-id <id> --ledger <path> [--json]
|
|
549
|
+
```
|
|
550
|
+
|
|
551
|
+
Dry-run classifies each drifted record into one finding category:
|
|
552
|
+
|
|
553
|
+
- `remap`: the recorded path is gone, but provenance reconstructs the artifact under
|
|
554
|
+
the current ledger/repo root (for example after a `shelf` -> `artshelf` or
|
|
555
|
+
`.shelf` -> `.artshelf` rename) and the basename plus optional file fingerprint
|
|
556
|
+
still match. The path can be safely rewritten to the reconstructed location.
|
|
557
|
+
- `resolve-missing`: an `active` or `review-required` record's path is gone and no
|
|
558
|
+
safe remap target was found (external path, legacy row, or nothing matches). The
|
|
559
|
+
row can be resolved after review.
|
|
560
|
+
- `resolve-stale-trash`: an already-`trashed` record's trash target is gone. The
|
|
561
|
+
ledger row is resolved ledger-only; the filesystem is never touched.
|
|
562
|
+
- `blocked`: a candidate exists at the reconstructed location but its name or
|
|
563
|
+
fingerprint does not match, or evidence is otherwise ambiguous or unsafe. Blocked
|
|
564
|
+
findings are surfaced for review and never auto-applied.
|
|
565
|
+
|
|
566
|
+
`registry-remap` is reserved in the finding taxonomy for a future registry pass that
|
|
567
|
+
updates a registered ledger whose path moved; the current dry-run classifies drift
|
|
568
|
+
within a single ledger's records and does not yet emit `registry-remap`.
|
|
569
|
+
|
|
570
|
+
Dry-run rules:
|
|
571
|
+
|
|
572
|
+
- Read-only except for reviewed plan artifact creation/reuse. It classifies drift
|
|
573
|
+
and, when actionable entries exist, persists the plan to
|
|
574
|
+
`<ledger-dir>/reconcile-plans/<id>.json` and registers an Artshelf-owned plan
|
|
575
|
+
record (`owner=artshelf`, `kind=run-artifact`, `ttl=14d`, `cleanup=trash`, labels
|
|
576
|
+
including `artshelf`, `reconcile-plan`, and the plan id).
|
|
577
|
+
- A no-op dry-run (only blocked or no findings) reports `planId=not-created`,
|
|
578
|
+
`planPath=null`, and writes no plan file. A later dry-run whose actionable entries
|
|
579
|
+
match an existing plan reuses that plan id and refreshes its plan artifact.
|
|
580
|
+
- `--all` is dry-run only and previews every registered ledger after the registry
|
|
581
|
+
validates. There is no global execute.
|
|
582
|
+
|
|
583
|
+
Execute rules:
|
|
584
|
+
|
|
585
|
+
- Requires `--plan-id` and one explicit `--ledger`. It binds to one reviewed plan id
|
|
586
|
+
and refuses a missing, unknown, or id/ledger-mismatched plan before any mutation.
|
|
587
|
+
There is no `reconcile --execute --all` and no fresh-plan-then-execute.
|
|
588
|
+
- Before applying each entry it re-classifies the live ledger and refuses entries
|
|
589
|
+
whose live state has drifted since review (record gone, status changed, remap
|
|
590
|
+
target vanished, or path reappeared), skipping them instead of mutating stale rows.
|
|
591
|
+
- A `remap` rewrites the record `path` and recomputes its provenance for the new
|
|
592
|
+
location while keeping the row's status; every resolve category archives the row
|
|
593
|
+
ledger-only as `resolved`.
|
|
594
|
+
- Preserves audit provenance on every touched row (`previousPath`, the rewritten
|
|
595
|
+
`path` for a remap, `reconcilePlanId`, `reconcileReceiptPath`, `reconciledAt`, and
|
|
596
|
+
`reconcileReason`), and writes a reconcile receipt to
|
|
597
|
+
`<ledger-dir>/reconcile-receipts/<id>.json` registered as an Artshelf-owned
|
|
598
|
+
artifact (`ttl=30d`, `cleanup=review`, labels including `artshelf`,
|
|
599
|
+
`reconcile-receipt`, and the plan id).
|
|
600
|
+
- Never creates or deletes filesystem artifacts. Reconcile is ledger/registry
|
|
601
|
+
bookkeeping only, and `doctor`, `status`, `review`, and `validate` never perform
|
|
602
|
+
silent reconcile edits.
|
|
603
|
+
|
|
604
|
+
JSON output is deterministic (findings preserve ledger order) so agents can render a
|
|
605
|
+
decision packet and approve a specific plan id.
|
|
606
|
+
|
|
529
607
|
## Ledger Storage
|
|
530
608
|
|
|
531
609
|
V1 supports two scopes:
|
|
@@ -659,6 +737,69 @@ the purge provenance:
|
|
|
659
737
|
}
|
|
660
738
|
```
|
|
661
739
|
|
|
740
|
+
Records touched by `artshelf reconcile --execute` carry the reconcile audit trail so a
|
|
741
|
+
remap or resolve stays traceable to the reviewed plan that produced it:
|
|
742
|
+
|
|
743
|
+
```json
|
|
744
|
+
{
|
|
745
|
+
"previousPath": "/old-absolute/path/build/out.txt",
|
|
746
|
+
"reconcilePlanId": "reconcile_20260601_062000_ab12",
|
|
747
|
+
"reconcileReceiptPath": "/absolute/path/.artshelf/reconcile-receipts/reconcile_20260601_062000_ab12.json",
|
|
748
|
+
"reconciledAt": "2026-06-01T06:20:00Z",
|
|
749
|
+
"reconcileReason": "recorded path is missing; reconstructed at the current root"
|
|
750
|
+
}
|
|
751
|
+
```
|
|
752
|
+
|
|
753
|
+
`previousPath` preserves the path the row held before the action; for a `remap` the new
|
|
754
|
+
location is the rewritten `path`, while resolve categories leave `path` and set
|
|
755
|
+
`status=resolved`. These fields are additive and absent on records reconcile never
|
|
756
|
+
touched.
|
|
757
|
+
|
|
758
|
+
### Path provenance
|
|
759
|
+
|
|
760
|
+
New records carry a `provenance` block alongside the absolute `path`. The absolute
|
|
761
|
+
path is still the audit record of where the artifact lived; provenance adds the data
|
|
762
|
+
a future reconcile needs to reason about an artifact that moved because its root was
|
|
763
|
+
renamed (for example `shelf` -> `artshelf` or `.shelf` -> `.artshelf`). Capturing it
|
|
764
|
+
at write time is what lets reconcile remap paths later **without** Artshelf running as
|
|
765
|
+
a daemon, watcher, or shell hook.
|
|
766
|
+
|
|
767
|
+
```json
|
|
768
|
+
{
|
|
769
|
+
"provenance": {
|
|
770
|
+
"root": "repo",
|
|
771
|
+
"rootPath": "/absolute/path/to/repo",
|
|
772
|
+
"relativePath": "build/out.txt",
|
|
773
|
+
"basename": "out.txt",
|
|
774
|
+
"pathKind": "file",
|
|
775
|
+
"fingerprint": { "byteSize": 1024 }
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
```
|
|
779
|
+
|
|
780
|
+
- `root` is `repo`, `ledger`, or `external`. Ledger-owned paths (`trash/`, `plans/`,
|
|
781
|
+
`receipts/`) classify as `ledger`; other paths inside the repo classify as `repo`;
|
|
782
|
+
anything else is `external`.
|
|
783
|
+
- `rootPath` and `relativePath` are the matched root and the POSIX path beneath it.
|
|
784
|
+
The relative path is what survives a root rename, so a reconcile can rebuild the
|
|
785
|
+
current absolute path from the current root. `external` paths cannot be rebuilt, so
|
|
786
|
+
both fields are `null`.
|
|
787
|
+
- `basename`, `pathKind`, and the optional file `fingerprint` (byte size only) are
|
|
788
|
+
cheap matching hints for disambiguating rename candidates.
|
|
789
|
+
|
|
790
|
+
Provenance is additive and backward compatible. Records written before provenance
|
|
791
|
+
existed simply omit the field; they are treated as **legacy records with missing
|
|
792
|
+
provenance, not malformed data**, and continue to validate, read, list, find, and get
|
|
793
|
+
normally. `artshelf validate` only inspects provenance when the field is present: a
|
|
794
|
+
present-but-structurally-invalid block (bad `root`, missing reconstruct data on a
|
|
795
|
+
`repo`/`ledger` root, reconstruct data on an `external` root, non-numeric fingerprint)
|
|
796
|
+
is reported as an error, while an absent block is not.
|
|
797
|
+
|
|
798
|
+
Provenance only records evidence. It never moves, deletes, or rewrites artifacts, and
|
|
799
|
+
capturing it does not change any path. Acting on provenance to remap a ledger remains
|
|
800
|
+
an explicit, approval-gated reconcile step — never an automatic side effect of `put`,
|
|
801
|
+
`doctor`, `status`, `review`, or `validate`.
|
|
802
|
+
|
|
662
803
|
## Cleanup Safety Model
|
|
663
804
|
|
|
664
805
|
Cleanup execution is intentionally boring and approval-only. Five boundaries
|
|
@@ -820,6 +961,17 @@ human review.
|
|
|
820
961
|
- CLI can list trashed records (single ledger or `--all`) and purge them through
|
|
821
962
|
an approval-first, ledger-scoped dry-run/execute boundary that writes a purge
|
|
822
963
|
receipt; purge refuses `--all` and never deletes without a reviewed plan id.
|
|
964
|
+
- New records capture path provenance (root class, root-relative path, basename,
|
|
965
|
+
path kind, and an optional byte-size fingerprint); provenance is additive and
|
|
966
|
+
backward compatible, so legacy records without it still validate and read, and
|
|
967
|
+
`validate` reports a malformed provenance block only when the field is present.
|
|
968
|
+
- CLI can reconcile drifted recorded paths through `artshelf reconcile` without
|
|
969
|
+
ever creating, moving, or deleting files: `--dry-run` classifies drift into a
|
|
970
|
+
reviewed plan (`remap`, `resolve-missing`, `resolve-stale-trash`, `blocked`) and
|
|
971
|
+
`--all` previews every registered ledger as dry-run only, while `--execute`
|
|
972
|
+
applies one reviewed plan id against one explicit ledger, refuses `--all`,
|
|
973
|
+
mismatched plans, and entries whose live state drifted since review, and writes
|
|
974
|
+
the reconcile audit trail and receipt.
|
|
823
975
|
- Package includes the deterministic `ArtshelfReviewReport` schema, canonical
|
|
824
976
|
example, and portable renderer script for agent-rendered review reports.
|
|
825
977
|
- All core commands support `--json`.
|
|
@@ -828,7 +980,8 @@ human review.
|
|
|
828
980
|
- Tests cover record/list/find/get/status-filter/due/validate/resolve/registry,
|
|
829
981
|
`artshelf doctor`, the `artshelf status` dashboard, `--all` review, stale-registry,
|
|
830
982
|
dry-run, global-dry-run, execute-plan, cleanup plan-id validation, concurrent
|
|
831
|
-
ledger writes,
|
|
983
|
+
ledger writes, trash list/purge, path provenance validation, and reconcile
|
|
984
|
+
dry-run/execute behavior.
|
|
832
985
|
|
|
833
986
|
## Deferred
|
|
834
987
|
|
|
@@ -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;
|
|
@@ -31,6 +31,6 @@ export function handlePut(parsed, ledgerPath, json) {
|
|
|
31
31
|
return printJson({ ok: true, record, ledgerPath, registryPath, ...(ledger ? { ledger } : {}), ...(registryError ? { registryError } : {}) });
|
|
32
32
|
process.stdout.write(`recorded ${record.id}\npath: ${record.path}\nretains until: ${record.retainUntil ?? "manual review"}\nledger: ${ledgerPath}\n`);
|
|
33
33
|
if (registryError)
|
|
34
|
-
process.
|
|
34
|
+
process.stderr.write(`registry warning: ${registryError}\n`);
|
|
35
35
|
return 0;
|
|
36
36
|
}
|
|
@@ -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
|
+
}
|
|
@@ -15,7 +15,7 @@ export function handleReview(parsed, ledgerPath, json) {
|
|
|
15
15
|
printCompactJson(buildReviewAgentPacketAll(results, summary, { path: registryPath, exists: existsSync(registryPath) }));
|
|
16
16
|
return ok ? 0 : 1;
|
|
17
17
|
}
|
|
18
|
-
const nextAction = reviewNextAction(summary, "all");
|
|
18
|
+
const nextAction = reviewNextAction(summary, "all", undefined, registryPath);
|
|
19
19
|
if (json) {
|
|
20
20
|
printJson({ ok, registryPath, summary, nextAction, ledgers: results.map(reviewJsonResult) });
|
|
21
21
|
return ok ? 0 : 1;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
import { dueEntries, previewCleanupPlan, readLedger, validateLedger } from "../ledger.js";
|
|
3
3
|
import { listRegisteredLedgers } from "../registry.js";
|
|
4
|
+
import { matchingReconcilePlan, previewReconcilePlan } from "../reconcile.js";
|
|
4
5
|
import { printJson } from "../renderers/json.js";
|
|
5
6
|
export function registeredLedgersOrThrow(registryPath) {
|
|
6
7
|
const ledgers = listRegisteredLedgers(registryPath);
|
|
@@ -43,15 +44,19 @@ export function reviewLedger(ledger, registered = true) {
|
|
|
43
44
|
ledgerExists,
|
|
44
45
|
validate,
|
|
45
46
|
due: [],
|
|
46
|
-
plan: emptyReviewPlan(ledger.path)
|
|
47
|
+
plan: emptyReviewPlan(ledger.path),
|
|
48
|
+
reconcile: null
|
|
47
49
|
};
|
|
48
50
|
}
|
|
51
|
+
const reconcilePlan = previewReconcilePlan(ledger.path);
|
|
52
|
+
const reviewedReconcilePlan = reconcilePlan.entries.length > 0 || reconcilePlan.blocked.length > 0 ? matchingReconcilePlan(ledger.path, reconcilePlan) : null;
|
|
49
53
|
return {
|
|
50
54
|
ledger,
|
|
51
55
|
ledgerExists,
|
|
52
56
|
validate,
|
|
53
57
|
due: dueEntries(readLedger(ledger.path)),
|
|
54
|
-
plan: previewCleanupPlan(ledger.path)
|
|
58
|
+
plan: previewCleanupPlan(ledger.path),
|
|
59
|
+
reconcile: { plan: reconcilePlan, reviewedPlan: reviewedReconcilePlan }
|
|
55
60
|
};
|
|
56
61
|
}
|
|
57
62
|
export function reviewJsonResult(result) {
|
|
@@ -103,6 +108,23 @@ export function printPlan(plan, ledgerPath) {
|
|
|
103
108
|
process.stdout.write(`plan ${plan.planId}: ${plan.entries.length} entries, ${plan.skipped.length} skipped\n`);
|
|
104
109
|
process.stdout.write(`plan: ${plan.planPath ?? "not created"}\nledger: ${ledgerPath}\n`);
|
|
105
110
|
}
|
|
111
|
+
export function printReconcilePlan(plan, ledgerPath) {
|
|
112
|
+
process.stdout.write(`plan ${plan.planId}: ${plan.entries.length} entries, ${plan.blocked.length} blocked\n`);
|
|
113
|
+
for (const entry of plan.entries) {
|
|
114
|
+
const target = entry.proposedPath ? `${entry.currentPath} -> ${entry.proposedPath}` : entry.currentPath;
|
|
115
|
+
process.stdout.write(`${entry.category} ${entry.id} ${entry.field} ${target} :: ${entry.reason}\n`);
|
|
116
|
+
}
|
|
117
|
+
for (const blocked of plan.blocked) {
|
|
118
|
+
process.stdout.write(`blocked ${blocked.id} ${blocked.field} ${blocked.currentPath} :: ${blocked.reason}\n`);
|
|
119
|
+
}
|
|
120
|
+
process.stdout.write(`plan: ${plan.planPath ?? "not created"}\nledger: ${ledgerPath}\n`);
|
|
121
|
+
}
|
|
122
|
+
export function printReconcilePlans(results) {
|
|
123
|
+
for (const result of results) {
|
|
124
|
+
process.stdout.write(`plan ${result.plan.planId} [${result.ledger.name}]: ${result.plan.entries.length} entries, ${result.plan.blocked.length} blocked\n`);
|
|
125
|
+
process.stdout.write(`plan: ${result.plan.planPath ?? "not created"}\nledger: ${result.ledger.path}\n`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
106
128
|
export function printTrashListEntries(results) {
|
|
107
129
|
const total = results.reduce((count, result) => count + result.entries.length, 0);
|
|
108
130
|
if (total === 0) {
|
|
@@ -130,6 +152,8 @@ export function summarizeReview(results) {
|
|
|
130
152
|
missingPath: 0,
|
|
131
153
|
executable: 0,
|
|
132
154
|
skipped: 0,
|
|
155
|
+
reconcileEntries: 0,
|
|
156
|
+
reconcileBlocked: 0,
|
|
133
157
|
previewPlanIds: []
|
|
134
158
|
};
|
|
135
159
|
for (const result of results) {
|
|
@@ -145,14 +169,18 @@ export function summarizeReview(results) {
|
|
|
145
169
|
const due = result.due.filter((entry) => entry.dueStatus === "due").length;
|
|
146
170
|
const manualReview = result.due.filter((entry) => entry.dueStatus === "manual-review").length;
|
|
147
171
|
const missingPath = result.due.filter((entry) => entry.dueStatus === "missing-path").length;
|
|
172
|
+
const reconcileEntries = result.reconcile?.plan.entries.length ?? 0;
|
|
173
|
+
const reconcileBlocked = result.reconcile?.plan.blocked.length ?? 0;
|
|
148
174
|
summary.due += due;
|
|
149
175
|
summary.manualReview += manualReview;
|
|
150
176
|
summary.missingPath += missingPath;
|
|
151
177
|
summary.executable += result.plan.entries.length;
|
|
152
178
|
summary.skipped += result.plan.skipped.length;
|
|
179
|
+
summary.reconcileEntries += reconcileEntries;
|
|
180
|
+
summary.reconcileBlocked += reconcileBlocked;
|
|
153
181
|
if (result.plan.planId !== "not-created")
|
|
154
182
|
summary.previewPlanIds.push(result.plan.planId);
|
|
155
|
-
if (!result.validate.ok || due + manualReview + missingPath > 0 || result.plan.entries.length > 0) {
|
|
183
|
+
if (!result.validate.ok || due + manualReview + missingPath + reconcileEntries + reconcileBlocked > 0 || result.plan.entries.length > 0) {
|
|
156
184
|
summary.affected += 1;
|
|
157
185
|
}
|
|
158
186
|
}
|
package/dist/src/ledger.js
CHANGED
|
@@ -3,6 +3,7 @@ import { existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, realpathSy
|
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
5
5
|
import { withPathLock } from "./locks.js";
|
|
6
|
+
import { computeProvenance, validateProvenance } from "./provenance.js";
|
|
6
7
|
import { addTtl, assertIsoDate, ageOf, now, ttlToMs, toIso } from "./time.js";
|
|
7
8
|
const KINDS = new Set([
|
|
8
9
|
"scratch",
|
|
@@ -26,11 +27,11 @@ export function normalizeLedgerPath(path) {
|
|
|
26
27
|
return resolve(path ?? defaultLedgerPath());
|
|
27
28
|
}
|
|
28
29
|
export function putRecord(ledgerPath, input) {
|
|
29
|
-
const record = prepareRecord(input);
|
|
30
|
+
const record = prepareRecord(input, ledgerPath);
|
|
30
31
|
appendPreparedRecord(ledgerPath, record);
|
|
31
32
|
return record;
|
|
32
33
|
}
|
|
33
|
-
export function prepareRecord(input) {
|
|
34
|
+
export function prepareRecord(input, ledgerPath) {
|
|
34
35
|
const artifactPath = resolve(input.path);
|
|
35
36
|
if (!existsSync(artifactPath)) {
|
|
36
37
|
throw new Error(`Path does not exist: ${input.path}`);
|
|
@@ -57,7 +58,8 @@ export function prepareRecord(input) {
|
|
|
57
58
|
cleanup,
|
|
58
59
|
owner: input.owner ?? "manual",
|
|
59
60
|
labels: input.labels,
|
|
60
|
-
status: "active"
|
|
61
|
+
status: "active",
|
|
62
|
+
provenance: computeProvenance(artifactPath, { ledgerPath })
|
|
61
63
|
};
|
|
62
64
|
return record;
|
|
63
65
|
}
|
|
@@ -239,6 +241,13 @@ export function validateLedger(ledgerPath) {
|
|
|
239
241
|
if (!record.resolutionReason)
|
|
240
242
|
errors.push(`${label}: resolved record missing resolutionReason`);
|
|
241
243
|
}
|
|
244
|
+
// Legacy rows simply omit provenance and are left alone; once a row carries
|
|
245
|
+
// provenance it must be well-formed so future reconcile can trust it.
|
|
246
|
+
if ("provenance" in record) {
|
|
247
|
+
for (const problem of validateProvenance(record.provenance)) {
|
|
248
|
+
errors.push(`${label}: ${problem}`);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
242
251
|
}
|
|
243
252
|
return { ok: errors.length === 0, errors, warnings, entries: records.length };
|
|
244
253
|
}
|
|
@@ -589,7 +598,10 @@ export function executeCleanupPlan(ledgerPath, planId) {
|
|
|
589
598
|
return { planId, receiptPath, results };
|
|
590
599
|
});
|
|
591
600
|
}
|
|
592
|
-
|
|
601
|
+
// Exported so the reconcile plan layer (src/reconcile.ts) registers its dry-run plan
|
|
602
|
+
// artifacts through the same upsert-by-path-and-labels path that cleanup plans use,
|
|
603
|
+
// keeping plan files tracked and reused under a stable plan id.
|
|
604
|
+
export function registerArtshelfArtifact(ledgerPath, path, input) {
|
|
593
605
|
const prepared = prepareRecord({
|
|
594
606
|
path,
|
|
595
607
|
reason: input.reason,
|
|
@@ -598,7 +610,7 @@ function registerArtshelfArtifact(ledgerPath, path, input) {
|
|
|
598
610
|
cleanup: input.cleanup,
|
|
599
611
|
owner: "artshelf",
|
|
600
612
|
labels: input.labels
|
|
601
|
-
});
|
|
613
|
+
}, ledgerPath);
|
|
602
614
|
withLedgerLock(ledgerPath, () => {
|
|
603
615
|
const records = readLedger(ledgerPath);
|
|
604
616
|
const index = records.findIndex((record) => (isMatchingArtshelfArtifact(record, path, input.labels) &&
|
|
@@ -684,7 +696,10 @@ function appendRecord(ledgerPath, record) {
|
|
|
684
696
|
atomicWriteFileSync(ledgerPath, `${previous}${previous && !previous.endsWith("\n") ? "\n" : ""}${JSON.stringify(record)}\n`);
|
|
685
697
|
});
|
|
686
698
|
}
|
|
687
|
-
|
|
699
|
+
// Exported so the reconcile execute layer (src/reconcile.ts) persists its mutated
|
|
700
|
+
// records through the canonical JSONL writer + ledger lock instead of duplicating the
|
|
701
|
+
// atomic-write format, keeping the reconcile -> ledger import direction one-way.
|
|
702
|
+
export function writeLedger(ledgerPath, records) {
|
|
688
703
|
withLedgerLock(ledgerPath, () => {
|
|
689
704
|
mkdirSync(dirname(ledgerPath), { recursive: true });
|
|
690
705
|
atomicWriteFileSync(ledgerPath, records.map((record) => JSON.stringify(record)).join("\n") + (records.length > 0 ? "\n" : ""));
|
|
@@ -867,7 +882,7 @@ function pathExistsForPurge(path) {
|
|
|
867
882
|
return false;
|
|
868
883
|
}
|
|
869
884
|
}
|
|
870
|
-
function assertSafeGeneratedId(value, label) {
|
|
885
|
+
export function assertSafeGeneratedId(value, label) {
|
|
871
886
|
if (!/^[A-Za-z0-9_-]+$/.test(value)) {
|
|
872
887
|
throw new Error(`Invalid ${label}: ${value}`);
|
|
873
888
|
}
|