@ag-eco/agentplate-cli 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/LICENSE +21 -0
- package/README.md +462 -0
- package/agents/ap-co-creation.md +90 -0
- package/agents/builder.md +144 -0
- package/agents/coordinator.md +377 -0
- package/agents/lead.md +435 -0
- package/agents/merger.md +164 -0
- package/agents/monitor.md +214 -0
- package/agents/orchestrator.md +239 -0
- package/agents/reviewer.md +140 -0
- package/agents/scout.md +125 -0
- package/agents/supervisor.md +427 -0
- package/package.json +66 -0
- package/src/agents/capabilities.test.ts +85 -0
- package/src/agents/capabilities.ts +125 -0
- package/src/agents/checkpoint.test.ts +88 -0
- package/src/agents/checkpoint.ts +101 -0
- package/src/agents/copilot-hooks-deployer.test.ts +162 -0
- package/src/agents/copilot-hooks-deployer.ts +93 -0
- package/src/agents/guard-rules.test.ts +372 -0
- package/src/agents/guard-rules.ts +97 -0
- package/src/agents/headless-mail-injector.test.ts +709 -0
- package/src/agents/headless-mail-injector.ts +377 -0
- package/src/agents/headless-prompt.test.ts +102 -0
- package/src/agents/headless-prompt.ts +68 -0
- package/src/agents/hooks-deployer.test.ts +3119 -0
- package/src/agents/hooks-deployer.ts +804 -0
- package/src/agents/identity.test.ts +604 -0
- package/src/agents/identity.ts +384 -0
- package/src/agents/lifecycle.test.ts +196 -0
- package/src/agents/lifecycle.ts +183 -0
- package/src/agents/mail-poll-detect.test.ts +153 -0
- package/src/agents/mail-poll-detect.ts +73 -0
- package/src/agents/manifest.test.ts +1026 -0
- package/src/agents/manifest.ts +376 -0
- package/src/agents/overlay.test.ts +1058 -0
- package/src/agents/overlay.ts +490 -0
- package/src/agents/scope-detect.test.ts +190 -0
- package/src/agents/scope-detect.ts +146 -0
- package/src/agents/turn-lock.test.ts +181 -0
- package/src/agents/turn-lock.ts +235 -0
- package/src/agents/turn-runner-dispatch.test.ts +182 -0
- package/src/agents/turn-runner-dispatch.ts +105 -0
- package/src/agents/turn-runner.test.ts +2312 -0
- package/src/agents/turn-runner.ts +1383 -0
- package/src/beads/client.test.ts +217 -0
- package/src/beads/client.ts +230 -0
- package/src/beads/molecules.test.ts +338 -0
- package/src/beads/molecules.ts +198 -0
- package/src/commands/agents.test.ts +328 -0
- package/src/commands/agents.ts +299 -0
- package/src/commands/clean.test.ts +797 -0
- package/src/commands/clean.ts +791 -0
- package/src/commands/completions.test.ts +348 -0
- package/src/commands/completions.ts +981 -0
- package/src/commands/coordinator.test.ts +2975 -0
- package/src/commands/coordinator.ts +1841 -0
- package/src/commands/costs.test.ts +1183 -0
- package/src/commands/costs.ts +599 -0
- package/src/commands/dashboard.test.ts +954 -0
- package/src/commands/dashboard.ts +1212 -0
- package/src/commands/discover.test.ts +288 -0
- package/src/commands/discover.ts +202 -0
- package/src/commands/doctor.test.ts +303 -0
- package/src/commands/doctor.ts +311 -0
- package/src/commands/ecosystem.test.ts +226 -0
- package/src/commands/ecosystem.ts +248 -0
- package/src/commands/errors.test.ts +654 -0
- package/src/commands/errors.ts +197 -0
- package/src/commands/feed.test.ts +709 -0
- package/src/commands/feed.ts +260 -0
- package/src/commands/group.test.ts +475 -0
- package/src/commands/group.ts +546 -0
- package/src/commands/hooks.test.ts +458 -0
- package/src/commands/hooks.ts +263 -0
- package/src/commands/init.test.ts +1011 -0
- package/src/commands/init.ts +967 -0
- package/src/commands/inspect.test.ts +1239 -0
- package/src/commands/inspect.ts +648 -0
- package/src/commands/log.test.ts +1913 -0
- package/src/commands/log.ts +958 -0
- package/src/commands/logs.test.ts +801 -0
- package/src/commands/logs.ts +483 -0
- package/src/commands/mail.test.ts +1501 -0
- package/src/commands/mail.ts +848 -0
- package/src/commands/merge.test.ts +864 -0
- package/src/commands/merge.ts +381 -0
- package/src/commands/metrics.test.ts +458 -0
- package/src/commands/metrics.ts +129 -0
- package/src/commands/monitor.test.ts +191 -0
- package/src/commands/monitor.ts +409 -0
- package/src/commands/nudge.test.ts +579 -0
- package/src/commands/nudge.ts +646 -0
- package/src/commands/orchestrator.ts +42 -0
- package/src/commands/prime.test.ts +612 -0
- package/src/commands/prime.ts +359 -0
- package/src/commands/replay.test.ts +757 -0
- package/src/commands/replay.ts +231 -0
- package/src/commands/run.test.ts +469 -0
- package/src/commands/run.ts +353 -0
- package/src/commands/serve/agent-actions.test.ts +210 -0
- package/src/commands/serve/agent-actions.ts +192 -0
- package/src/commands/serve/build.test.ts +202 -0
- package/src/commands/serve/build.ts +206 -0
- package/src/commands/serve/coordinator-actions.test.ts +339 -0
- package/src/commands/serve/coordinator-actions.ts +410 -0
- package/src/commands/serve/dev.test.ts +168 -0
- package/src/commands/serve/dev.ts +117 -0
- package/src/commands/serve/mail-actions.test.ts +312 -0
- package/src/commands/serve/mail-actions.ts +167 -0
- package/src/commands/serve/rest.test.ts +1680 -0
- package/src/commands/serve/rest.ts +1130 -0
- package/src/commands/serve/static.ts +51 -0
- package/src/commands/serve/ws.test.ts +361 -0
- package/src/commands/serve/ws.ts +332 -0
- package/src/commands/serve.test.ts +459 -0
- package/src/commands/serve.ts +654 -0
- package/src/commands/sling.test.ts +1583 -0
- package/src/commands/sling.ts +1351 -0
- package/src/commands/spec.test.ts +179 -0
- package/src/commands/spec.ts +105 -0
- package/src/commands/status.test.ts +614 -0
- package/src/commands/status.ts +403 -0
- package/src/commands/stop.test.ts +964 -0
- package/src/commands/stop.ts +319 -0
- package/src/commands/supervisor.test.ts +185 -0
- package/src/commands/supervisor.ts +537 -0
- package/src/commands/trace.test.ts +762 -0
- package/src/commands/trace.ts +205 -0
- package/src/commands/update.test.ts +466 -0
- package/src/commands/update.ts +263 -0
- package/src/commands/upgrade.test.ts +48 -0
- package/src/commands/upgrade.ts +240 -0
- package/src/commands/watch.test.ts +257 -0
- package/src/commands/watch.ts +308 -0
- package/src/commands/worktree.test.ts +1297 -0
- package/src/commands/worktree.ts +451 -0
- package/src/config.test.ts +1535 -0
- package/src/config.ts +1064 -0
- package/src/doctor/agents.test.ts +523 -0
- package/src/doctor/agents.ts +399 -0
- package/src/doctor/config-check.test.ts +191 -0
- package/src/doctor/config-check.ts +183 -0
- package/src/doctor/consistency.test.ts +807 -0
- package/src/doctor/consistency.ts +347 -0
- package/src/doctor/databases.test.ts +350 -0
- package/src/doctor/databases.ts +243 -0
- package/src/doctor/dependencies.test.ts +296 -0
- package/src/doctor/dependencies.ts +272 -0
- package/src/doctor/ecosystem.test.ts +308 -0
- package/src/doctor/ecosystem.ts +156 -0
- package/src/doctor/logs.test.ts +253 -0
- package/src/doctor/logs.ts +295 -0
- package/src/doctor/merge-queue.test.ts +315 -0
- package/src/doctor/merge-queue.ts +167 -0
- package/src/doctor/providers.test.ts +409 -0
- package/src/doctor/providers.ts +250 -0
- package/src/doctor/serve.test.ts +95 -0
- package/src/doctor/serve.ts +86 -0
- package/src/doctor/structure.test.ts +423 -0
- package/src/doctor/structure.ts +285 -0
- package/src/doctor/types.ts +43 -0
- package/src/doctor/version.test.ts +241 -0
- package/src/doctor/version.ts +132 -0
- package/src/doctor/watchdog.test.ts +167 -0
- package/src/doctor/watchdog.ts +214 -0
- package/src/e2e/init-sling-lifecycle.test.ts +283 -0
- package/src/errors.test.ts +350 -0
- package/src/errors.ts +217 -0
- package/src/events/store.test.ts +660 -0
- package/src/events/store.ts +369 -0
- package/src/events/tailer.test.ts +719 -0
- package/src/events/tailer.ts +332 -0
- package/src/events/tool-filter.test.ts +330 -0
- package/src/events/tool-filter.ts +126 -0
- package/src/index.ts +533 -0
- package/src/insights/analyzer.test.ts +466 -0
- package/src/insights/analyzer.ts +203 -0
- package/src/insights/quality-gates.test.ts +141 -0
- package/src/insights/quality-gates.ts +156 -0
- package/src/json.test.ts +72 -0
- package/src/json.ts +53 -0
- package/src/loam/client.test.ts +752 -0
- package/src/loam/client.ts +664 -0
- package/src/logging/color.test.ts +252 -0
- package/src/logging/color.ts +105 -0
- package/src/logging/format.test.ts +110 -0
- package/src/logging/format.ts +255 -0
- package/src/logging/logger.test.ts +814 -0
- package/src/logging/logger.ts +266 -0
- package/src/logging/reporter.test.ts +259 -0
- package/src/logging/reporter.ts +110 -0
- package/src/logging/sanitizer.test.ts +190 -0
- package/src/logging/sanitizer.ts +57 -0
- package/src/logging/theme.ts +140 -0
- package/src/mail/broadcast.test.ts +204 -0
- package/src/mail/broadcast.ts +92 -0
- package/src/mail/client.test.ts +774 -0
- package/src/mail/client.ts +236 -0
- package/src/mail/store.test.ts +898 -0
- package/src/mail/store.ts +425 -0
- package/src/merge/lock.test.ts +149 -0
- package/src/merge/lock.ts +140 -0
- package/src/merge/predict.test.ts +387 -0
- package/src/merge/predict.ts +249 -0
- package/src/merge/queue.test.ts +426 -0
- package/src/merge/queue.ts +246 -0
- package/src/merge/resolver.test.ts +1993 -0
- package/src/merge/resolver.ts +926 -0
- package/src/metrics/pricing.test.ts +258 -0
- package/src/metrics/pricing.ts +135 -0
- package/src/metrics/store.test.ts +978 -0
- package/src/metrics/store.ts +501 -0
- package/src/metrics/summary.test.ts +398 -0
- package/src/metrics/summary.ts +178 -0
- package/src/metrics/transcript.test.ts +483 -0
- package/src/metrics/transcript.ts +114 -0
- package/src/runtimes/__fixtures__/claude-stream-fixture.ts +22 -0
- package/src/runtimes/aider.test.ts +124 -0
- package/src/runtimes/aider.ts +147 -0
- package/src/runtimes/amp.test.ts +164 -0
- package/src/runtimes/amp.ts +154 -0
- package/src/runtimes/claude.test.ts +1474 -0
- package/src/runtimes/claude.ts +579 -0
- package/src/runtimes/codex.test.ts +805 -0
- package/src/runtimes/codex.ts +273 -0
- package/src/runtimes/connections.test.ts +214 -0
- package/src/runtimes/connections.ts +103 -0
- package/src/runtimes/copilot.test.ts +707 -0
- package/src/runtimes/copilot.ts +316 -0
- package/src/runtimes/cursor.test.ts +497 -0
- package/src/runtimes/cursor.ts +205 -0
- package/src/runtimes/gemini.test.ts +537 -0
- package/src/runtimes/gemini.ts +243 -0
- package/src/runtimes/goose.test.ts +133 -0
- package/src/runtimes/goose.ts +157 -0
- package/src/runtimes/headless-connection.test.ts +264 -0
- package/src/runtimes/headless-connection.ts +158 -0
- package/src/runtimes/opencode.test.ts +325 -0
- package/src/runtimes/opencode.ts +188 -0
- package/src/runtimes/pi-guards.test.ts +486 -0
- package/src/runtimes/pi-guards.ts +367 -0
- package/src/runtimes/pi.test.ts +789 -0
- package/src/runtimes/pi.ts +305 -0
- package/src/runtimes/registry.test.ts +196 -0
- package/src/runtimes/registry.ts +99 -0
- package/src/runtimes/sapling.test.ts +1267 -0
- package/src/runtimes/sapling.ts +710 -0
- package/src/runtimes/types.ts +266 -0
- package/src/schema-consistency.test.ts +246 -0
- package/src/sessions/compat.test.ts +281 -0
- package/src/sessions/compat.ts +105 -0
- package/src/sessions/store.test.ts +1748 -0
- package/src/sessions/store.ts +858 -0
- package/src/test-helpers.test.ts +124 -0
- package/src/test-helpers.ts +145 -0
- package/src/test-setup.test.ts +31 -0
- package/src/test-setup.ts +28 -0
- package/src/tools/loam/api.ts +368 -0
- package/src/tools/loam/cli.ts +278 -0
- package/src/tools/loam/commands/add.ts +52 -0
- package/src/tools/loam/commands/archive.ts +214 -0
- package/src/tools/loam/commands/audit.ts +276 -0
- package/src/tools/loam/commands/compact.ts +1062 -0
- package/src/tools/loam/commands/completions.ts +79 -0
- package/src/tools/loam/commands/config.ts +381 -0
- package/src/tools/loam/commands/delete-domain.ts +121 -0
- package/src/tools/loam/commands/delete.ts +316 -0
- package/src/tools/loam/commands/diff.ts +200 -0
- package/src/tools/loam/commands/doctor.ts +1113 -0
- package/src/tools/loam/commands/edit.ts +226 -0
- package/src/tools/loam/commands/init.ts +31 -0
- package/src/tools/loam/commands/learn.ts +179 -0
- package/src/tools/loam/commands/move.ts +323 -0
- package/src/tools/loam/commands/onboard.ts +374 -0
- package/src/tools/loam/commands/outcome.ts +185 -0
- package/src/tools/loam/commands/prime.ts +688 -0
- package/src/tools/loam/commands/prune.ts +614 -0
- package/src/tools/loam/commands/query.ts +218 -0
- package/src/tools/loam/commands/rank.ts +180 -0
- package/src/tools/loam/commands/ready.ts +189 -0
- package/src/tools/loam/commands/record.ts +1210 -0
- package/src/tools/loam/commands/restore.ts +166 -0
- package/src/tools/loam/commands/search.ts +327 -0
- package/src/tools/loam/commands/setup.ts +887 -0
- package/src/tools/loam/commands/status.ts +103 -0
- package/src/tools/loam/commands/sync.ts +298 -0
- package/src/tools/loam/commands/update.ts +19 -0
- package/src/tools/loam/commands/upgrade.ts +93 -0
- package/src/tools/loam/commands/validate.ts +190 -0
- package/src/tools/loam/index.ts +62 -0
- package/src/tools/loam/log.ts +127 -0
- package/src/tools/loam/registry/builtins.ts +409 -0
- package/src/tools/loam/registry/custom.ts +431 -0
- package/src/tools/loam/registry/init.ts +55 -0
- package/src/tools/loam/registry/template.ts +40 -0
- package/src/tools/loam/registry/type-registry.ts +113 -0
- package/src/tools/loam/schemas/config-schema.ts +489 -0
- package/src/tools/loam/schemas/config.ts +245 -0
- package/src/tools/loam/schemas/index.ts +18 -0
- package/src/tools/loam/schemas/record-schema.ts +191 -0
- package/src/tools/loam/schemas/record.ts +115 -0
- package/src/tools/loam/utils/active-work.ts +205 -0
- package/src/tools/loam/utils/anchor-validity.ts +80 -0
- package/src/tools/loam/utils/archive.ts +146 -0
- package/src/tools/loam/utils/audit.ts +667 -0
- package/src/tools/loam/utils/bm25.ts +238 -0
- package/src/tools/loam/utils/budget.ts +142 -0
- package/src/tools/loam/utils/config.ts +344 -0
- package/src/tools/loam/utils/dir-anchors.ts +62 -0
- package/src/tools/loam/utils/domain-rules.ts +114 -0
- package/src/tools/loam/utils/expertise.ts +393 -0
- package/src/tools/loam/utils/format-helpers.ts +96 -0
- package/src/tools/loam/utils/format.ts +1234 -0
- package/src/tools/loam/utils/git-context.ts +50 -0
- package/src/tools/loam/utils/git.ts +183 -0
- package/src/tools/loam/utils/hooks.ts +299 -0
- package/src/tools/loam/utils/index.ts +52 -0
- package/src/tools/loam/utils/json-output.ts +13 -0
- package/src/tools/loam/utils/lock.ts +76 -0
- package/src/tools/loam/utils/markers.ts +48 -0
- package/src/tools/loam/utils/numeric-flags.ts +20 -0
- package/src/tools/loam/utils/palette.ts +44 -0
- package/src/tools/loam/utils/prime-ranking.ts +135 -0
- package/src/tools/loam/utils/recipe-discovery.ts +195 -0
- package/src/tools/loam/utils/runtime-flags.ts +28 -0
- package/src/tools/loam/utils/scoring.ts +94 -0
- package/src/tools/loam/utils/version.ts +116 -0
- package/src/tools/sprout/commands/block.ts +64 -0
- package/src/tools/sprout/commands/blocked.ts +86 -0
- package/src/tools/sprout/commands/close.ts +129 -0
- package/src/tools/sprout/commands/completions.ts +198 -0
- package/src/tools/sprout/commands/config.ts +238 -0
- package/src/tools/sprout/commands/create.ts +164 -0
- package/src/tools/sprout/commands/dep.ts +148 -0
- package/src/tools/sprout/commands/doctor.ts +979 -0
- package/src/tools/sprout/commands/init.ts +83 -0
- package/src/tools/sprout/commands/label.ts +178 -0
- package/src/tools/sprout/commands/list.ts +210 -0
- package/src/tools/sprout/commands/migrate.ts +133 -0
- package/src/tools/sprout/commands/onboard.ts +207 -0
- package/src/tools/sprout/commands/plan-show.ts +278 -0
- package/src/tools/sprout/commands/plan.ts +2526 -0
- package/src/tools/sprout/commands/prime.ts +399 -0
- package/src/tools/sprout/commands/ready.ts +245 -0
- package/src/tools/sprout/commands/search.ts +221 -0
- package/src/tools/sprout/commands/show.ts +277 -0
- package/src/tools/sprout/commands/stats.ts +146 -0
- package/src/tools/sprout/commands/sync.ts +134 -0
- package/src/tools/sprout/commands/tpl.ts +364 -0
- package/src/tools/sprout/commands/unblock.ts +115 -0
- package/src/tools/sprout/commands/update.ts +257 -0
- package/src/tools/sprout/commands/upgrade.ts +91 -0
- package/src/tools/sprout/config-schema.ts +152 -0
- package/src/tools/sprout/config.ts +355 -0
- package/src/tools/sprout/filter.ts +107 -0
- package/src/tools/sprout/format.ts +43 -0
- package/src/tools/sprout/id.ts +22 -0
- package/src/tools/sprout/index.ts +204 -0
- package/src/tools/sprout/log.ts +76 -0
- package/src/tools/sprout/markers.ts +22 -0
- package/src/tools/sprout/output.ts +121 -0
- package/src/tools/sprout/plan-backref.ts +93 -0
- package/src/tools/sprout/plan-context.ts +81 -0
- package/src/tools/sprout/plan-domain.ts +139 -0
- package/src/tools/sprout/plan-lifecycle.ts +65 -0
- package/src/tools/sprout/plan-loam.ts +207 -0
- package/src/tools/sprout/plan-schema.ts +209 -0
- package/src/tools/sprout/sort.ts +31 -0
- package/src/tools/sprout/store.ts +172 -0
- package/src/tools/sprout/types.ts +118 -0
- package/src/tools/sprout/validation.ts +119 -0
- package/src/tools/sprout/version.ts +1 -0
- package/src/tools/sprout/yaml.ts +387 -0
- package/src/tools/trellis/commands/archive.ts +87 -0
- package/src/tools/trellis/commands/completions.ts +610 -0
- package/src/tools/trellis/commands/config.ts +382 -0
- package/src/tools/trellis/commands/create.ts +252 -0
- package/src/tools/trellis/commands/diff.ts +150 -0
- package/src/tools/trellis/commands/doctor.ts +771 -0
- package/src/tools/trellis/commands/emit.ts +365 -0
- package/src/tools/trellis/commands/history.ts +83 -0
- package/src/tools/trellis/commands/import.ts +198 -0
- package/src/tools/trellis/commands/init.ts +81 -0
- package/src/tools/trellis/commands/list.ts +103 -0
- package/src/tools/trellis/commands/onboard.ts +156 -0
- package/src/tools/trellis/commands/pin.ts +172 -0
- package/src/tools/trellis/commands/prime.ts +193 -0
- package/src/tools/trellis/commands/render.ts +122 -0
- package/src/tools/trellis/commands/schema.ts +353 -0
- package/src/tools/trellis/commands/show.ts +115 -0
- package/src/tools/trellis/commands/stats.ts +65 -0
- package/src/tools/trellis/commands/sync.ts +112 -0
- package/src/tools/trellis/commands/tree.ts +123 -0
- package/src/tools/trellis/commands/update.ts +330 -0
- package/src/tools/trellis/commands/upgrade.ts +95 -0
- package/src/tools/trellis/commands/validate.ts +166 -0
- package/src/tools/trellis/config-schema.ts +81 -0
- package/src/tools/trellis/config.ts +108 -0
- package/src/tools/trellis/frontmatter.ts +348 -0
- package/src/tools/trellis/id.ts +24 -0
- package/src/tools/trellis/index.ts +209 -0
- package/src/tools/trellis/markers.ts +28 -0
- package/src/tools/trellis/output.ts +84 -0
- package/src/tools/trellis/render.ts +212 -0
- package/src/tools/trellis/store.ts +144 -0
- package/src/tools/trellis/types.ts +82 -0
- package/src/tools/trellis/validate.ts +199 -0
- package/src/tools/trellis/yaml.ts +309 -0
- package/src/tracker/beads.test.ts +454 -0
- package/src/tracker/beads.ts +56 -0
- package/src/tracker/factory.test.ts +90 -0
- package/src/tracker/factory.ts +65 -0
- package/src/tracker/sprout.test.ts +461 -0
- package/src/tracker/sprout.ts +182 -0
- package/src/tracker/types.ts +52 -0
- package/src/trellis/client.test.ts +107 -0
- package/src/trellis/client.ts +179 -0
- package/src/types.ts +970 -0
- package/src/utils/bin.test.ts +10 -0
- package/src/utils/bin.ts +37 -0
- package/src/utils/browser.test.ts +49 -0
- package/src/utils/browser.ts +48 -0
- package/src/utils/fs.test.ts +119 -0
- package/src/utils/fs.ts +62 -0
- package/src/utils/pid.test.ts +152 -0
- package/src/utils/pid.ts +130 -0
- package/src/utils/process-scan.test.ts +53 -0
- package/src/utils/process-scan.ts +76 -0
- package/src/utils/time.test.ts +43 -0
- package/src/utils/time.ts +37 -0
- package/src/utils/version.test.ts +33 -0
- package/src/utils/version.ts +70 -0
- package/src/version.ts +5 -0
- package/src/watchdog/daemon.test.ts +3721 -0
- package/src/watchdog/daemon.ts +1257 -0
- package/src/watchdog/health.test.ts +830 -0
- package/src/watchdog/health.ts +434 -0
- package/src/watchdog/triage.test.ts +205 -0
- package/src/watchdog/triage.ts +205 -0
- package/src/worktree/manager.test.ts +720 -0
- package/src/worktree/manager.ts +405 -0
- package/src/worktree/process.test.ts +172 -0
- package/src/worktree/process.ts +131 -0
- package/src/worktree/tmux.test.ts +1616 -0
- package/src/worktree/tmux.ts +721 -0
- package/templates/CLAUDE.md.tmpl +100 -0
- package/templates/copilot-hooks.json.tmpl +13 -0
- package/templates/hooks.json.tmpl +109 -0
- package/templates/overlay.md.tmpl +88 -0
- package/ui/dist/apple-touch-icon-bdy6teep.png +0 -0
- package/ui/dist/chunk-8s31f05k.css +1 -0
- package/ui/dist/chunk-vm5rz679.js +300 -0
- package/ui/dist/favicon-nzb39vza.svg +4 -0
- package/ui/dist/index.html +17 -0
|
@@ -0,0 +1,614 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import type { Command } from "commander";
|
|
3
|
+
import {
|
|
4
|
+
DEFAULT_ANCHOR_VALIDITY_GRACE_DAYS,
|
|
5
|
+
DEFAULT_ANCHOR_VALIDITY_THRESHOLD,
|
|
6
|
+
validateAnchorValidityConfig,
|
|
7
|
+
} from "../schemas/config.ts";
|
|
8
|
+
import type { Classification, ExpertiseRecord } from "../schemas/record.ts";
|
|
9
|
+
import {
|
|
10
|
+
type AnchorValidity,
|
|
11
|
+
computeAnchorValidity,
|
|
12
|
+
passedAnchorGrace,
|
|
13
|
+
} from "../utils/anchor-validity.ts";
|
|
14
|
+
import { archiveRecords } from "../utils/archive.ts";
|
|
15
|
+
import { getExpertisePath, readConfig } from "../utils/config.ts";
|
|
16
|
+
import { readExpertiseFile, writeExpertiseFile } from "../utils/expertise.ts";
|
|
17
|
+
import { runHooks } from "../utils/hooks.ts";
|
|
18
|
+
import { outputJson, outputJsonError } from "../utils/json-output.ts";
|
|
19
|
+
import { withFileLock } from "../utils/lock.ts";
|
|
20
|
+
import { brand, isQuiet } from "../utils/palette.ts";
|
|
21
|
+
|
|
22
|
+
interface PruneResult {
|
|
23
|
+
domain: string;
|
|
24
|
+
before: number;
|
|
25
|
+
pruned: number;
|
|
26
|
+
demoted: number;
|
|
27
|
+
anchor_demoted: number;
|
|
28
|
+
supersession_demoted: number;
|
|
29
|
+
after: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
type ActionReason = "stale" | "superseded" | "anchor_decay";
|
|
33
|
+
|
|
34
|
+
interface RecordAction {
|
|
35
|
+
domain: string;
|
|
36
|
+
id?: string;
|
|
37
|
+
type: string;
|
|
38
|
+
from: Classification;
|
|
39
|
+
// "archived" when the record bottomed out and moved to the archive (or was
|
|
40
|
+
// hard-deleted with --hard).
|
|
41
|
+
to: Classification | "archived";
|
|
42
|
+
reasons: ActionReason[];
|
|
43
|
+
anchors?: {
|
|
44
|
+
valid_fraction: number;
|
|
45
|
+
valid: number;
|
|
46
|
+
total: number;
|
|
47
|
+
broken: { kind: string; path: string }[];
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function isStale(
|
|
52
|
+
record: ExpertiseRecord,
|
|
53
|
+
now: Date,
|
|
54
|
+
shelfLife: { tactical: number; observational: number },
|
|
55
|
+
): boolean {
|
|
56
|
+
const classification: Classification = record.classification;
|
|
57
|
+
|
|
58
|
+
if (classification === "foundational") {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const recordedAt = new Date(record.recorded_at);
|
|
63
|
+
const ageInDays = Math.floor((now.getTime() - recordedAt.getTime()) / (1000 * 60 * 60 * 24));
|
|
64
|
+
|
|
65
|
+
if (classification === "tactical") {
|
|
66
|
+
return ageInDays > shelfLife.tactical;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (classification === "observational") {
|
|
70
|
+
return ageInDays > shelfLife.observational;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Next classification tier in the supersession-demotion ladder. Returns null
|
|
78
|
+
* when the record has bottomed out and should be archived (or hard-deleted
|
|
79
|
+
* with --hard).
|
|
80
|
+
*/
|
|
81
|
+
function nextDemotionTier(c: Classification): Classification | null {
|
|
82
|
+
if (c === "foundational") return "tactical";
|
|
83
|
+
if (c === "tactical") return "observational";
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Identify ids that participate in any supersession cycle (SCC of size > 1).
|
|
89
|
+
* Iterative Tarjan, so long chains and self-loops can't blow the stack.
|
|
90
|
+
* Self-loops are filtered at edge-collection time so they don't register as
|
|
91
|
+
* trivial cycles.
|
|
92
|
+
*/
|
|
93
|
+
function findSupersessionCycleIds(graph: ReadonlyMap<string, Set<string>>): Set<string> {
|
|
94
|
+
const cycleIds = new Set<string>();
|
|
95
|
+
const indices = new Map<string, number>();
|
|
96
|
+
const lowlinks = new Map<string, number>();
|
|
97
|
+
const onStack = new Set<string>();
|
|
98
|
+
const stack: string[] = [];
|
|
99
|
+
let nextIndex = 0;
|
|
100
|
+
|
|
101
|
+
type Frame = { node: string; iter: Iterator<string>; pendingChild: string | null };
|
|
102
|
+
for (const start of graph.keys()) {
|
|
103
|
+
if (indices.has(start)) continue;
|
|
104
|
+
const callStack: Frame[] = [];
|
|
105
|
+
const open = (node: string) => {
|
|
106
|
+
indices.set(node, nextIndex);
|
|
107
|
+
lowlinks.set(node, nextIndex);
|
|
108
|
+
nextIndex++;
|
|
109
|
+
stack.push(node);
|
|
110
|
+
onStack.add(node);
|
|
111
|
+
const edges = graph.get(node);
|
|
112
|
+
callStack.push({
|
|
113
|
+
node,
|
|
114
|
+
iter: (edges ?? new Set<string>()).values(),
|
|
115
|
+
pendingChild: null,
|
|
116
|
+
});
|
|
117
|
+
};
|
|
118
|
+
open(start);
|
|
119
|
+
|
|
120
|
+
while (callStack.length > 0) {
|
|
121
|
+
const frame = callStack[callStack.length - 1];
|
|
122
|
+
if (!frame) break;
|
|
123
|
+
if (frame.pendingChild !== null) {
|
|
124
|
+
const childLow = lowlinks.get(frame.pendingChild);
|
|
125
|
+
const nodeLow = lowlinks.get(frame.node);
|
|
126
|
+
if (childLow !== undefined && nodeLow !== undefined && childLow < nodeLow) {
|
|
127
|
+
lowlinks.set(frame.node, childLow);
|
|
128
|
+
}
|
|
129
|
+
frame.pendingChild = null;
|
|
130
|
+
}
|
|
131
|
+
const next = frame.iter.next();
|
|
132
|
+
if (next.done) {
|
|
133
|
+
const idx = indices.get(frame.node);
|
|
134
|
+
const low = lowlinks.get(frame.node);
|
|
135
|
+
if (idx !== undefined && low !== undefined && idx === low) {
|
|
136
|
+
const component: string[] = [];
|
|
137
|
+
while (stack.length > 0) {
|
|
138
|
+
const popped = stack.pop();
|
|
139
|
+
if (popped === undefined) break;
|
|
140
|
+
onStack.delete(popped);
|
|
141
|
+
component.push(popped);
|
|
142
|
+
if (popped === frame.node) break;
|
|
143
|
+
}
|
|
144
|
+
if (component.length > 1) {
|
|
145
|
+
for (const c of component) cycleIds.add(c);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
callStack.pop();
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
const target = next.value;
|
|
152
|
+
if (!indices.has(target)) {
|
|
153
|
+
frame.pendingChild = target;
|
|
154
|
+
open(target);
|
|
155
|
+
} else if (onStack.has(target)) {
|
|
156
|
+
const targetIdx = indices.get(target);
|
|
157
|
+
const nodeLow = lowlinks.get(frame.node);
|
|
158
|
+
if (targetIdx !== undefined && nodeLow !== undefined && targetIdx < nodeLow) {
|
|
159
|
+
lowlinks.set(frame.node, targetIdx);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return cycleIds;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Set of record IDs referenced by any live record's `supersedes` field,
|
|
169
|
+
* unioned across every domain. Cross-domain by design — supersession is
|
|
170
|
+
* content-relational, not domain-bound. Self-references are filtered out,
|
|
171
|
+
* and any record that participates in a multi-record cycle (e.g. A↔B) is
|
|
172
|
+
* excluded so cycle members aren't both demoted/archived together.
|
|
173
|
+
*/
|
|
174
|
+
function collectSupersededIds(liveByDomain: ReadonlyArray<{ records: ExpertiseRecord[] }>): {
|
|
175
|
+
supersededIds: Set<string>;
|
|
176
|
+
cycleIds: Set<string>;
|
|
177
|
+
} {
|
|
178
|
+
const graph = new Map<string, Set<string>>();
|
|
179
|
+
const allEdges: Array<[string, string]> = [];
|
|
180
|
+
for (const { records } of liveByDomain) {
|
|
181
|
+
for (const r of records) {
|
|
182
|
+
if (!r.id || !r.supersedes || r.supersedes.length === 0) continue;
|
|
183
|
+
let edges = graph.get(r.id);
|
|
184
|
+
if (!edges) {
|
|
185
|
+
edges = new Set<string>();
|
|
186
|
+
graph.set(r.id, edges);
|
|
187
|
+
}
|
|
188
|
+
for (const targetId of r.supersedes) {
|
|
189
|
+
if (targetId === r.id) continue;
|
|
190
|
+
edges.add(targetId);
|
|
191
|
+
if (!graph.has(targetId)) graph.set(targetId, new Set<string>());
|
|
192
|
+
allEdges.push([r.id, targetId]);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const cycleIds = findSupersessionCycleIds(graph);
|
|
198
|
+
const supersededIds = new Set<string>();
|
|
199
|
+
for (const [, target] of allEdges) {
|
|
200
|
+
if (cycleIds.has(target)) continue;
|
|
201
|
+
supersededIds.add(target);
|
|
202
|
+
}
|
|
203
|
+
return { supersededIds, cycleIds };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export function registerPruneCommand(program: Command): void {
|
|
207
|
+
program
|
|
208
|
+
.command("prune")
|
|
209
|
+
.description(
|
|
210
|
+
"Soft-archive (default) or hard-delete stale records, plus tier-demote superseded ones",
|
|
211
|
+
)
|
|
212
|
+
.option("--dry-run", "Show what would be pruned without removing", false)
|
|
213
|
+
.option(
|
|
214
|
+
"--hard",
|
|
215
|
+
"Permanently delete stale records instead of moving them to .loam/archive/",
|
|
216
|
+
false,
|
|
217
|
+
)
|
|
218
|
+
.option(
|
|
219
|
+
"--aggressive",
|
|
220
|
+
"Collapse superseded records straight to archived in one pass instead of one tier at a time",
|
|
221
|
+
false,
|
|
222
|
+
)
|
|
223
|
+
.option(
|
|
224
|
+
"--check-anchors",
|
|
225
|
+
"Demote records whose file/dir anchors no longer resolve (R-05f)",
|
|
226
|
+
false,
|
|
227
|
+
)
|
|
228
|
+
.option(
|
|
229
|
+
"--explain",
|
|
230
|
+
"Print per-record reasons for each demotion (anchor list + decision)",
|
|
231
|
+
false,
|
|
232
|
+
)
|
|
233
|
+
.action(
|
|
234
|
+
async (options: {
|
|
235
|
+
dryRun: boolean;
|
|
236
|
+
hard: boolean;
|
|
237
|
+
aggressive: boolean;
|
|
238
|
+
checkAnchors: boolean;
|
|
239
|
+
explain: boolean;
|
|
240
|
+
}) => {
|
|
241
|
+
const jsonMode = program.opts().json === true;
|
|
242
|
+
const config = await readConfig();
|
|
243
|
+
const now = new Date();
|
|
244
|
+
const shelfLife = config.classification_defaults.shelf_life;
|
|
245
|
+
const projectRoot = process.cwd();
|
|
246
|
+
const anchorCfg = config.decay?.anchor_validity ?? {};
|
|
247
|
+
const anchorValidationErrors = validateAnchorValidityConfig(anchorCfg);
|
|
248
|
+
if (anchorValidationErrors.length > 0) {
|
|
249
|
+
const msg = `Invalid decay.anchor_validity config: ${anchorValidationErrors.join("; ")}. Edit .loam/loam.config.yaml.`;
|
|
250
|
+
if (jsonMode) {
|
|
251
|
+
outputJsonError("prune", msg);
|
|
252
|
+
} else {
|
|
253
|
+
console.error(chalk.red(`Error: ${msg}`));
|
|
254
|
+
}
|
|
255
|
+
process.exitCode = 1;
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
const anchorThreshold = anchorCfg.threshold ?? DEFAULT_ANCHOR_VALIDITY_THRESHOLD;
|
|
259
|
+
const anchorGrace = anchorCfg.grace_days ?? DEFAULT_ANCHOR_VALIDITY_GRACE_DAYS;
|
|
260
|
+
|
|
261
|
+
const results: PruneResult[] = [];
|
|
262
|
+
const actions: RecordAction[] = [];
|
|
263
|
+
let totalPruned = 0;
|
|
264
|
+
let totalDemoted = 0;
|
|
265
|
+
let totalAnchorDemoted = 0;
|
|
266
|
+
let totalSupersessionDemoted = 0;
|
|
267
|
+
|
|
268
|
+
// Phase 1 — preview: load every live record across all domains so
|
|
269
|
+
// we can detect staleness candidates AND build the cross-domain
|
|
270
|
+
// supersession set in one pass. No locks here; phase 3 re-reads
|
|
271
|
+
// each candidate domain under its lock so concurrent writers don't
|
|
272
|
+
// lose data.
|
|
273
|
+
const liveByDomain: Array<{ domain: string; records: ExpertiseRecord[] }> = [];
|
|
274
|
+
for (const domain of Object.keys(config.domains)) {
|
|
275
|
+
const filePath = getExpertisePath(domain);
|
|
276
|
+
const records = await readExpertiseFile(filePath);
|
|
277
|
+
liveByDomain.push({ domain, records });
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const { supersededIds, cycleIds } = collectSupersededIds(liveByDomain);
|
|
281
|
+
if (cycleIds.size > 0 && !jsonMode && !isQuiet()) {
|
|
282
|
+
console.error(
|
|
283
|
+
chalk.yellow(
|
|
284
|
+
`Warning: supersession cycle detected for ${cycleIds.size} record(s); cycle members will not be demoted. Run \`lm doctor\` for details.`,
|
|
285
|
+
),
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Per-record anchor validity, keyed by record id (only when
|
|
290
|
+
// --check-anchors is set). Records with no id are evaluated inline
|
|
291
|
+
// inside phase 3 since they can't be looked up cross-pass.
|
|
292
|
+
const anchorValidityById = new Map<string, AnchorValidity>();
|
|
293
|
+
if (options.checkAnchors) {
|
|
294
|
+
for (const { records } of liveByDomain) {
|
|
295
|
+
for (const r of records) {
|
|
296
|
+
if (!r.id) continue;
|
|
297
|
+
anchorValidityById.set(r.id, computeAnchorValidity(r, projectRoot));
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const isAnchorDecayed = (r: ExpertiseRecord): AnchorValidity | null => {
|
|
303
|
+
if (!options.checkAnchors) return null;
|
|
304
|
+
if (!passedAnchorGrace(r, now, anchorGrace)) return null;
|
|
305
|
+
const v = r.id ? anchorValidityById.get(r.id) : computeAnchorValidity(r, projectRoot);
|
|
306
|
+
if (!v) return null;
|
|
307
|
+
if (v.validFraction === null) return null; // exempt: zero anchors
|
|
308
|
+
if (v.validFraction >= anchorThreshold) return null;
|
|
309
|
+
return v;
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
const candidatesByDomain: Array<{
|
|
313
|
+
domain: string;
|
|
314
|
+
stale: ExpertiseRecord[];
|
|
315
|
+
demote: ExpertiseRecord[];
|
|
316
|
+
anchor_decay: ExpertiseRecord[];
|
|
317
|
+
}> = [];
|
|
318
|
+
for (const { domain, records } of liveByDomain) {
|
|
319
|
+
const stale = records.filter((r) => isStale(r, now, shelfLife));
|
|
320
|
+
const staleIds = new Set(stale.map((r) => r.id).filter((id): id is string => !!id));
|
|
321
|
+
const demote = records.filter(
|
|
322
|
+
(r) => r.id !== undefined && supersededIds.has(r.id) && !staleIds.has(r.id),
|
|
323
|
+
);
|
|
324
|
+
const anchor_decay = records.filter(
|
|
325
|
+
(r) => isAnchorDecayed(r) !== null && !stale.includes(r),
|
|
326
|
+
);
|
|
327
|
+
if (stale.length > 0 || demote.length > 0 || anchor_decay.length > 0) {
|
|
328
|
+
candidatesByDomain.push({ domain, stale, demote, anchor_decay });
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Phase 2 — pre-prune hook. Skipped in dry-run since hooks like
|
|
333
|
+
// digest-then-confirm imply user interaction that shouldn't fire on a
|
|
334
|
+
// preview. Block-on-non-zero, no payload mutation per spec.
|
|
335
|
+
if (!options.dryRun && candidatesByDomain.length > 0) {
|
|
336
|
+
const hookRes = await runHooks("pre-prune", { candidates: candidatesByDomain });
|
|
337
|
+
if (hookRes.blocked) {
|
|
338
|
+
const reason = hookRes.blockReason ?? "pre-prune hook blocked";
|
|
339
|
+
if (jsonMode) {
|
|
340
|
+
outputJsonError("prune", reason);
|
|
341
|
+
} else {
|
|
342
|
+
console.error(chalk.red(`Error: ${reason}`));
|
|
343
|
+
}
|
|
344
|
+
process.exitCode = 1;
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
for (const w of hookRes.warnings) {
|
|
348
|
+
if (!jsonMode) console.error(chalk.yellow(`Warning: ${w}`));
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Phase 3 — perform writes. Re-read under the lock to absorb any
|
|
353
|
+
// records added since phase 1. Staleness wins over supersession on a
|
|
354
|
+
// record that hits both: no point demoting something we're already
|
|
355
|
+
// archiving. A record that's both superseded AND anchor-decayed
|
|
356
|
+
// still demotes only one tier per pass; both reasons get stamped
|
|
357
|
+
// onto the kept record.
|
|
358
|
+
const candidateDomains = new Set(candidatesByDomain.map((c) => c.domain));
|
|
359
|
+
for (const domain of Object.keys(config.domains)) {
|
|
360
|
+
if (!candidateDomains.has(domain)) continue;
|
|
361
|
+
const filePath = getExpertisePath(domain);
|
|
362
|
+
|
|
363
|
+
const archived: ExpertiseRecord[] = [];
|
|
364
|
+
const domainActions: RecordAction[] = [];
|
|
365
|
+
const domainResult = await withFileLock(filePath, async () => {
|
|
366
|
+
const records = await readExpertiseFile(filePath);
|
|
367
|
+
if (records.length === 0) return null;
|
|
368
|
+
|
|
369
|
+
const kept: ExpertiseRecord[] = [];
|
|
370
|
+
let pruned = 0;
|
|
371
|
+
let demoted = 0;
|
|
372
|
+
let anchorDemoted = 0;
|
|
373
|
+
let supersessionDemoted = 0;
|
|
374
|
+
|
|
375
|
+
for (const record of records) {
|
|
376
|
+
if (isStale(record, now, shelfLife)) {
|
|
377
|
+
pruned++;
|
|
378
|
+
archived.push(record);
|
|
379
|
+
domainActions.push({
|
|
380
|
+
domain,
|
|
381
|
+
id: record.id,
|
|
382
|
+
type: record.type,
|
|
383
|
+
from: record.classification,
|
|
384
|
+
to: "archived",
|
|
385
|
+
reasons: ["stale"],
|
|
386
|
+
});
|
|
387
|
+
continue;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
const supersededHit = !!record.id && supersededIds.has(record.id);
|
|
391
|
+
const anchorHit = isAnchorDecayed(record);
|
|
392
|
+
|
|
393
|
+
if (!supersededHit && !anchorHit) {
|
|
394
|
+
kept.push(record);
|
|
395
|
+
continue;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
const reasons: ActionReason[] = [];
|
|
399
|
+
if (supersededHit) reasons.push("superseded");
|
|
400
|
+
if (anchorHit) reasons.push("anchor_decay");
|
|
401
|
+
|
|
402
|
+
const target = options.aggressive ? null : nextDemotionTier(record.classification);
|
|
403
|
+
if (target === null) {
|
|
404
|
+
pruned++;
|
|
405
|
+
// Bottom-out via supersession/anchor_decay — stamp the
|
|
406
|
+
// archive_reason inline so the multi-record archive write
|
|
407
|
+
// in this domain preserves per-record reasons (vs the
|
|
408
|
+
// caller-wide reason param passed to archiveRecords).
|
|
409
|
+
const bottomReason =
|
|
410
|
+
supersededHit && anchorHit
|
|
411
|
+
? "superseded+anchor_decay"
|
|
412
|
+
: supersededHit
|
|
413
|
+
? "superseded"
|
|
414
|
+
: "anchor_decay";
|
|
415
|
+
archived.push({ ...record, archive_reason: bottomReason });
|
|
416
|
+
domainActions.push({
|
|
417
|
+
domain,
|
|
418
|
+
id: record.id,
|
|
419
|
+
type: record.type,
|
|
420
|
+
from: record.classification,
|
|
421
|
+
to: "archived",
|
|
422
|
+
reasons,
|
|
423
|
+
...(anchorHit
|
|
424
|
+
? {
|
|
425
|
+
anchors: {
|
|
426
|
+
valid_fraction: anchorHit.validFraction ?? 0,
|
|
427
|
+
valid: anchorHit.valid,
|
|
428
|
+
total: anchorHit.total,
|
|
429
|
+
broken: anchorHit.broken,
|
|
430
|
+
},
|
|
431
|
+
}
|
|
432
|
+
: {}),
|
|
433
|
+
});
|
|
434
|
+
continue;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const demotedRecord: ExpertiseRecord = {
|
|
438
|
+
...record,
|
|
439
|
+
classification: target,
|
|
440
|
+
};
|
|
441
|
+
if (supersededHit) {
|
|
442
|
+
demotedRecord.supersession_demoted_at = now.toISOString();
|
|
443
|
+
supersessionDemoted++;
|
|
444
|
+
}
|
|
445
|
+
if (anchorHit) {
|
|
446
|
+
demotedRecord.anchor_decay_demoted_at = now.toISOString();
|
|
447
|
+
anchorDemoted++;
|
|
448
|
+
}
|
|
449
|
+
kept.push(demotedRecord);
|
|
450
|
+
demoted++;
|
|
451
|
+
domainActions.push({
|
|
452
|
+
domain,
|
|
453
|
+
id: record.id,
|
|
454
|
+
type: record.type,
|
|
455
|
+
from: record.classification,
|
|
456
|
+
to: target,
|
|
457
|
+
reasons,
|
|
458
|
+
...(anchorHit
|
|
459
|
+
? {
|
|
460
|
+
anchors: {
|
|
461
|
+
valid_fraction: anchorHit.validFraction ?? 0,
|
|
462
|
+
valid: anchorHit.valid,
|
|
463
|
+
total: anchorHit.total,
|
|
464
|
+
broken: anchorHit.broken,
|
|
465
|
+
},
|
|
466
|
+
}
|
|
467
|
+
: {}),
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
if (pruned > 0 || demoted > 0) {
|
|
472
|
+
if (!options.dryRun) {
|
|
473
|
+
await writeExpertiseFile(filePath, kept);
|
|
474
|
+
}
|
|
475
|
+
return {
|
|
476
|
+
domain,
|
|
477
|
+
before: records.length,
|
|
478
|
+
pruned,
|
|
479
|
+
demoted,
|
|
480
|
+
anchor_demoted: anchorDemoted,
|
|
481
|
+
supersession_demoted: supersessionDemoted,
|
|
482
|
+
after: kept.length,
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
return null;
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
if (domainResult) {
|
|
489
|
+
if (!options.dryRun && !options.hard && archived.length > 0) {
|
|
490
|
+
// Default reason "stale" covers records pushed via the
|
|
491
|
+
// shelf-life path; bottom-out records pre-stamp their own
|
|
492
|
+
// reason and archiveRecords preserves it.
|
|
493
|
+
await archiveRecords(domain, archived, now, "stale");
|
|
494
|
+
}
|
|
495
|
+
results.push(domainResult);
|
|
496
|
+
totalPruned += domainResult.pruned;
|
|
497
|
+
totalDemoted += domainResult.demoted;
|
|
498
|
+
totalAnchorDemoted += domainResult.anchor_demoted;
|
|
499
|
+
totalSupersessionDemoted += domainResult.supersession_demoted;
|
|
500
|
+
actions.push(...domainActions);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
if (jsonMode) {
|
|
505
|
+
// `explanations` in JSON is the legacy shape: demotion-only,
|
|
506
|
+
// gated on --explain. Per-seed acceptance: JSON output is
|
|
507
|
+
// unchanged. Stale-archive entries live in `results` /
|
|
508
|
+
// `totalPruned`; they're never in `explanations`.
|
|
509
|
+
const explanations = options.explain
|
|
510
|
+
? actions.filter((a) => a.reasons.some((r) => r !== "stale"))
|
|
511
|
+
: undefined;
|
|
512
|
+
outputJson({
|
|
513
|
+
success: true,
|
|
514
|
+
command: "prune",
|
|
515
|
+
dryRun: options.dryRun,
|
|
516
|
+
hard: options.hard,
|
|
517
|
+
aggressive: options.aggressive,
|
|
518
|
+
checkAnchors: options.checkAnchors,
|
|
519
|
+
totalPruned,
|
|
520
|
+
totalDemoted,
|
|
521
|
+
totalAnchorDemoted,
|
|
522
|
+
totalSupersessionDemoted,
|
|
523
|
+
results,
|
|
524
|
+
...(explanations ? { explanations } : {}),
|
|
525
|
+
});
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
if (totalPruned === 0 && totalDemoted === 0) {
|
|
530
|
+
if (!isQuiet())
|
|
531
|
+
console.log(brand("No stale or superseded records found. All records are current."));
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
const action = options.hard ? "Deleted" : "Archived";
|
|
536
|
+
const wouldAction = options.hard ? "Would delete" : "Would archive";
|
|
537
|
+
const label = options.dryRun ? wouldAction : action;
|
|
538
|
+
const demoteLabel = options.dryRun ? "Would demote" : "Demoted";
|
|
539
|
+
const prefix = options.dryRun ? chalk.yellow("[DRY RUN] ") : "";
|
|
540
|
+
|
|
541
|
+
const quiet = isQuiet();
|
|
542
|
+
const actionsByDomain = new Map<string, RecordAction[]>();
|
|
543
|
+
for (const a of actions) {
|
|
544
|
+
const list = actionsByDomain.get(a.domain) ?? [];
|
|
545
|
+
list.push(a);
|
|
546
|
+
actionsByDomain.set(a.domain, list);
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
for (const result of results) {
|
|
550
|
+
let body: string;
|
|
551
|
+
if (result.pruned > 0 && result.demoted > 0) {
|
|
552
|
+
body = `${label} ${chalk.red(String(result.pruned))}, demoted ${chalk.yellow(String(result.demoted))}`;
|
|
553
|
+
} else if (result.pruned > 0) {
|
|
554
|
+
body = `${label} ${chalk.red(String(result.pruned))}`;
|
|
555
|
+
} else {
|
|
556
|
+
body = `${demoteLabel} ${chalk.yellow(String(result.demoted))}`;
|
|
557
|
+
}
|
|
558
|
+
if (!quiet) {
|
|
559
|
+
console.log(
|
|
560
|
+
`${prefix}${chalk.cyan(result.domain)}: ${body} of ${result.before} records (${result.after} remaining)`,
|
|
561
|
+
);
|
|
562
|
+
const domainActions = actionsByDomain.get(result.domain) ?? [];
|
|
563
|
+
for (const a of domainActions) {
|
|
564
|
+
const idPart = a.id ? chalk.dim(a.id) : chalk.dim("(no id)");
|
|
565
|
+
const reasonPart = a.reasons.join(" + ");
|
|
566
|
+
console.log(` ${idPart} [${a.type}]: ${a.from} → ${a.to} (${reasonPart})`);
|
|
567
|
+
if (options.explain && a.anchors) {
|
|
568
|
+
const frac = (a.anchors.valid_fraction * 100).toFixed(0);
|
|
569
|
+
console.log(
|
|
570
|
+
` anchors: ${a.anchors.valid}/${a.anchors.total} valid (${frac}%)`,
|
|
571
|
+
);
|
|
572
|
+
for (const b of a.anchors.broken) {
|
|
573
|
+
console.log(` ${chalk.red("✗")} ${b.kind}: ${b.path}`);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
const totals: string[] = [];
|
|
581
|
+
if (totalPruned > 0) {
|
|
582
|
+
totals.push(
|
|
583
|
+
`${label.toLowerCase()} ${totalPruned} stale ${totalPruned === 1 ? "record" : "records"}`,
|
|
584
|
+
);
|
|
585
|
+
}
|
|
586
|
+
if (totalDemoted > 0) {
|
|
587
|
+
const noun = totalDemoted === 1 ? "record" : "records";
|
|
588
|
+
const breakdown: string[] = [];
|
|
589
|
+
if (totalSupersessionDemoted > 0)
|
|
590
|
+
breakdown.push(`${totalSupersessionDemoted} superseded`);
|
|
591
|
+
if (totalAnchorDemoted > 0) breakdown.push(`${totalAnchorDemoted} anchor-decayed`);
|
|
592
|
+
const suffix = breakdown.length > 1 ? ` (${breakdown.join(", ")})` : "";
|
|
593
|
+
const tag =
|
|
594
|
+
breakdown.length === 1 && totalSupersessionDemoted > 0
|
|
595
|
+
? "superseded "
|
|
596
|
+
: breakdown.length === 1 && totalAnchorDemoted > 0
|
|
597
|
+
? "anchor-decayed "
|
|
598
|
+
: "";
|
|
599
|
+
totals.push(`${demoteLabel.toLowerCase()} ${totalDemoted} ${tag}${noun}${suffix}`);
|
|
600
|
+
}
|
|
601
|
+
// Totals print even under --quiet; the per-record list above is
|
|
602
|
+
// what --quiet suppresses (seed loam-5ce3).
|
|
603
|
+
const separator = quiet ? "" : "\n";
|
|
604
|
+
console.log(`${separator}${prefix}${chalk.bold(`Total: ${totals.join("; ")}.`)}`);
|
|
605
|
+
if (!quiet && !options.hard && !options.dryRun && totalPruned > 0) {
|
|
606
|
+
console.log(
|
|
607
|
+
chalk.dim(
|
|
608
|
+
"Records moved to .loam/archive/. Restore with `lm restore <id>` or use `--hard` next time to permanently delete.",
|
|
609
|
+
),
|
|
610
|
+
);
|
|
611
|
+
}
|
|
612
|
+
},
|
|
613
|
+
);
|
|
614
|
+
}
|