@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,1113 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { writeFile as fsWriteFile, readdir, readFile } from "node:fs/promises";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
import chalk from "chalk";
|
|
5
|
+
import type { Command } from "commander";
|
|
6
|
+
import { getRegistry, type TypeDefinition } from "../registry/type-registry.ts";
|
|
7
|
+
import { type LoamConfig, validateAnchorValidityConfig } from "../schemas/config.ts";
|
|
8
|
+
import type { ExpertiseRecord } from "../schemas/record.ts";
|
|
9
|
+
import {
|
|
10
|
+
getExpertiseDir,
|
|
11
|
+
getExpertisePath,
|
|
12
|
+
getLoamDir,
|
|
13
|
+
readConfig,
|
|
14
|
+
writeConfig,
|
|
15
|
+
} from "../utils/config.ts";
|
|
16
|
+
import {
|
|
17
|
+
findIncompatibleRequiredFields,
|
|
18
|
+
findMissingDomainFields,
|
|
19
|
+
getAllowedTypes,
|
|
20
|
+
getRequiredFields,
|
|
21
|
+
} from "../utils/domain-rules.ts";
|
|
22
|
+
import {
|
|
23
|
+
applyAliases,
|
|
24
|
+
createExpertiseFile,
|
|
25
|
+
findDuplicate,
|
|
26
|
+
readExpertiseFile,
|
|
27
|
+
writeExpertiseFile,
|
|
28
|
+
} from "../utils/expertise.ts";
|
|
29
|
+
import { outputJson } from "../utils/json-output.ts";
|
|
30
|
+
import { withFileLock } from "../utils/lock.ts";
|
|
31
|
+
import { brand, icons, isQuiet } from "../utils/palette.ts";
|
|
32
|
+
import { compareSemver, getCurrentVersion, getLatestVersion } from "../utils/version.ts";
|
|
33
|
+
import { isStale } from "./prune.ts";
|
|
34
|
+
|
|
35
|
+
interface DoctorCheck {
|
|
36
|
+
name: string;
|
|
37
|
+
status: "pass" | "warn" | "fail";
|
|
38
|
+
message: string;
|
|
39
|
+
fixable: boolean;
|
|
40
|
+
details: string[];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function checkConfig(cwd?: string): Promise<DoctorCheck> {
|
|
44
|
+
try {
|
|
45
|
+
const loamDir = getLoamDir(cwd);
|
|
46
|
+
if (!existsSync(loamDir)) {
|
|
47
|
+
return {
|
|
48
|
+
name: "config",
|
|
49
|
+
status: "fail",
|
|
50
|
+
message: "No .loam/ directory found",
|
|
51
|
+
fixable: false,
|
|
52
|
+
details: [],
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
await readConfig(cwd);
|
|
56
|
+
return {
|
|
57
|
+
name: "config",
|
|
58
|
+
status: "pass",
|
|
59
|
+
message: "Config is valid",
|
|
60
|
+
fixable: false,
|
|
61
|
+
details: [],
|
|
62
|
+
};
|
|
63
|
+
} catch (err) {
|
|
64
|
+
return {
|
|
65
|
+
name: "config",
|
|
66
|
+
status: "fail",
|
|
67
|
+
message: `Config error: ${(err as Error).message}`,
|
|
68
|
+
fixable: false,
|
|
69
|
+
details: [],
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function checkJsonlIntegrity(config: LoamConfig, cwd?: string): Promise<DoctorCheck> {
|
|
75
|
+
const details: string[] = [];
|
|
76
|
+
for (const domain of Object.keys(config.domains)) {
|
|
77
|
+
const filePath = getExpertisePath(domain, cwd);
|
|
78
|
+
let content: string;
|
|
79
|
+
try {
|
|
80
|
+
content = await readFile(filePath, "utf-8");
|
|
81
|
+
} catch {
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
const lines = content.split("\n");
|
|
85
|
+
for (let i = 0; i < lines.length; i++) {
|
|
86
|
+
const line = (lines[i] ?? "").trim();
|
|
87
|
+
if (line.length === 0) continue;
|
|
88
|
+
try {
|
|
89
|
+
JSON.parse(line);
|
|
90
|
+
} catch {
|
|
91
|
+
details.push(`${domain}:${i + 1} - Invalid JSON`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
if (details.length > 0) {
|
|
96
|
+
return {
|
|
97
|
+
name: "jsonl-integrity",
|
|
98
|
+
status: "fail",
|
|
99
|
+
message: `${details.length} invalid JSON line(s) found`,
|
|
100
|
+
fixable: true,
|
|
101
|
+
details,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
return {
|
|
105
|
+
name: "jsonl-integrity",
|
|
106
|
+
status: "pass",
|
|
107
|
+
message: "All JSONL lines are valid JSON",
|
|
108
|
+
fixable: true,
|
|
109
|
+
details: [],
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function checkSchemaValidation(config: LoamConfig, cwd?: string): Promise<DoctorCheck> {
|
|
114
|
+
const registry = getRegistry();
|
|
115
|
+
const validate = registry.validator;
|
|
116
|
+
const details: string[] = [];
|
|
117
|
+
|
|
118
|
+
for (const domain of Object.keys(config.domains)) {
|
|
119
|
+
const filePath = getExpertisePath(domain, cwd);
|
|
120
|
+
let content: string;
|
|
121
|
+
try {
|
|
122
|
+
content = await readFile(filePath, "utf-8");
|
|
123
|
+
} catch {
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
const lines = content.split("\n");
|
|
127
|
+
for (let i = 0; i < lines.length; i++) {
|
|
128
|
+
const line = (lines[i] ?? "").trim();
|
|
129
|
+
if (line.length === 0) continue;
|
|
130
|
+
let parsed: unknown;
|
|
131
|
+
try {
|
|
132
|
+
parsed = JSON.parse(line);
|
|
133
|
+
} catch {
|
|
134
|
+
continue; // Already caught by integrity check
|
|
135
|
+
}
|
|
136
|
+
// Skip records of unregistered types — checkUnknownTypes flags those
|
|
137
|
+
// with a clearer message. Otherwise Ajv reports a noisy "no oneOf
|
|
138
|
+
// matched" for the same record. Apply aliases before Ajv so a
|
|
139
|
+
// record with a legacy field name (post-rename) validates cleanly.
|
|
140
|
+
if (parsed && typeof parsed === "object" && "type" in parsed) {
|
|
141
|
+
const t = (parsed as { type: unknown }).type;
|
|
142
|
+
if (typeof t === "string") {
|
|
143
|
+
const def = registry.get(t);
|
|
144
|
+
if (!def) continue;
|
|
145
|
+
if (def.aliases) applyAliases(parsed as Record<string, unknown>, def.aliases);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
if (!validate(parsed)) {
|
|
149
|
+
const errors = (validate.errors ?? [])
|
|
150
|
+
.map((e) => `${e.instancePath} ${e.message}`)
|
|
151
|
+
.join("; ");
|
|
152
|
+
details.push(`${domain}:${i + 1} - ${errors}`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
if (details.length > 0) {
|
|
157
|
+
return {
|
|
158
|
+
name: "schema-validation",
|
|
159
|
+
status: "fail",
|
|
160
|
+
message: `${details.length} record(s) failed schema validation`,
|
|
161
|
+
fixable: true,
|
|
162
|
+
details,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
return {
|
|
166
|
+
name: "schema-validation",
|
|
167
|
+
status: "pass",
|
|
168
|
+
message: "All records pass schema validation",
|
|
169
|
+
fixable: true,
|
|
170
|
+
details: [],
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async function checkStaleRecords(config: LoamConfig, cwd?: string): Promise<DoctorCheck> {
|
|
175
|
+
const now = new Date();
|
|
176
|
+
const shelfLife = config.classification_defaults.shelf_life;
|
|
177
|
+
const details: string[] = [];
|
|
178
|
+
let staleCount = 0;
|
|
179
|
+
|
|
180
|
+
for (const domain of Object.keys(config.domains)) {
|
|
181
|
+
const filePath = getExpertisePath(domain, cwd);
|
|
182
|
+
const records = await readExpertiseFile(filePath, { allowUnknownTypes: true });
|
|
183
|
+
for (const record of records) {
|
|
184
|
+
if (isStale(record, now, shelfLife)) {
|
|
185
|
+
staleCount++;
|
|
186
|
+
details.push(`${domain}: stale ${record.type} (${record.classification})`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
if (staleCount > 0) {
|
|
191
|
+
return {
|
|
192
|
+
name: "stale-records",
|
|
193
|
+
status: "warn",
|
|
194
|
+
message: `${staleCount} stale record(s) found`,
|
|
195
|
+
fixable: true,
|
|
196
|
+
details,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
return {
|
|
200
|
+
name: "stale-records",
|
|
201
|
+
status: "pass",
|
|
202
|
+
message: "No stale records",
|
|
203
|
+
fixable: true,
|
|
204
|
+
details: [],
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
async function checkTypeRegistry(config: LoamConfig, cwd?: string): Promise<DoctorCheck> {
|
|
209
|
+
const registry = getRegistry();
|
|
210
|
+
const builtinDefs = registry.builtinDefs();
|
|
211
|
+
const customDefs = registry.customDefs();
|
|
212
|
+
const disabledNames = new Set(registry.disabledNames());
|
|
213
|
+
|
|
214
|
+
// Bucket records by type across all domains.
|
|
215
|
+
const counts: Record<string, number> = {};
|
|
216
|
+
for (const def of registry.enabled()) counts[def.name] = 0;
|
|
217
|
+
for (const domain of Object.keys(config.domains)) {
|
|
218
|
+
const filePath = getExpertisePath(domain, cwd);
|
|
219
|
+
// allowUnknownTypes here so this informational check never throws on
|
|
220
|
+
// pre-existing unknown types (checkUnknownTypes flags them separately).
|
|
221
|
+
const records = await readExpertiseFile(filePath, { allowUnknownTypes: true });
|
|
222
|
+
for (const r of records) {
|
|
223
|
+
if (counts[r.type] !== undefined) counts[r.type] = (counts[r.type] ?? 0) + 1;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const fmt = (def: TypeDefinition): string => {
|
|
228
|
+
const labels: string[] = [def.kind === "builtin" ? "built-in" : "custom"];
|
|
229
|
+
if (disabledNames.has(def.name)) labels.push("disabled");
|
|
230
|
+
const count = counts[def.name] ?? 0;
|
|
231
|
+
return `${def.name} (${labels.join(", ")}): ${count} record${count === 1 ? "" : "s"}`;
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
const details = [...builtinDefs.map(fmt), ...customDefs.map(fmt)];
|
|
235
|
+
const total = builtinDefs.length + customDefs.length;
|
|
236
|
+
const disabledCount = disabledNames.size;
|
|
237
|
+
const message =
|
|
238
|
+
`${total} type(s) registered: ${builtinDefs.length} built-in, ${customDefs.length} custom` +
|
|
239
|
+
(disabledCount > 0 ? `, ${disabledCount} disabled` : "");
|
|
240
|
+
|
|
241
|
+
return {
|
|
242
|
+
name: "type-registry",
|
|
243
|
+
status: "pass",
|
|
244
|
+
message,
|
|
245
|
+
fixable: false,
|
|
246
|
+
details,
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
async function checkUnknownTypes(config: LoamConfig, cwd?: string): Promise<DoctorCheck> {
|
|
251
|
+
const registry = getRegistry();
|
|
252
|
+
const details: string[] = [];
|
|
253
|
+
|
|
254
|
+
for (const domain of Object.keys(config.domains)) {
|
|
255
|
+
const filePath = getExpertisePath(domain, cwd);
|
|
256
|
+
let content: string;
|
|
257
|
+
try {
|
|
258
|
+
content = await readFile(filePath, "utf-8");
|
|
259
|
+
} catch {
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
const lines = content.split("\n");
|
|
263
|
+
for (let i = 0; i < lines.length; i++) {
|
|
264
|
+
const line = (lines[i] ?? "").trim();
|
|
265
|
+
if (line.length === 0) continue;
|
|
266
|
+
let parsed: unknown;
|
|
267
|
+
try {
|
|
268
|
+
parsed = JSON.parse(line);
|
|
269
|
+
} catch {
|
|
270
|
+
continue; // covered by jsonl-integrity check
|
|
271
|
+
}
|
|
272
|
+
if (parsed && typeof parsed === "object" && "type" in parsed) {
|
|
273
|
+
const t = (parsed as { type: unknown }).type;
|
|
274
|
+
if (typeof t === "string" && !registry.get(t)) {
|
|
275
|
+
const id = (parsed as { id?: unknown }).id;
|
|
276
|
+
const idPart = typeof id === "string" ? ` [${id}]` : "";
|
|
277
|
+
details.push(`${domain}:${i + 1}${idPart} - unknown type "${t}"`);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (details.length > 0) {
|
|
284
|
+
return {
|
|
285
|
+
name: "unknown-types",
|
|
286
|
+
status: "fail",
|
|
287
|
+
message: `${details.length} record(s) with unregistered types`,
|
|
288
|
+
fixable: false,
|
|
289
|
+
details,
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
return {
|
|
293
|
+
name: "unknown-types",
|
|
294
|
+
status: "pass",
|
|
295
|
+
message: "All record types are registered",
|
|
296
|
+
fixable: false,
|
|
297
|
+
details: [],
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
async function checkOrphanedDomains(config: LoamConfig, cwd?: string): Promise<DoctorCheck> {
|
|
302
|
+
const expertiseDir = getExpertiseDir(cwd);
|
|
303
|
+
const details: string[] = [];
|
|
304
|
+
|
|
305
|
+
// Check for JSONL files not in config
|
|
306
|
+
try {
|
|
307
|
+
const files = await readdir(expertiseDir);
|
|
308
|
+
for (const file of files) {
|
|
309
|
+
if (file.endsWith(".jsonl")) {
|
|
310
|
+
const domain = file.replace(".jsonl", "");
|
|
311
|
+
if (!(domain in config.domains)) {
|
|
312
|
+
details.push(`File "${file}" exists but domain "${domain}" is not in config`);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
} catch {
|
|
317
|
+
// expertise dir doesn't exist
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Check for config domains without JSONL files
|
|
321
|
+
for (const domain of Object.keys(config.domains)) {
|
|
322
|
+
const filePath = getExpertisePath(domain, cwd);
|
|
323
|
+
if (!existsSync(filePath)) {
|
|
324
|
+
details.push(`Domain "${domain}" in config but no JSONL file exists`);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
if (details.length > 0) {
|
|
329
|
+
return {
|
|
330
|
+
name: "orphaned-domains",
|
|
331
|
+
status: "warn",
|
|
332
|
+
message: `${details.length} orphaned domain issue(s)`,
|
|
333
|
+
fixable: true,
|
|
334
|
+
details,
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
return {
|
|
338
|
+
name: "orphaned-domains",
|
|
339
|
+
status: "pass",
|
|
340
|
+
message: "No orphaned domains",
|
|
341
|
+
fixable: true,
|
|
342
|
+
details: [],
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
async function checkDuplicates(config: LoamConfig, cwd?: string): Promise<DoctorCheck> {
|
|
347
|
+
const details: string[] = [];
|
|
348
|
+
let dupCount = 0;
|
|
349
|
+
|
|
350
|
+
for (const domain of Object.keys(config.domains)) {
|
|
351
|
+
const filePath = getExpertisePath(domain, cwd);
|
|
352
|
+
const records = await readExpertiseFile(filePath, { allowUnknownTypes: true });
|
|
353
|
+
for (let i = 1; i < records.length; i++) {
|
|
354
|
+
const rec = records[i];
|
|
355
|
+
if (!rec) continue;
|
|
356
|
+
const dup = findDuplicate(records.slice(0, i), rec);
|
|
357
|
+
if (dup) {
|
|
358
|
+
dupCount++;
|
|
359
|
+
details.push(
|
|
360
|
+
`${domain}: duplicate ${rec.type} at index ${i + 1} (matches #${dup.index + 1})`,
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
if (dupCount > 0) {
|
|
366
|
+
return {
|
|
367
|
+
name: "duplicates",
|
|
368
|
+
status: "warn",
|
|
369
|
+
message: `${dupCount} duplicate record(s) found`,
|
|
370
|
+
fixable: false,
|
|
371
|
+
details,
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
return {
|
|
375
|
+
name: "duplicates",
|
|
376
|
+
status: "pass",
|
|
377
|
+
message: "No duplicates",
|
|
378
|
+
fixable: false,
|
|
379
|
+
details: [],
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
async function checkLegacyOutcome(config: LoamConfig, cwd?: string): Promise<DoctorCheck> {
|
|
384
|
+
const details: string[] = [];
|
|
385
|
+
|
|
386
|
+
for (const domain of Object.keys(config.domains)) {
|
|
387
|
+
const filePath = getExpertisePath(domain, cwd);
|
|
388
|
+
let content: string;
|
|
389
|
+
try {
|
|
390
|
+
content = await readFile(filePath, "utf-8");
|
|
391
|
+
} catch {
|
|
392
|
+
continue;
|
|
393
|
+
}
|
|
394
|
+
const lines = content.split("\n");
|
|
395
|
+
for (let i = 0; i < lines.length; i++) {
|
|
396
|
+
const line = (lines[i] ?? "").trim();
|
|
397
|
+
if (line.length === 0) continue;
|
|
398
|
+
let parsed: unknown;
|
|
399
|
+
try {
|
|
400
|
+
parsed = JSON.parse(line);
|
|
401
|
+
} catch {
|
|
402
|
+
continue;
|
|
403
|
+
}
|
|
404
|
+
if (
|
|
405
|
+
parsed !== null &&
|
|
406
|
+
typeof parsed === "object" &&
|
|
407
|
+
"outcome" in parsed &&
|
|
408
|
+
!("outcomes" in parsed)
|
|
409
|
+
) {
|
|
410
|
+
details.push(
|
|
411
|
+
`${domain}:${i + 1} - legacy "outcome" field (singular); should be "outcomes[]"`,
|
|
412
|
+
);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
if (details.length > 0) {
|
|
418
|
+
return {
|
|
419
|
+
name: "legacy-outcome",
|
|
420
|
+
status: "warn",
|
|
421
|
+
message: `${details.length} record(s) with legacy "outcome" field on disk`,
|
|
422
|
+
fixable: true,
|
|
423
|
+
details,
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
return {
|
|
427
|
+
name: "legacy-outcome",
|
|
428
|
+
status: "pass",
|
|
429
|
+
message: 'No legacy "outcome" fields on disk',
|
|
430
|
+
fixable: true,
|
|
431
|
+
details: [],
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
async function checkFileAnchors(config: LoamConfig, cwd?: string): Promise<DoctorCheck> {
|
|
436
|
+
const projectRoot = cwd ?? process.cwd();
|
|
437
|
+
const details: string[] = [];
|
|
438
|
+
|
|
439
|
+
for (const domain of Object.keys(config.domains)) {
|
|
440
|
+
const filePath = getExpertisePath(domain, cwd);
|
|
441
|
+
const records = await readExpertiseFile(filePath, { allowUnknownTypes: true });
|
|
442
|
+
for (const record of records) {
|
|
443
|
+
const id = record.id ? `[${record.id}]` : "";
|
|
444
|
+
if ("files" in record && Array.isArray(record.files)) {
|
|
445
|
+
for (const f of record.files) {
|
|
446
|
+
if (!existsSync(resolve(projectRoot, f))) {
|
|
447
|
+
details.push(`${domain}${id}: files[] path not found: ${f}`);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
if (Array.isArray(record.dir_anchors)) {
|
|
452
|
+
for (const d of record.dir_anchors) {
|
|
453
|
+
if (!existsSync(resolve(projectRoot, d))) {
|
|
454
|
+
details.push(`${domain}${id}: dir_anchors[] path not found: ${d}`);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
if (record.evidence?.file && !existsSync(resolve(projectRoot, record.evidence.file))) {
|
|
459
|
+
details.push(`${domain}${id}: evidence.file not found: ${record.evidence.file}`);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
if (details.length > 0) {
|
|
465
|
+
return {
|
|
466
|
+
name: "file-anchors",
|
|
467
|
+
status: "warn",
|
|
468
|
+
message: `${details.length} broken file anchor(s) found`,
|
|
469
|
+
fixable: true,
|
|
470
|
+
details,
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
return {
|
|
474
|
+
name: "file-anchors",
|
|
475
|
+
status: "pass",
|
|
476
|
+
message: "All file anchors resolve to existing paths",
|
|
477
|
+
fixable: true,
|
|
478
|
+
details: [],
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
async function checkGovernance(config: LoamConfig, cwd?: string): Promise<DoctorCheck> {
|
|
483
|
+
const details: string[] = [];
|
|
484
|
+
let worstStatus: "pass" | "warn" | "fail" = "pass";
|
|
485
|
+
|
|
486
|
+
for (const domain of Object.keys(config.domains)) {
|
|
487
|
+
const filePath = getExpertisePath(domain, cwd);
|
|
488
|
+
const records = await readExpertiseFile(filePath, { allowUnknownTypes: true });
|
|
489
|
+
const count = records.length;
|
|
490
|
+
|
|
491
|
+
if (count >= config.governance.hard_limit) {
|
|
492
|
+
details.push(
|
|
493
|
+
`${domain}: ${count} records (over hard limit of ${config.governance.hard_limit})`,
|
|
494
|
+
);
|
|
495
|
+
worstStatus = "fail";
|
|
496
|
+
} else if (count >= config.governance.warn_entries) {
|
|
497
|
+
details.push(
|
|
498
|
+
`${domain}: ${count} records (over warn threshold of ${config.governance.warn_entries})`,
|
|
499
|
+
);
|
|
500
|
+
if (worstStatus !== "fail") worstStatus = "warn";
|
|
501
|
+
} else if (count >= config.governance.max_entries) {
|
|
502
|
+
details.push(
|
|
503
|
+
`${domain}: ${count} records (approaching limit of ${config.governance.max_entries})`,
|
|
504
|
+
);
|
|
505
|
+
if (worstStatus !== "fail") worstStatus = "warn";
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
if (details.length > 0) {
|
|
510
|
+
return {
|
|
511
|
+
name: "governance",
|
|
512
|
+
status: worstStatus,
|
|
513
|
+
message: `${details.length} domain(s) over governance thresholds`,
|
|
514
|
+
fixable: false,
|
|
515
|
+
details,
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
return {
|
|
519
|
+
name: "governance",
|
|
520
|
+
status: "pass",
|
|
521
|
+
message: "All domains within governance limits",
|
|
522
|
+
fixable: false,
|
|
523
|
+
details: [],
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// Informational: report whether `lm compact` runs the mechanical merge (no
|
|
528
|
+
// `pre-compact` hook registered) vs. a semantic summarizer (one or more
|
|
529
|
+
// scripts configured). Always passes; the message tells the user which mode
|
|
530
|
+
// they're in.
|
|
531
|
+
function checkCompactSummarizer(config: LoamConfig): DoctorCheck {
|
|
532
|
+
const scripts = (config.hooks?.["pre-compact"] ?? []).filter(
|
|
533
|
+
(s) => typeof s === "string" && s.trim().length > 0,
|
|
534
|
+
);
|
|
535
|
+
if (scripts.length === 0) {
|
|
536
|
+
return {
|
|
537
|
+
name: "compact-summarizer",
|
|
538
|
+
status: "pass",
|
|
539
|
+
message: "compact-summarizer: not configured (mechanical merge in use)",
|
|
540
|
+
fixable: false,
|
|
541
|
+
details: [],
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
return {
|
|
545
|
+
name: "compact-summarizer",
|
|
546
|
+
status: "pass",
|
|
547
|
+
message: `compact-summarizer: ${scripts.length} pre-compact hook${scripts.length === 1 ? "" : "s"} registered`,
|
|
548
|
+
fixable: false,
|
|
549
|
+
details: scripts,
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
function checkDecayConfig(config: LoamConfig): DoctorCheck {
|
|
554
|
+
const cfg = config.decay?.anchor_validity;
|
|
555
|
+
if (!cfg) {
|
|
556
|
+
return {
|
|
557
|
+
name: "decay-config",
|
|
558
|
+
status: "pass",
|
|
559
|
+
message: "decay.anchor_validity uses defaults",
|
|
560
|
+
fixable: false,
|
|
561
|
+
details: [],
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
const errors = validateAnchorValidityConfig(cfg);
|
|
565
|
+
if (errors.length > 0) {
|
|
566
|
+
return {
|
|
567
|
+
name: "decay-config",
|
|
568
|
+
status: "fail",
|
|
569
|
+
message: `decay.anchor_validity is invalid (${errors.length} issue${errors.length === 1 ? "" : "s"})`,
|
|
570
|
+
fixable: false,
|
|
571
|
+
details: errors,
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
return {
|
|
575
|
+
name: "decay-config",
|
|
576
|
+
status: "pass",
|
|
577
|
+
message: "decay.anchor_validity is valid",
|
|
578
|
+
fixable: false,
|
|
579
|
+
details: [],
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// Walk every domain's records and bucket them as conforming or violating
|
|
584
|
+
// domain-level rules (allowed_types / required_fields). Used by both the
|
|
585
|
+
// informational and failing checks so we read+parse JSONL once.
|
|
586
|
+
async function collectDomainConformance(
|
|
587
|
+
config: LoamConfig,
|
|
588
|
+
cwd?: string,
|
|
589
|
+
): Promise<{
|
|
590
|
+
perDomain: Array<{
|
|
591
|
+
domain: string;
|
|
592
|
+
total: number;
|
|
593
|
+
conforming: number;
|
|
594
|
+
violations: number;
|
|
595
|
+
hasRules: boolean;
|
|
596
|
+
}>;
|
|
597
|
+
violations: string[];
|
|
598
|
+
}> {
|
|
599
|
+
const perDomain: Array<{
|
|
600
|
+
domain: string;
|
|
601
|
+
total: number;
|
|
602
|
+
conforming: number;
|
|
603
|
+
violations: number;
|
|
604
|
+
hasRules: boolean;
|
|
605
|
+
}> = [];
|
|
606
|
+
const violations: string[] = [];
|
|
607
|
+
|
|
608
|
+
for (const domain of Object.keys(config.domains)) {
|
|
609
|
+
const allowedTypes = getAllowedTypes(config, domain);
|
|
610
|
+
const requiredFields = getRequiredFields(config, domain);
|
|
611
|
+
const hasRules = allowedTypes !== null || requiredFields !== null;
|
|
612
|
+
|
|
613
|
+
const filePath = getExpertisePath(domain, cwd);
|
|
614
|
+
let content: string;
|
|
615
|
+
try {
|
|
616
|
+
content = await readFile(filePath, "utf-8");
|
|
617
|
+
} catch {
|
|
618
|
+
perDomain.push({ domain, total: 0, conforming: 0, violations: 0, hasRules });
|
|
619
|
+
continue;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
const lines = content.split("\n");
|
|
623
|
+
let total = 0;
|
|
624
|
+
let badCount = 0;
|
|
625
|
+
|
|
626
|
+
for (let i = 0; i < lines.length; i++) {
|
|
627
|
+
const line = (lines[i] ?? "").trim();
|
|
628
|
+
if (line.length === 0) continue;
|
|
629
|
+
let parsed: unknown;
|
|
630
|
+
try {
|
|
631
|
+
parsed = JSON.parse(line);
|
|
632
|
+
} catch {
|
|
633
|
+
continue; // jsonl-integrity reports parse errors
|
|
634
|
+
}
|
|
635
|
+
if (!parsed || typeof parsed !== "object") continue;
|
|
636
|
+
total++;
|
|
637
|
+
|
|
638
|
+
const record = parsed as Record<string, unknown>;
|
|
639
|
+
const recordType = typeof record.type === "string" ? record.type : "<unknown>";
|
|
640
|
+
const id = typeof record.id === "string" ? record.id : "<no-id>";
|
|
641
|
+
const lineNumber = i + 1;
|
|
642
|
+
let recordViolated = false;
|
|
643
|
+
|
|
644
|
+
if (allowedTypes && !allowedTypes.includes(recordType)) {
|
|
645
|
+
recordViolated = true;
|
|
646
|
+
violations.push(
|
|
647
|
+
`${domain}:${lineNumber} [${id}] type "${recordType}" not in allowed_types [${allowedTypes.join(", ")}]`,
|
|
648
|
+
);
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
if (requiredFields) {
|
|
652
|
+
const missing = findMissingDomainFields(record, requiredFields);
|
|
653
|
+
if (missing.length > 0) {
|
|
654
|
+
recordViolated = true;
|
|
655
|
+
violations.push(
|
|
656
|
+
`${domain}:${lineNumber} [${id}] (${recordType}) missing required field(s): ${missing.join(", ")}`,
|
|
657
|
+
);
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
if (recordViolated) badCount++;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
perDomain.push({
|
|
665
|
+
domain,
|
|
666
|
+
total,
|
|
667
|
+
conforming: total - badCount,
|
|
668
|
+
violations: badCount,
|
|
669
|
+
hasRules,
|
|
670
|
+
});
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
return { perDomain, violations };
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
// Static compatibility check: a domain's required_fields can name a field that
|
|
677
|
+
// no allowed type accepts. Built-in/custom schemas are closed
|
|
678
|
+
// (additionalProperties: false), so writes silently fail with a confusing AJV
|
|
679
|
+
// error. Surfacing this at doctor time tells the user to either declare a
|
|
680
|
+
// custom_type that holds the field or drop it from required_fields.
|
|
681
|
+
async function checkDomainRulesCompatibility(config: LoamConfig): Promise<DoctorCheck> {
|
|
682
|
+
const registry = getRegistry();
|
|
683
|
+
const details: string[] = [];
|
|
684
|
+
let count = 0;
|
|
685
|
+
|
|
686
|
+
for (const domain of Object.keys(config.domains)) {
|
|
687
|
+
const incompatible = findIncompatibleRequiredFields(config, domain, registry);
|
|
688
|
+
for (const { field, allowedTypes } of incompatible) {
|
|
689
|
+
count++;
|
|
690
|
+
details.push(
|
|
691
|
+
`domain "${domain}" requires field "${field}", but no allowed type accepts it (allowed: ${allowedTypes.join(", ")}). Declare a custom_type with "${field}" in required/optional, or remove "${field}" from required_fields.`,
|
|
692
|
+
);
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
if (count > 0) {
|
|
697
|
+
return {
|
|
698
|
+
name: "domain-rules-compatibility",
|
|
699
|
+
status: "fail",
|
|
700
|
+
message: `${count} domain required_field(s) incompatible with allowed types`,
|
|
701
|
+
fixable: false,
|
|
702
|
+
details,
|
|
703
|
+
};
|
|
704
|
+
}
|
|
705
|
+
return {
|
|
706
|
+
name: "domain-rules-compatibility",
|
|
707
|
+
status: "pass",
|
|
708
|
+
message: "All domain required_fields are compatible with allowed types",
|
|
709
|
+
fixable: false,
|
|
710
|
+
details: [],
|
|
711
|
+
};
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
async function checkDomainConformance(
|
|
715
|
+
config: LoamConfig,
|
|
716
|
+
cwd?: string,
|
|
717
|
+
): Promise<{ info: DoctorCheck; violations: DoctorCheck }> {
|
|
718
|
+
const { perDomain, violations } = await collectDomainConformance(config, cwd);
|
|
719
|
+
|
|
720
|
+
const infoDetails = perDomain.map((d) => {
|
|
721
|
+
const rulesLabel = d.hasRules ? "rules" : "no rules";
|
|
722
|
+
return `${d.domain} (${rulesLabel}): ${d.conforming}/${d.total} conforming, ${d.violations} violation${d.violations === 1 ? "" : "s"}`;
|
|
723
|
+
});
|
|
724
|
+
const totalRecords = perDomain.reduce((s, d) => s + d.total, 0);
|
|
725
|
+
const totalViolations = perDomain.reduce((s, d) => s + d.violations, 0);
|
|
726
|
+
const info: DoctorCheck = {
|
|
727
|
+
name: "domain-conformance",
|
|
728
|
+
status: "pass",
|
|
729
|
+
message: `${totalRecords - totalViolations}/${totalRecords} record(s) conform to domain rules`,
|
|
730
|
+
fixable: false,
|
|
731
|
+
details: infoDetails,
|
|
732
|
+
};
|
|
733
|
+
|
|
734
|
+
const violationsCheck: DoctorCheck =
|
|
735
|
+
violations.length > 0
|
|
736
|
+
? {
|
|
737
|
+
name: "domain-violations",
|
|
738
|
+
status: "fail",
|
|
739
|
+
message: `${totalViolations} record(s) violate domain rules (allowed_types / required_fields)`,
|
|
740
|
+
fixable: false,
|
|
741
|
+
details: violations,
|
|
742
|
+
}
|
|
743
|
+
: {
|
|
744
|
+
name: "domain-violations",
|
|
745
|
+
status: "pass",
|
|
746
|
+
message: "All records conform to domain rules",
|
|
747
|
+
fixable: false,
|
|
748
|
+
details: [],
|
|
749
|
+
};
|
|
750
|
+
|
|
751
|
+
return { info, violations: violationsCheck };
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
async function checkUpdateAvailable(): Promise<DoctorCheck> {
|
|
755
|
+
const current = getCurrentVersion();
|
|
756
|
+
const latest = getLatestVersion();
|
|
757
|
+
|
|
758
|
+
if (latest === null) {
|
|
759
|
+
return {
|
|
760
|
+
name: "upgrade",
|
|
761
|
+
status: "pass",
|
|
762
|
+
message: `Version ${current} (unable to check registry)`,
|
|
763
|
+
fixable: false,
|
|
764
|
+
details: [],
|
|
765
|
+
};
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
const cmp = compareSemver(current, latest);
|
|
769
|
+
if (cmp >= 0) {
|
|
770
|
+
return {
|
|
771
|
+
name: "upgrade",
|
|
772
|
+
status: "pass",
|
|
773
|
+
message: `Version ${current} is up to date`,
|
|
774
|
+
fixable: false,
|
|
775
|
+
details: [],
|
|
776
|
+
};
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
return {
|
|
780
|
+
name: "upgrade",
|
|
781
|
+
status: "warn",
|
|
782
|
+
message: `Update available: ${current} → ${latest}`,
|
|
783
|
+
fixable: false,
|
|
784
|
+
details: ["Run `loam upgrade` to upgrade"],
|
|
785
|
+
};
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
async function applyFixes(
|
|
789
|
+
checks: DoctorCheck[],
|
|
790
|
+
config: LoamConfig,
|
|
791
|
+
cwd?: string,
|
|
792
|
+
): Promise<string[]> {
|
|
793
|
+
const fixed: string[] = [];
|
|
794
|
+
|
|
795
|
+
for (const check of checks) {
|
|
796
|
+
if (check.status === "pass" || !check.fixable) continue;
|
|
797
|
+
|
|
798
|
+
switch (check.name) {
|
|
799
|
+
case "jsonl-integrity": {
|
|
800
|
+
// Remove invalid JSON lines
|
|
801
|
+
for (const domain of Object.keys(config.domains)) {
|
|
802
|
+
const filePath = getExpertisePath(domain, cwd);
|
|
803
|
+
await withFileLock(filePath, async () => {
|
|
804
|
+
let content: string;
|
|
805
|
+
try {
|
|
806
|
+
content = await readFile(filePath, "utf-8");
|
|
807
|
+
} catch {
|
|
808
|
+
return;
|
|
809
|
+
}
|
|
810
|
+
const lines = content.split("\n");
|
|
811
|
+
const valid: string[] = [];
|
|
812
|
+
let removed = 0;
|
|
813
|
+
for (const line of lines) {
|
|
814
|
+
const trimmed = line.trim();
|
|
815
|
+
if (trimmed.length === 0) continue;
|
|
816
|
+
try {
|
|
817
|
+
JSON.parse(trimmed);
|
|
818
|
+
valid.push(trimmed);
|
|
819
|
+
} catch {
|
|
820
|
+
removed++;
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
if (removed > 0) {
|
|
824
|
+
await fsWriteFile(
|
|
825
|
+
filePath,
|
|
826
|
+
valid.map((l) => l).join("\n") + (valid.length > 0 ? "\n" : ""),
|
|
827
|
+
"utf-8",
|
|
828
|
+
);
|
|
829
|
+
fixed.push(`Removed ${removed} invalid JSON line(s) from ${domain}`);
|
|
830
|
+
}
|
|
831
|
+
});
|
|
832
|
+
}
|
|
833
|
+
break;
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
case "schema-validation": {
|
|
837
|
+
const registry = getRegistry();
|
|
838
|
+
const validate = registry.validator;
|
|
839
|
+
for (const domain of Object.keys(config.domains)) {
|
|
840
|
+
const filePath = getExpertisePath(domain, cwd);
|
|
841
|
+
await withFileLock(filePath, async () => {
|
|
842
|
+
const records = await readExpertiseFile(filePath, { allowUnknownTypes: true });
|
|
843
|
+
// Keep unknown-type records — they're flagged separately by
|
|
844
|
+
// checkUnknownTypes and shouldn't be silently deleted.
|
|
845
|
+
const valid = records.filter((r) => !registry.get(r.type) || validate(r));
|
|
846
|
+
const removed = records.length - valid.length;
|
|
847
|
+
if (removed > 0) {
|
|
848
|
+
await writeExpertiseFile(filePath, valid);
|
|
849
|
+
fixed.push(`Removed ${removed} invalid record(s) from ${domain}`);
|
|
850
|
+
}
|
|
851
|
+
});
|
|
852
|
+
}
|
|
853
|
+
break;
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
case "stale-records": {
|
|
857
|
+
const now = new Date();
|
|
858
|
+
const shelfLife = config.classification_defaults.shelf_life;
|
|
859
|
+
for (const domain of Object.keys(config.domains)) {
|
|
860
|
+
const filePath = getExpertisePath(domain, cwd);
|
|
861
|
+
await withFileLock(filePath, async () => {
|
|
862
|
+
const records = await readExpertiseFile(filePath, { allowUnknownTypes: true });
|
|
863
|
+
const kept = records.filter((r) => !isStale(r, now, shelfLife));
|
|
864
|
+
const pruned = records.length - kept.length;
|
|
865
|
+
if (pruned > 0) {
|
|
866
|
+
await writeExpertiseFile(filePath, kept);
|
|
867
|
+
fixed.push(`Pruned ${pruned} stale record(s) from ${domain}`);
|
|
868
|
+
}
|
|
869
|
+
});
|
|
870
|
+
}
|
|
871
|
+
break;
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
case "legacy-outcome": {
|
|
875
|
+
for (const domain of Object.keys(config.domains)) {
|
|
876
|
+
const filePath = getExpertisePath(domain, cwd);
|
|
877
|
+
await withFileLock(filePath, async () => {
|
|
878
|
+
let content: string;
|
|
879
|
+
try {
|
|
880
|
+
content = await readFile(filePath, "utf-8");
|
|
881
|
+
} catch {
|
|
882
|
+
return;
|
|
883
|
+
}
|
|
884
|
+
const lines = content.split("\n");
|
|
885
|
+
const migrated: string[] = [];
|
|
886
|
+
let count = 0;
|
|
887
|
+
for (const line of lines) {
|
|
888
|
+
const trimmed = line.trim();
|
|
889
|
+
if (trimmed.length === 0) continue;
|
|
890
|
+
let raw: Record<string, unknown>;
|
|
891
|
+
try {
|
|
892
|
+
raw = JSON.parse(trimmed) as Record<string, unknown>;
|
|
893
|
+
} catch {
|
|
894
|
+
migrated.push(trimmed);
|
|
895
|
+
continue;
|
|
896
|
+
}
|
|
897
|
+
if (
|
|
898
|
+
"outcome" in raw &&
|
|
899
|
+
raw.outcome !== null &&
|
|
900
|
+
raw.outcome !== undefined &&
|
|
901
|
+
!("outcomes" in raw)
|
|
902
|
+
) {
|
|
903
|
+
const legacy = raw.outcome as Record<string, unknown>;
|
|
904
|
+
const rewritten: Record<string, unknown> = { ...raw };
|
|
905
|
+
rewritten.outcome = undefined;
|
|
906
|
+
rewritten.outcomes = [
|
|
907
|
+
{
|
|
908
|
+
status: legacy.status,
|
|
909
|
+
...(legacy.duration !== undefined ? { duration: legacy.duration } : {}),
|
|
910
|
+
...(legacy.test_results !== undefined
|
|
911
|
+
? { test_results: legacy.test_results }
|
|
912
|
+
: {}),
|
|
913
|
+
...(legacy.agent !== undefined ? { agent: legacy.agent } : {}),
|
|
914
|
+
},
|
|
915
|
+
];
|
|
916
|
+
migrated.push(JSON.stringify(rewritten));
|
|
917
|
+
count++;
|
|
918
|
+
} else {
|
|
919
|
+
migrated.push(trimmed);
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
if (count > 0) {
|
|
923
|
+
await fsWriteFile(
|
|
924
|
+
filePath,
|
|
925
|
+
migrated.join("\n") + (migrated.length > 0 ? "\n" : ""),
|
|
926
|
+
"utf-8",
|
|
927
|
+
);
|
|
928
|
+
fixed.push(
|
|
929
|
+
`Migrated ${count} legacy "outcome" field(s) to "outcomes[]" in ${domain}`,
|
|
930
|
+
);
|
|
931
|
+
}
|
|
932
|
+
});
|
|
933
|
+
}
|
|
934
|
+
break;
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
case "file-anchors": {
|
|
938
|
+
const projectRoot = cwd ?? process.cwd();
|
|
939
|
+
for (const domain of Object.keys(config.domains)) {
|
|
940
|
+
const filePath = getExpertisePath(domain, cwd);
|
|
941
|
+
await withFileLock(filePath, async () => {
|
|
942
|
+
const records = await readExpertiseFile(filePath, { allowUnknownTypes: true });
|
|
943
|
+
let removedCount = 0;
|
|
944
|
+
const updated: ExpertiseRecord[] = records.map((record) => {
|
|
945
|
+
let r: ExpertiseRecord = record;
|
|
946
|
+
if ("files" in r && Array.isArray(r.files)) {
|
|
947
|
+
const before = r.files.length;
|
|
948
|
+
const kept = r.files.filter((f) => existsSync(resolve(projectRoot, f)));
|
|
949
|
+
if (kept.length < before) {
|
|
950
|
+
removedCount += before - kept.length;
|
|
951
|
+
r = { ...r, files: kept.length > 0 ? kept : undefined } as ExpertiseRecord;
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
if (Array.isArray(r.dir_anchors)) {
|
|
955
|
+
const before = r.dir_anchors.length;
|
|
956
|
+
const kept = r.dir_anchors.filter((d) => existsSync(resolve(projectRoot, d)));
|
|
957
|
+
if (kept.length < before) {
|
|
958
|
+
removedCount += before - kept.length;
|
|
959
|
+
r = { ...r, dir_anchors: kept.length > 0 ? kept : undefined };
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
if (r.evidence?.file && !existsSync(resolve(projectRoot, r.evidence.file))) {
|
|
963
|
+
removedCount++;
|
|
964
|
+
r = { ...r, evidence: { ...r.evidence, file: undefined } };
|
|
965
|
+
}
|
|
966
|
+
return r;
|
|
967
|
+
});
|
|
968
|
+
if (removedCount > 0) {
|
|
969
|
+
await writeExpertiseFile(filePath, updated);
|
|
970
|
+
fixed.push(`Removed ${removedCount} broken file anchor(s) from ${domain}`);
|
|
971
|
+
}
|
|
972
|
+
});
|
|
973
|
+
}
|
|
974
|
+
break;
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
case "orphaned-domains": {
|
|
978
|
+
const expertiseDir = getExpertiseDir(cwd);
|
|
979
|
+
// Add missing domains to config
|
|
980
|
+
try {
|
|
981
|
+
const files = await readdir(expertiseDir);
|
|
982
|
+
for (const file of files) {
|
|
983
|
+
if (file.endsWith(".jsonl")) {
|
|
984
|
+
const domain = file.replace(".jsonl", "");
|
|
985
|
+
if (!(domain in config.domains)) {
|
|
986
|
+
config.domains[domain] = {};
|
|
987
|
+
fixed.push(`Added orphaned domain "${domain}" to config`);
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
} catch {
|
|
992
|
+
// expertise dir doesn't exist
|
|
993
|
+
}
|
|
994
|
+
// Create missing JSONL files
|
|
995
|
+
for (const domain of Object.keys(config.domains)) {
|
|
996
|
+
const filePath = getExpertisePath(domain, cwd);
|
|
997
|
+
if (!existsSync(filePath)) {
|
|
998
|
+
await createExpertiseFile(filePath);
|
|
999
|
+
fixed.push(`Created missing JSONL file for domain "${domain}"`);
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
if (fixed.length > 0) {
|
|
1003
|
+
await writeConfig(config, cwd);
|
|
1004
|
+
}
|
|
1005
|
+
break;
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
return fixed;
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
export function registerDoctorCommand(program: Command): void {
|
|
1014
|
+
program
|
|
1015
|
+
.command("doctor")
|
|
1016
|
+
.description("Run health checks on expertise records")
|
|
1017
|
+
.option("--fix", "auto-fix fixable issues")
|
|
1018
|
+
.action(async (options: { fix?: boolean }) => {
|
|
1019
|
+
const jsonMode = program.opts().json === true;
|
|
1020
|
+
|
|
1021
|
+
// Check config first — if it fails, we can't run other checks
|
|
1022
|
+
const configCheck = await checkConfig();
|
|
1023
|
+
if (configCheck.status === "fail") {
|
|
1024
|
+
const checks = [configCheck];
|
|
1025
|
+
const summary = { pass: 0, warn: 0, fail: 1 };
|
|
1026
|
+
if (jsonMode) {
|
|
1027
|
+
outputJson({ success: false, command: "doctor", checks, summary });
|
|
1028
|
+
} else {
|
|
1029
|
+
if (!isQuiet()) console.log("Loam Doctor");
|
|
1030
|
+
console.error(` ${icons.fail} ${chalk.red(configCheck.message)}`);
|
|
1031
|
+
if (!isQuiet()) console.log("\n0 passed, 0 warnings, 1 failed");
|
|
1032
|
+
}
|
|
1033
|
+
process.exitCode = 1;
|
|
1034
|
+
return;
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
const config = await readConfig();
|
|
1038
|
+
|
|
1039
|
+
const checks: DoctorCheck[] = [configCheck];
|
|
1040
|
+
checks.push(await checkJsonlIntegrity(config));
|
|
1041
|
+
checks.push(await checkLegacyOutcome(config));
|
|
1042
|
+
checks.push(await checkSchemaValidation(config));
|
|
1043
|
+
checks.push(await checkUnknownTypes(config));
|
|
1044
|
+
checks.push(await checkTypeRegistry(config));
|
|
1045
|
+
checks.push(await checkDomainRulesCompatibility(config));
|
|
1046
|
+
const domainConformance = await checkDomainConformance(config);
|
|
1047
|
+
checks.push(domainConformance.info);
|
|
1048
|
+
checks.push(domainConformance.violations);
|
|
1049
|
+
checks.push(await checkStaleRecords(config));
|
|
1050
|
+
checks.push(await checkOrphanedDomains(config));
|
|
1051
|
+
checks.push(await checkDuplicates(config));
|
|
1052
|
+
checks.push(await checkFileAnchors(config));
|
|
1053
|
+
checks.push(await checkGovernance(config));
|
|
1054
|
+
checks.push(checkDecayConfig(config));
|
|
1055
|
+
checks.push(checkCompactSummarizer(config));
|
|
1056
|
+
checks.push(await checkUpdateAvailable());
|
|
1057
|
+
|
|
1058
|
+
const summary = {
|
|
1059
|
+
pass: checks.filter((c) => c.status === "pass").length,
|
|
1060
|
+
warn: checks.filter((c) => c.status === "warn").length,
|
|
1061
|
+
fail: checks.filter((c) => c.status === "fail").length,
|
|
1062
|
+
};
|
|
1063
|
+
|
|
1064
|
+
let fixed: string[] = [];
|
|
1065
|
+
if (options.fix) {
|
|
1066
|
+
fixed = await applyFixes(checks, config);
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
if (jsonMode) {
|
|
1070
|
+
outputJson({
|
|
1071
|
+
success: summary.fail === 0,
|
|
1072
|
+
command: "doctor",
|
|
1073
|
+
checks,
|
|
1074
|
+
summary,
|
|
1075
|
+
...(options.fix && { fixed }),
|
|
1076
|
+
});
|
|
1077
|
+
} else {
|
|
1078
|
+
if (!isQuiet()) console.log("Loam Doctor");
|
|
1079
|
+
for (const check of checks) {
|
|
1080
|
+
const icon =
|
|
1081
|
+
check.status === "pass"
|
|
1082
|
+
? icons.pass
|
|
1083
|
+
: check.status === "warn"
|
|
1084
|
+
? icons.warn
|
|
1085
|
+
: icons.fail;
|
|
1086
|
+
const msg = check.status === "pass" ? check.message : `${check.message}`;
|
|
1087
|
+
if (!isQuiet()) console.log(` ${icon} ${msg}`);
|
|
1088
|
+
|
|
1089
|
+
// Print details for non-pass checks
|
|
1090
|
+
if (check.status !== "pass" && check.details.length > 0) {
|
|
1091
|
+
for (const detail of check.details) {
|
|
1092
|
+
if (!isQuiet()) console.log(` ${detail}`);
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
if (!isQuiet())
|
|
1097
|
+
console.log(
|
|
1098
|
+
`\n${summary.pass} passed, ${summary.warn} warning(s), ${summary.fail} failed`,
|
|
1099
|
+
);
|
|
1100
|
+
|
|
1101
|
+
if (fixed.length > 0) {
|
|
1102
|
+
if (!isQuiet()) console.log(`\n${brand("Fixed:")}`);
|
|
1103
|
+
for (const f of fixed) {
|
|
1104
|
+
if (!isQuiet()) console.log(` ${icons.pass} ${f}`);
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
if (summary.fail > 0) {
|
|
1110
|
+
process.exitCode = 1;
|
|
1111
|
+
}
|
|
1112
|
+
});
|
|
1113
|
+
}
|