@ybotok/loam 0.2.0-alpha.6 → 0.2.0-alpha.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +1477 -1
- package/COMPARISON.md +91 -3
- package/CONTRIBUTING.md +37 -0
- package/MIGRATING-from-OpenSpec.md +40 -22
- package/QUICKSTART.md +181 -9
- package/README.md +256 -57
- package/ROADMAP.md +108 -11
- package/SCHEMA.md +721 -130
- package/WORKFLOW.md +272 -62
- package/dist/cli.js +26 -8
- package/dist/cli.js.map +1 -1
- package/dist/commands/adopt/adopt.js +5 -10
- package/dist/commands/adopt/adopt.js.map +1 -1
- package/dist/commands/archive/archive.js +15 -18
- package/dist/commands/archive/archive.js.map +1 -1
- package/dist/commands/archive/plan/contracts/asyncapi.js +27 -41
- package/dist/commands/archive/plan/contracts/asyncapi.js.map +1 -1
- package/dist/commands/archive/plan/contracts/consumers.js +56 -0
- package/dist/commands/archive/plan/contracts/consumers.js.map +1 -0
- package/dist/commands/archive/plan/contracts/openapi.js +25 -42
- package/dist/commands/archive/plan/contracts/openapi.js.map +1 -1
- package/dist/commands/archive/plan/contracts/verdict-lines.js +41 -0
- package/dist/commands/archive/plan/contracts/verdict-lines.js.map +1 -0
- package/dist/commands/archive/plan/gate.js +3 -7
- package/dist/commands/archive/plan/gate.js.map +1 -1
- package/dist/commands/archive/plan/landscape.js +4 -4
- package/dist/commands/archive/plan/landscape.js.map +1 -1
- package/dist/commands/archive/plan/refusal.js +8 -18
- package/dist/commands/archive/plan/refusal.js.map +1 -1
- package/dist/commands/archive/plan/specs.js +134 -25
- package/dist/commands/archive/plan/specs.js.map +1 -1
- package/dist/commands/archive/plan/state.js +1 -0
- package/dist/commands/archive/plan/state.js.map +1 -1
- package/dist/commands/archive/run.js +82 -33
- package/dist/commands/archive/run.js.map +1 -1
- package/dist/commands/archive/shipped.js +49 -0
- package/dist/commands/archive/shipped.js.map +1 -0
- package/dist/commands/context/context.js +4 -7
- package/dist/commands/context/context.js.map +1 -1
- package/dist/commands/context/print.js +2 -2
- package/dist/commands/context/print.js.map +1 -1
- package/dist/commands/delta/delta.js +44 -12
- package/dist/commands/delta/delta.js.map +1 -1
- package/dist/commands/delta/print.js +1 -1
- package/dist/commands/delta/print.js.map +1 -1
- package/dist/commands/dependencies.js +3 -7
- package/dist/commands/dependencies.js.map +1 -1
- package/dist/commands/diff/diff.js +20 -27
- package/dist/commands/diff/diff.js.map +1 -1
- package/dist/commands/explore.js +4 -8
- package/dist/commands/explore.js.map +1 -1
- package/dist/commands/gate/gate.js +45 -8
- package/dist/commands/gate/gate.js.map +1 -1
- package/dist/commands/gate/print.js +15 -3
- package/dist/commands/gate/print.js.map +1 -1
- package/dist/commands/gherkin/commit.js +13 -54
- package/dist/commands/gherkin/commit.js.map +1 -1
- package/dist/commands/gherkin/gherkin.js +36 -20
- package/dist/commands/gherkin/gherkin.js.map +1 -1
- package/dist/commands/gherkin/reconcile.js +10 -13
- package/dist/commands/gherkin/reconcile.js.map +1 -1
- package/dist/commands/init/first-hour.js +44 -0
- package/dist/commands/init/first-hour.js.map +1 -1
- package/dist/commands/init/init.js +18 -22
- package/dist/commands/init/init.js.map +1 -1
- package/dist/commands/list/campaign/campaign.js +4 -6
- package/dist/commands/list/campaign/campaign.js.map +1 -1
- package/dist/commands/list/json.js +0 -4
- package/dist/commands/list/json.js.map +1 -1
- package/dist/commands/list/list.js +38 -23
- package/dist/commands/list/list.js.map +1 -1
- package/dist/commands/list/print.js +38 -6
- package/dist/commands/list/print.js.map +1 -1
- package/dist/commands/list/suites/suites.js +140 -0
- package/dist/commands/list/suites/suites.js.map +1 -0
- package/dist/commands/list/views.js +28 -33
- package/dist/commands/list/views.js.map +1 -1
- package/dist/commands/mcp/dispatch.js +91 -36
- package/dist/commands/mcp/dispatch.js.map +1 -1
- package/dist/commands/mcp/mcp.js +56 -15
- package/dist/commands/mcp/mcp.js.map +1 -1
- package/dist/commands/mcp/serve.js.map +1 -1
- package/dist/commands/migrate-openspec/materialize/active.js +27 -22
- package/dist/commands/migrate-openspec/materialize/active.js.map +1 -1
- package/dist/commands/migrate-openspec/materialize/stage.js +4 -3
- package/dist/commands/migrate-openspec/materialize/stage.js.map +1 -1
- package/dist/commands/migrate-openspec/materialize/target.js +2 -2
- package/dist/commands/migrate-openspec/materialize/target.js.map +1 -1
- package/dist/commands/migrate-openspec/openspec/decisions.js +9 -14
- package/dist/commands/migrate-openspec/openspec/decisions.js.map +1 -1
- package/dist/commands/migrate-openspec/openspec/error.js +7 -8
- package/dist/commands/migrate-openspec/openspec/error.js.map +1 -1
- package/dist/commands/migrate-openspec/openspec/mapping.js +1 -9
- package/dist/commands/migrate-openspec/openspec/mapping.js.map +1 -1
- package/dist/commands/migrate-openspec/openspec/paths.js +3 -6
- package/dist/commands/migrate-openspec/openspec/paths.js.map +1 -1
- package/dist/commands/new/capability-template.js +118 -0
- package/dist/commands/new/capability-template.js.map +1 -0
- package/dist/commands/new/commit.js +8 -22
- package/dist/commands/new/commit.js.map +1 -1
- package/dist/commands/new/modifies.js +206 -0
- package/dist/commands/new/modifies.js.map +1 -0
- package/dist/commands/new/new.js +59 -14
- package/dist/commands/new/new.js.map +1 -1
- package/dist/commands/new/templates.js +48 -124
- package/dist/commands/new/templates.js.map +1 -1
- package/dist/commands/open.js +3 -7
- package/dist/commands/open.js.map +1 -1
- package/dist/commands/policy/gate.js +27 -0
- package/dist/commands/policy/gate.js.map +1 -1
- package/dist/commands/rebase/plan.js +2 -2
- package/dist/commands/rebase/plan.js.map +1 -1
- package/dist/commands/rebase/rebase.js +60 -35
- package/dist/commands/rebase/rebase.js.map +1 -1
- package/dist/commands/seed/commit.js +55 -26
- package/dist/commands/seed/commit.js.map +1 -1
- package/dist/commands/seed/plan.js +64 -13
- package/dist/commands/seed/plan.js.map +1 -1
- package/dist/commands/seed/seed.js +6 -9
- package/dist/commands/seed/seed.js.map +1 -1
- package/dist/commands/show/show.js +3 -7
- package/dist/commands/show/show.js.map +1 -1
- package/dist/commands/status/print.js +13 -4
- package/dist/commands/status/print.js.map +1 -1
- package/dist/commands/status/status.js +3 -7
- package/dist/commands/status/status.js.map +1 -1
- package/dist/commands/steps/steps.js +3 -7
- package/dist/commands/steps/steps.js.map +1 -1
- package/dist/commands/subsystem/edit.js +7 -7
- package/dist/commands/subsystem/edit.js.map +1 -1
- package/dist/commands/subsystem/move.js +60 -25
- package/dist/commands/subsystem/move.js.map +1 -1
- package/dist/commands/subsystem/query.js +2 -4
- package/dist/commands/subsystem/query.js.map +1 -1
- package/dist/commands/subsystem/subsystem.js +4 -7
- package/dist/commands/subsystem/subsystem.js.map +1 -1
- package/dist/commands/subsystem/sync.js +141 -71
- package/dist/commands/subsystem/sync.js.map +1 -1
- package/dist/commands/subsystem/txn/report.js +55 -5
- package/dist/commands/subsystem/txn/report.js.map +1 -1
- package/dist/commands/subsystem/txn/txn.js +121 -45
- package/dist/commands/subsystem/txn/txn.js.map +1 -1
- package/dist/commands/subsystem/txn/views.js +82 -12
- package/dist/commands/subsystem/txn/views.js.map +1 -1
- package/dist/commands/tasks/render.js +116 -0
- package/dist/commands/tasks/render.js.map +1 -0
- package/dist/commands/tasks/tasks.js +113 -0
- package/dist/commands/tasks/tasks.js.map +1 -0
- package/dist/commands/unarchive/manifest.js +6 -9
- package/dist/commands/unarchive/manifest.js.map +1 -1
- package/dist/commands/unarchive/restore.js +17 -28
- package/dist/commands/unarchive/restore.js.map +1 -1
- package/dist/commands/unarchive/unarchive.js +22 -30
- package/dist/commands/unarchive/unarchive.js.map +1 -1
- package/dist/commands/validate/arch-coverage.js +6 -15
- package/dist/commands/validate/arch-coverage.js.map +1 -1
- package/dist/commands/validate/checks/fleet-shape.js +4 -49
- package/dist/commands/validate/checks/fleet-shape.js.map +1 -1
- package/dist/commands/validate/checks/health-deps.js +9 -6
- package/dist/commands/validate/checks/health-deps.js.map +1 -1
- package/dist/commands/validate/checks/requirements.js +18 -8
- package/dist/commands/validate/checks/requirements.js.map +1 -1
- package/dist/commands/validate/checks/sources.js +6 -2
- package/dist/commands/validate/checks/sources.js.map +1 -1
- package/dist/commands/validate/feature.js +23 -19
- package/dist/commands/validate/feature.js.map +1 -1
- package/dist/commands/validate/fleet/census.js +26 -3
- package/dist/commands/validate/fleet/census.js.map +1 -1
- package/dist/commands/validate/fleet/landscape.js +35 -32
- package/dist/commands/validate/fleet/landscape.js.map +1 -1
- package/dist/commands/validate/fleet/map/attest.js +7 -12
- package/dist/commands/validate/fleet/map/attest.js.map +1 -1
- package/dist/commands/validate/fleet/map/bindings.js +16 -5
- package/dist/commands/validate/fleet/map/bindings.js.map +1 -1
- package/dist/commands/validate/fleet/map/consumers.js +43 -122
- package/dist/commands/validate/fleet/map/consumers.js.map +1 -1
- package/dist/commands/validate/fleet/map/usecases.js +5 -11
- package/dist/commands/validate/fleet/map/usecases.js.map +1 -1
- package/dist/commands/validate/fleet/obligations.js +1 -5
- package/dist/commands/validate/fleet/obligations.js.map +1 -1
- package/dist/commands/validate/fleet/scorecard/print.js +5 -1
- package/dist/commands/validate/fleet/scorecard/print.js.map +1 -1
- package/dist/commands/validate/fleet/scorecard/scorecard.js +17 -11
- package/dist/commands/validate/fleet/scorecard/scorecard.js.map +1 -1
- package/dist/commands/validate/fleet/usecases/usecases.js +6 -4
- package/dist/commands/validate/fleet/usecases/usecases.js.map +1 -1
- package/dist/commands/validate/fleet/views/fleet-project.js +1 -1
- package/dist/commands/validate/fleet/views/fleet-project.js.map +1 -1
- package/dist/commands/validate/fleet/views/ids.js +33 -7
- package/dist/commands/validate/fleet/views/ids.js.map +1 -1
- package/dist/commands/validate/fleet/views/root-documents.js +17 -35
- package/dist/commands/validate/fleet/views/root-documents.js.map +1 -1
- package/dist/commands/validate/fleet/views/stale.js +31 -14
- package/dist/commands/validate/fleet/views/stale.js.map +1 -1
- package/dist/commands/validate/links/corpus.js +1 -4
- package/dist/commands/validate/links/corpus.js.map +1 -1
- package/dist/commands/validate/links/glossary.js.map +1 -1
- package/dist/commands/validate/report.js +5 -4
- package/dist/commands/validate/report.js.map +1 -1
- package/dist/commands/validate/scope.js +113 -0
- package/dist/commands/validate/scope.js.map +1 -0
- package/dist/commands/validate/service/api.js +16 -104
- package/dist/commands/validate/service/api.js.map +1 -1
- package/dist/commands/validate/service/contracts/generated.js +134 -0
- package/dist/commands/validate/service/contracts/generated.js.map +1 -0
- package/dist/commands/validate/service/events/events.js +1 -1
- package/dist/commands/validate/service/events/events.js.map +1 -1
- package/dist/commands/validate/service/events/message-producers.js +1 -1
- package/dist/commands/validate/service/events/message-producers.js.map +1 -1
- package/dist/commands/validate/service/evidence-pins.js +56 -10
- package/dist/commands/validate/service/evidence-pins.js.map +1 -1
- package/dist/commands/validate/service/service.js +16 -26
- package/dist/commands/validate/service/service.js.map +1 -1
- package/dist/commands/validate/service/specs.js +31 -38
- package/dist/commands/validate/service/specs.js.map +1 -1
- package/dist/commands/validate/service/spine.js +8 -11
- package/dist/commands/validate/service/spine.js.map +1 -1
- package/dist/commands/validate/service/usecases/usecases.js +3 -3
- package/dist/commands/validate/service/usecases/usecases.js.map +1 -1
- package/dist/commands/validate/validate.js +44 -26
- package/dist/commands/validate/validate.js.map +1 -1
- package/dist/commands/verify/cross/diff.js +2 -6
- package/dist/commands/verify/cross/diff.js.map +1 -1
- package/dist/commands/verify/evidence/contract.js +1 -1
- package/dist/commands/verify/evidence/contract.js.map +1 -1
- package/dist/commands/verify/evidence/read.js +12 -3
- package/dist/commands/verify/evidence/read.js.map +1 -1
- package/dist/commands/verify/frozen.js +45 -23
- package/dist/commands/verify/frozen.js.map +1 -1
- package/dist/commands/verify/record.js +69 -17
- package/dist/commands/verify/record.js.map +1 -1
- package/dist/commands/verify/report.js +27 -44
- package/dist/commands/verify/report.js.map +1 -1
- package/dist/commands/verify/results.js +112 -30
- package/dist/commands/verify/results.js.map +1 -1
- package/dist/commands/verify/verify.js +54 -51
- package/dist/commands/verify/verify.js.map +1 -1
- package/dist/commands/vouch/pack/pack.js +2 -2
- package/dist/commands/vouch/pack/pack.js.map +1 -1
- package/dist/commands/vouch/pack/print.js +1 -6
- package/dist/commands/vouch/pack/print.js.map +1 -1
- package/dist/commands/vouch/pack/sections.js +1 -2
- package/dist/commands/vouch/pack/sections.js.map +1 -1
- package/dist/commands/vouch/run.js +26 -81
- package/dist/commands/vouch/run.js.map +1 -1
- package/dist/commands/vouch/sample/plan.js +20 -34
- package/dist/commands/vouch/sample/plan.js.map +1 -1
- package/dist/commands/vouch/vet/verify.js +39 -7
- package/dist/commands/vouch/vet/verify.js.map +1 -1
- package/dist/commands/vouch/vouch.js +10 -17
- package/dist/commands/vouch/vouch.js.map +1 -1
- package/dist/core/agent/agents-md/command-map.js +57 -102
- package/dist/core/agent/agents-md/command-map.js.map +1 -1
- package/dist/core/agent/agents-md/map/explain.js +4 -3
- package/dist/core/agent/agents-md/map/explain.js.map +1 -1
- package/dist/core/agent/agents-md/map/lenses/context.js +4 -3
- package/dist/core/agent/agents-md/map/lenses/context.js.map +1 -1
- package/dist/core/agent/agents-md/map/lenses/diff.js +33 -20
- package/dist/core/agent/agents-md/map/lenses/diff.js.map +1 -1
- package/dist/core/agent/agents-md/map/lenses/gate.js +13 -5
- package/dist/core/agent/agents-md/map/lenses/gate.js.map +1 -1
- package/dist/core/agent/agents-md/map/mcp.js +10 -7
- package/dist/core/agent/agents-md/map/mcp.js.map +1 -1
- package/dist/core/agent/agents-md/map/status.js +9 -3
- package/dist/core/agent/agents-md/map/status.js.map +1 -1
- package/dist/core/agent/agents-md/map/subsystem.js +31 -17
- package/dist/core/agent/agents-md/map/subsystem.js.map +1 -1
- package/dist/core/agent/agents-md/map/usecases.js +24 -6
- package/dist/core/agent/agents-md/map/usecases.js.map +1 -1
- package/dist/core/agent/agents-md/refusals.js +37 -21
- package/dist/core/agent/agents-md/refusals.js.map +1 -1
- package/dist/core/agent/agents-md.js +25 -10
- package/dist/core/agent/agents-md.js.map +1 -1
- package/dist/core/agent/agents-stamp.js +10 -0
- package/dist/core/agent/agents-stamp.js.map +1 -1
- package/dist/core/agent/scaffold.js +10 -15
- package/dist/core/agent/scaffold.js.map +1 -1
- package/dist/core/agent/tools/dialects.js +1 -0
- package/dist/core/agent/tools/dialects.js.map +1 -1
- package/dist/core/agent/tools/registry.js +41 -76
- package/dist/core/agent/tools/registry.js.map +1 -1
- package/dist/core/agent/workflows/adopt.js +5 -3
- package/dist/core/agent/workflows/adopt.js.map +1 -1
- package/dist/core/agent/workflows/check.js +25 -22
- package/dist/core/agent/workflows/check.js.map +1 -1
- package/dist/core/agent/workflows/closing.js +54 -21
- package/dist/core/agent/workflows/closing.js.map +1 -1
- package/dist/core/agent/workflows/feature.js +79 -11
- package/dist/core/agent/workflows/feature.js.map +1 -1
- package/dist/core/agent/workflows/implement.js +91 -13
- package/dist/core/agent/workflows/implement.js.map +1 -1
- package/dist/core/agent/workflows/reference/authoring.js +3 -1
- package/dist/core/agent/workflows/reference/authoring.js.map +1 -1
- package/dist/core/agent/workflows/reference/codes.js +8 -6
- package/dist/core/agent/workflows/reference/codes.js.map +1 -1
- package/dist/core/agent/workflows/reference/done-check.js +37 -15
- package/dist/core/agent/workflows/reference/done-check.js.map +1 -1
- package/dist/core/agent/workflows/reference/reference.js.map +1 -1
- package/dist/core/agent/workflows/reference/spine.js +62 -24
- package/dist/core/agent/workflows/reference/spine.js.map +1 -1
- package/dist/core/asyncapi/baseline/plan.js +14 -81
- package/dist/core/asyncapi/baseline/plan.js.map +1 -1
- package/dist/core/asyncapi/digest.js +30 -25
- package/dist/core/asyncapi/digest.js.map +1 -1
- package/dist/core/asyncapi/merge/components.js +5 -4
- package/dist/core/asyncapi/merge/components.js.map +1 -1
- package/dist/core/asyncapi/merge/error.js +7 -2
- package/dist/core/asyncapi/merge/error.js.map +1 -1
- package/dist/core/asyncapi/merge/markers.js +30 -50
- package/dist/core/asyncapi/merge/markers.js.map +1 -1
- package/dist/core/asyncapi/merge/merge.js +31 -53
- package/dist/core/asyncapi/merge/merge.js.map +1 -1
- package/dist/core/asyncapi/merge/pin.js +10 -36
- package/dist/core/asyncapi/merge/pin.js.map +1 -1
- package/dist/core/asyncapi/model.js.map +1 -1
- package/dist/core/asyncapi/read.js +36 -12
- package/dist/core/asyncapi/read.js.map +1 -1
- package/dist/core/brief/brief.js +30 -25
- package/dist/core/brief/brief.js.map +1 -1
- package/dist/core/brief/landscape.js +8 -12
- package/dist/core/brief/landscape.js.map +1 -1
- package/dist/core/brief/map/extend.js +8 -5
- package/dist/core/brief/map/extend.js.map +1 -1
- package/dist/core/brief/map/owed.js +8 -26
- package/dist/core/brief/map/owed.js.map +1 -1
- package/dist/core/brief/targets.js +79 -5
- package/dist/core/brief/targets.js.map +1 -1
- package/dist/core/brief/unchecked.js +1 -1
- package/dist/core/brief/unchecked.js.map +1 -1
- package/dist/core/c4/likec4.js +147 -16
- package/dist/core/c4/likec4.js.map +1 -1
- package/dist/core/c4/locate/unlocated.js +150 -0
- package/dist/core/c4/locate/unlocated.js.map +1 -0
- package/dist/core/c4/parsed/values.js +54 -1
- package/dist/core/c4/parsed/values.js.map +1 -1
- package/dist/core/c4/prefetch/fleet.js +1 -9
- package/dist/core/c4/prefetch/fleet.js.map +1 -1
- package/dist/core/c4/project/architecture.js +31 -9
- package/dist/core/c4/project/architecture.js.map +1 -1
- package/dist/core/c4/project/documents.js +36 -10
- package/dist/core/c4/project/documents.js.map +1 -1
- package/dist/core/c4/project/load.js +14 -21
- package/dist/core/c4/project/load.js.map +1 -1
- package/dist/core/c4/project/staged.js +7 -3
- package/dist/core/c4/project/staged.js.map +1 -1
- package/dist/core/c4/resolve/external.js +51 -0
- package/dist/core/c4/resolve/external.js.map +1 -0
- package/dist/core/c4/resolve/service.js +25 -1
- package/dist/core/c4/resolve/service.js.map +1 -1
- package/dist/core/c4/seed/template.js +15 -1
- package/dist/core/c4/seed/template.js.map +1 -1
- package/dist/core/c4/service-model/shape.js +2 -22
- package/dist/core/c4/service-model/shape.js.map +1 -1
- package/dist/core/c4/source-mask.js +288 -4
- package/dist/core/c4/source-mask.js.map +1 -1
- package/dist/core/c4/source-scan.js +203 -147
- package/dist/core/c4/source-scan.js.map +1 -1
- package/dist/core/c4/splice/authored-source.js +19 -0
- package/dist/core/c4/splice/authored-source.js.map +1 -1
- package/dist/core/c4/splice/delta-blocks.js +5 -18
- package/dist/core/c4/splice/delta-blocks.js.map +1 -1
- package/dist/core/c4/splice/landscape-merge.js +13 -8
- package/dist/core/c4/splice/landscape-merge.js.map +1 -1
- package/dist/core/c4/splice/model/merge.js +10 -6
- package/dist/core/c4/splice/model/merge.js.map +1 -1
- package/dist/core/c4/splice/placement.js +108 -18
- package/dist/core/c4/splice/placement.js.map +1 -1
- package/dist/core/c4/workspace.js +13 -22
- package/dist/core/c4/workspace.js.map +1 -1
- package/dist/core/capabilities/capabilities.js +6 -6
- package/dist/core/capabilities/capabilities.js.map +1 -1
- package/dist/core/capabilities/delta/doc.js +7 -10
- package/dist/core/capabilities/delta/doc.js.map +1 -1
- package/dist/core/capabilities/findings.js +15 -20
- package/dist/core/capabilities/findings.js.map +1 -1
- package/dist/core/capabilities/realizes/findings.js +12 -19
- package/dist/core/capabilities/realizes/findings.js.map +1 -1
- package/dist/core/capabilities/realizes/join.js +0 -20
- package/dist/core/capabilities/realizes/join.js.map +1 -1
- package/dist/core/capabilities/rollup.js +6 -0
- package/dist/core/capabilities/rollup.js.map +1 -1
- package/dist/core/capabilities/tree.js +6 -28
- package/dist/core/capabilities/tree.js.map +1 -1
- package/dist/core/capabilities/usecase-join.js +3 -13
- package/dist/core/capabilities/usecase-join.js.map +1 -1
- package/dist/core/coherence/authoring/scaffold.js +11 -3
- package/dist/core/coherence/authoring/scaffold.js.map +1 -1
- package/dist/core/coherence/authoring/sentinels.js +2 -1
- package/dist/core/coherence/authoring/sentinels.js.map +1 -1
- package/dist/core/coherence/coherence.js +30 -16
- package/dist/core/coherence/coherence.js.map +1 -1
- package/dist/core/coherence/consumed/operations.js +70 -0
- package/dist/core/coherence/consumed/operations.js.map +1 -0
- package/dist/core/coherence/consumed/refusal.js +38 -0
- package/dist/core/coherence/consumed/refusal.js.map +1 -0
- package/dist/core/coherence/declared.js +60 -34
- package/dist/core/coherence/declared.js.map +1 -1
- package/dist/core/coherence/events/declared.js +44 -59
- package/dist/core/coherence/events/declared.js.map +1 -1
- package/dist/core/coherence/events/events.js +119 -8
- package/dist/core/coherence/events/events.js.map +1 -1
- package/dist/core/coherence/events/grades.js +31 -8
- package/dist/core/coherence/events/grades.js.map +1 -1
- package/dist/core/coherence/events/lookups.js +14 -82
- package/dist/core/coherence/events/lookups.js.map +1 -1
- package/dist/core/coherence/lookups.js +18 -78
- package/dist/core/coherence/lookups.js.map +1 -1
- package/dist/core/coherence/retired/archived.js +285 -0
- package/dist/core/coherence/retired/archived.js.map +1 -0
- package/dist/core/coherence/retired/event-pair.js +62 -0
- package/dist/core/coherence/retired/event-pair.js.map +1 -0
- package/dist/core/coherence/retired/merged.js +79 -0
- package/dist/core/coherence/retired/merged.js.map +1 -0
- package/dist/core/coherence/retired/retirement.js +49 -0
- package/dist/core/coherence/retired/retirement.js.map +1 -0
- package/dist/core/coherence/retired/revival/arrivals.js +131 -0
- package/dist/core/coherence/retired/revival/arrivals.js.map +1 -0
- package/dist/core/coherence/retired/revival/revived.js +297 -0
- package/dist/core/coherence/retired/revival/revived.js.map +1 -0
- package/dist/core/consumers/after.js +113 -0
- package/dist/core/consumers/after.js.map +1 -0
- package/dist/core/consumers/consumers.js +296 -0
- package/dist/core/consumers/consumers.js.map +1 -0
- package/dist/core/consumers/current.js +211 -0
- package/dist/core/consumers/current.js.map +1 -0
- package/dist/core/consumers/scan.js +46 -0
- package/dist/core/consumers/scan.js.map +1 -0
- package/dist/core/consumers/state.js +27 -0
- package/dist/core/consumers/state.js.map +1 -0
- package/dist/core/delta/delta.js +1 -1
- package/dist/core/delta/delta.js.map +1 -1
- package/dist/core/deployment/delta.js +15 -16
- package/dist/core/deployment/delta.js.map +1 -1
- package/dist/core/diff/base-git.js +94 -12
- package/dist/core/diff/base-git.js.map +1 -1
- package/dist/core/diff/base-state.js +28 -16
- package/dist/core/diff/base-state.js.map +1 -1
- package/dist/core/diff/joins.js +136 -0
- package/dist/core/diff/joins.js.map +1 -0
- package/dist/core/diff/scope/changed-paths.js +22 -6
- package/dist/core/diff/scope/changed-paths.js.map +1 -1
- package/dist/core/diff/scope/edges.js +82 -0
- package/dist/core/diff/scope/edges.js.map +1 -0
- package/dist/core/diff/semantic.js +100 -31
- package/dist/core/diff/semantic.js.map +1 -1
- package/dist/core/doctor/agents.js +34 -18
- package/dist/core/doctor/agents.js.map +1 -1
- package/dist/core/doctor/report.js.map +1 -1
- package/dist/core/document/apply.js +10 -2
- package/dist/core/document/apply.js.map +1 -1
- package/dist/core/document/frontmatter.js +55 -0
- package/dist/core/document/frontmatter.js.map +1 -1
- package/dist/core/document/parse.js +29 -6
- package/dist/core/document/parse.js.map +1 -1
- package/dist/core/document/scenarios.js +17 -0
- package/dist/core/document/scenarios.js.map +1 -1
- package/dist/core/document/spec.js +4 -11
- package/dist/core/document/spec.js.map +1 -1
- package/dist/core/envelope/config.js +10 -0
- package/dist/core/envelope/config.js.map +1 -1
- package/dist/core/envelope/json.js +34 -0
- package/dist/core/envelope/json.js.map +1 -1
- package/dist/core/explain/families.js +17 -12
- package/dist/core/explain/families.js.map +1 -1
- package/dist/core/explain/refusals.js +3 -3
- package/dist/core/explain/refusals.js.map +1 -1
- package/dist/core/explain/terms.js +18 -9
- package/dist/core/explain/terms.js.map +1 -1
- package/dist/core/explore/explore.js +2 -7
- package/dist/core/explore/explore.js.map +1 -1
- package/dist/core/fleet-context.js +34 -95
- package/dist/core/fleet-context.js.map +1 -1
- package/dist/core/gate/checks.js +10 -3
- package/dist/core/gate/checks.js.map +1 -1
- package/dist/core/gate/partners.js +64 -19
- package/dist/core/gate/partners.js.map +1 -1
- package/dist/core/gate/report.js.map +1 -1
- package/dist/core/gate/verification.js +75 -12
- package/dist/core/gate/verification.js.map +1 -1
- package/dist/core/gherkin/stale.js +13 -37
- package/dist/core/gherkin/stale.js.map +1 -1
- package/dist/core/glossary/tree.js +6 -19
- package/dist/core/glossary/tree.js.map +1 -1
- package/dist/core/kernel/concurrency.js +19 -0
- package/dist/core/kernel/concurrency.js.map +1 -1
- package/dist/core/kernel/records.js +11 -0
- package/dist/core/kernel/records.js.map +1 -1
- package/dist/core/links/backlinks.js +1 -3
- package/dist/core/links/backlinks.js.map +1 -1
- package/dist/core/links/corpus.js +5 -36
- package/dist/core/links/corpus.js.map +1 -1
- package/dist/core/mcp/argv.js +101 -0
- package/dist/core/mcp/argv.js.map +1 -1
- package/dist/core/mcp/author-tools.js +2 -1
- package/dist/core/mcp/author-tools.js.map +1 -1
- package/dist/core/mcp/protocol.js +2 -2
- package/dist/core/mcp/protocol.js.map +1 -1
- package/dist/core/mcp/tools.js +43 -33
- package/dist/core/mcp/tools.js.map +1 -1
- package/dist/core/openapi/baseline/gate.js +53 -12
- package/dist/core/openapi/baseline/gate.js.map +1 -1
- package/dist/core/openapi/baseline/plan.js +22 -8
- package/dist/core/openapi/baseline/plan.js.map +1 -1
- package/dist/core/openapi/baseline/record.js +0 -1
- package/dist/core/openapi/baseline/record.js.map +1 -1
- package/dist/core/openapi/contract-text.js +33 -0
- package/dist/core/openapi/contract-text.js.map +1 -0
- package/dist/core/openapi/contract-tree.js +56 -0
- package/dist/core/openapi/contract-tree.js.map +1 -0
- package/dist/core/openapi/depth.js +38 -24
- package/dist/core/openapi/depth.js.map +1 -1
- package/dist/core/openapi/digest.js +52 -2
- package/dist/core/openapi/digest.js.map +1 -1
- package/dist/core/openapi/doc.js +12 -4
- package/dist/core/openapi/doc.js.map +1 -1
- package/dist/core/openapi/merge/components.js +25 -60
- package/dist/core/openapi/merge/components.js.map +1 -1
- package/dist/core/openapi/merge/error.js +3 -13
- package/dist/core/openapi/merge/error.js.map +1 -1
- package/dist/core/openapi/merge/markers.js +26 -30
- package/dist/core/openapi/merge/markers.js.map +1 -1
- package/dist/core/openapi/merge/merge.js +26 -52
- package/dist/core/openapi/merge/merge.js.map +1 -1
- package/dist/core/openapi/merge/pin.js +15 -35
- package/dist/core/openapi/merge/pin.js.map +1 -1
- package/dist/core/openspec/decide/verdict.js.map +1 -1
- package/dist/core/openspec/inventory.js +4 -5
- package/dist/core/openspec/inventory.js.map +1 -1
- package/dist/core/openspec/model/mapping.js +30 -28
- package/dist/core/openspec/model/mapping.js.map +1 -1
- package/dist/core/openspec/model/model.js +12 -7
- package/dist/core/openspec/model/model.js.map +1 -1
- package/dist/core/openspec/scan/changes.js +10 -4
- package/dist/core/openspec/scan/changes.js.map +1 -1
- package/dist/core/openspec/scan/config.js +7 -16
- package/dist/core/openspec/scan/config.js.map +1 -1
- package/dist/core/openspec/scan/shape.js +63 -19
- package/dist/core/openspec/scan/shape.js.map +1 -1
- package/dist/core/openspec/scan/walk.js +21 -38
- package/dist/core/openspec/scan/walk.js.map +1 -1
- package/dist/core/pack/living.js +2 -16
- package/dist/core/pack/living.js.map +1 -1
- package/dist/core/provenance/findings.js +48 -12
- package/dist/core/provenance/findings.js.map +1 -1
- package/dist/core/provenance/git.js +33 -57
- package/dist/core/provenance/git.js.map +1 -1
- package/dist/core/provenance/gitq/moves.js +71 -114
- package/dist/core/provenance/gitq/moves.js.map +1 -1
- package/dist/core/provenance/gitq/vouched-ancestor.js +2 -2
- package/dist/core/provenance/gitq/vouched-ancestor.js.map +1 -1
- package/dist/core/provenance/sources.js +23 -6
- package/dist/core/provenance/sources.js.map +1 -1
- package/dist/core/provenance/stamp.js +5 -17
- package/dist/core/provenance/stamp.js.map +1 -1
- package/dist/core/repo/entries.js.map +1 -1
- package/dist/core/repo/paths.js +34 -29
- package/dist/core/repo/paths.js.map +1 -1
- package/dist/core/repo/repo.js +24 -14
- package/dist/core/repo/repo.js.map +1 -1
- package/dist/core/repo/tree/find.js +12 -3
- package/dist/core/repo/tree/find.js.map +1 -1
- package/dist/core/repo/tree/fs.js +27 -1
- package/dist/core/repo/tree/fs.js.map +1 -1
- package/dist/core/repo/tree/render/body.js +45 -6
- package/dist/core/repo/tree/render/body.js.map +1 -1
- package/dist/core/repo/tree/render/service/header.js +125 -0
- package/dist/core/repo/tree/render/service/header.js.map +1 -0
- package/dist/core/repo/tree/render/service/peers.js +28 -0
- package/dist/core/repo/tree/render/service/peers.js.map +1 -0
- package/dist/core/repo/tree/render/service/view-id.js +67 -0
- package/dist/core/repo/tree/render/service/view-id.js.map +1 -0
- package/dist/core/repo/tree/render/service/views.js +78 -0
- package/dist/core/repo/tree/render/service/views.js.map +1 -0
- package/dist/core/repo/tree/render/stale.js +19 -2
- package/dist/core/repo/tree/render/stale.js.map +1 -1
- package/dist/core/repo/tree/render/views.js +102 -33
- package/dist/core/repo/tree/render/views.js.map +1 -1
- package/dist/core/repo/tree/walk.js +8 -30
- package/dist/core/repo/tree/walk.js.map +1 -1
- package/dist/core/results.js +47 -12
- package/dist/core/results.js.map +1 -1
- package/dist/core/staging/commit.js +10 -12
- package/dist/core/staging/commit.js.map +1 -1
- package/dist/core/staging/lock.js +74 -12
- package/dist/core/staging/lock.js.map +1 -1
- package/dist/core/staging/recovery/intent.js +2 -5
- package/dist/core/staging/recovery/intent.js.map +1 -1
- package/dist/core/staging/recovery/recover.js +60 -1
- package/dist/core/staging/recovery/recover.js.map +1 -1
- package/dist/core/staging/snapshot/manifest.js +152 -0
- package/dist/core/staging/snapshot/manifest.js.map +1 -0
- package/dist/core/staging/{snapshot.js → snapshot/snapshot.js} +10 -34
- package/dist/core/staging/snapshot/snapshot.js.map +1 -0
- package/dist/core/status/actions/execution.js +11 -0
- package/dist/core/status/actions/execution.js.map +1 -1
- package/dist/core/status/feature/artifacts.js +2 -2
- package/dist/core/status/feature/artifacts.js.map +1 -1
- package/dist/core/status/feature/feature.js +5 -11
- package/dist/core/status/feature/feature.js.map +1 -1
- package/dist/core/status/fleet/fleet.js +6 -2
- package/dist/core/status/fleet/fleet.js.map +1 -1
- package/dist/core/status/fleet/next.js +26 -3
- package/dist/core/status/fleet/next.js.map +1 -1
- package/dist/core/status/report.js.map +1 -1
- package/dist/core/status/verification.js +30 -15
- package/dist/core/status/verification.js.map +1 -1
- package/dist/core/tasks/packet.js +174 -0
- package/dist/core/tasks/packet.js.map +1 -0
- package/dist/core/tasks/record.js +43 -0
- package/dist/core/tasks/record.js.map +1 -0
- package/dist/core/tasks/sources.js +181 -0
- package/dist/core/tasks/sources.js.map +1 -0
- package/dist/core/tasks/stamps.js +76 -0
- package/dist/core/tasks/stamps.js.map +1 -0
- package/dist/core/tasks/unit.js +0 -0
- package/dist/core/tasks/unit.js.map +1 -0
- package/dist/core/usecases/capability.js +11 -14
- package/dist/core/usecases/capability.js.map +1 -1
- package/dist/core/usecases/delta/flows.js +5 -7
- package/dist/core/usecases/delta/flows.js.map +1 -1
- package/dist/core/usecases/fleet.js +178 -31
- package/dist/core/usecases/fleet.js.map +1 -1
- package/dist/core/usecases/operations.js +5 -4
- package/dist/core/usecases/operations.js.map +1 -1
- package/dist/core/usecases/service/flows.js +9 -6
- package/dist/core/usecases/service/flows.js.map +1 -1
- package/dist/core/usecases/suites/participants.js +83 -0
- package/dist/core/usecases/suites/participants.js.map +1 -0
- package/dist/core/usecases/suites/suites.js +45 -0
- package/dist/core/usecases/suites/suites.js.map +1 -0
- package/dist/core/verify/answers.js +12 -5
- package/dist/core/verify/answers.js.map +1 -1
- package/dist/core/verify/build.js +102 -82
- package/dist/core/verify/build.js.map +1 -1
- package/dist/core/verify/checklist.js +45 -10
- package/dist/core/verify/checklist.js.map +1 -1
- package/dist/core/verify/location.js +67 -0
- package/dist/core/verify/location.js.map +1 -0
- package/dist/core/verify/pins/lint.js +3 -2
- package/dist/core/verify/pins/lint.js.map +1 -1
- package/dist/core/verify/pins/pin.js +1 -1
- package/dist/core/verify/pins/pin.js.map +1 -1
- package/dist/core/verify/read/file.js +224 -0
- package/dist/core/verify/read/file.js.map +1 -0
- package/dist/core/verify/{file.js → read/shape.js} +16 -104
- package/dist/core/verify/read/shape.js.map +1 -0
- package/dist/core/verify/read/union.js +105 -0
- package/dist/core/verify/read/union.js.map +1 -0
- package/dist/core/verify/record.js +35 -12
- package/dist/core/verify/record.js.map +1 -1
- package/dist/core/verify/store/commit.js +40 -30
- package/dist/core/verify/store/commit.js.map +1 -1
- package/dist/core/verify/store/render.js +8 -4
- package/dist/core/verify/store/render.js.map +1 -1
- package/dist/core/vocabulary/issue.js +1 -0
- package/dist/core/vocabulary/issue.js.map +1 -1
- package/dist/core/vocabulary/maturity.js +53 -3
- package/dist/core/vocabulary/maturity.js.map +1 -1
- package/examples/README.md +28 -9
- package/examples/docs/architecture/landscape.likec4 +21 -7
- package/examples/docs/architecture/services.likec4 +70 -0
- package/examples/docs/architecture/subsystems.likec4 +3 -0
- package/examples/docs/architecture/usecases/order-notification.likec4 +6 -2
- package/examples/docs/features/FEAT-101-payment-splitting/specs/payment-service/asyncapi.yaml +11 -8
- package/examples/docs/features/FEAT-101-payment-splitting/specs/payment-split-service/openapi.yaml +6 -6
- package/examples/docs/features/archive/FEAT-088-refunds/specs/payment-service/openapi.yaml +13 -10
- package/examples/docs/services/order-service/asyncapi.yaml +3 -2
- package/examples/docs/services/order-service/model.likec4 +10 -8
- package/examples/docs/services/payment-service/model.likec4 +6 -4
- package/examples/docs/services/platform/identity-service/model.likec4 +9 -7
- package/examples/docs/services/platform/notification-service/asyncapi.yaml +2 -1
- package/examples/docs/services/platform/notification-service/model.likec4 +4 -5
- package/package.json +1 -1
- package/dist/commands/validate/checks/vocabulary.js +0 -16
- package/dist/commands/validate/checks/vocabulary.js.map +0 -1
- package/dist/core/agent/agents-md/artifacts.js +0 -221
- package/dist/core/agent/agents-md/artifacts.js.map +0 -1
- package/dist/core/agent/agents-md/cycle.js +0 -180
- package/dist/core/agent/agents-md/cycle.js.map +0 -1
- package/dist/core/agent/agents-md/shipped/archive-gate.js +0 -98
- package/dist/core/agent/agents-md/shipped/archive-gate.js.map +0 -1
- package/dist/core/agent/agents-md/spine.js +0 -40
- package/dist/core/agent/agents-md/spine.js.map +0 -1
- package/dist/core/coherence/events/merged.js +0 -40
- package/dist/core/coherence/events/merged.js.map +0 -1
- package/dist/core/diff/current-state.js +0 -96
- package/dist/core/diff/current-state.js.map +0 -1
- package/dist/core/diff/victims.js +0 -256
- package/dist/core/diff/victims.js.map +0 -1
- package/dist/core/staging/snapshot.js.map +0 -1
- package/dist/core/verify/file.js.map +0 -1
|
@@ -12,10 +12,9 @@ import { serviceResolver } from "../../core/c4/resolve/service.js";
|
|
|
12
12
|
import { maturityGaps, serviceMaturity, } from "../../core/vocabulary/maturity.js";
|
|
13
13
|
import {} from "../../core/repo/entries.js";
|
|
14
14
|
import { landscapePath, servicePathsAt } from "../../core/repo/paths.js";
|
|
15
|
-
import {
|
|
16
|
-
import { readVerification } from "../../core/verify/file.js";
|
|
15
|
+
import { verificationState } from "../../core/status/verification.js";
|
|
17
16
|
import {} from "../../core/verify/answers.js";
|
|
18
|
-
import {
|
|
17
|
+
import {} from "../../core/verify/record.js";
|
|
19
18
|
/**
|
|
20
19
|
* Read the living landscape ONCE and decide, per service, whether an API is
|
|
21
20
|
* expected of it. `serviceResolver` is handed the real service ids so an edge
|
|
@@ -70,40 +69,36 @@ export async function serviceViews(docsDir, services, boundService, preloaded) {
|
|
|
70
69
|
});
|
|
71
70
|
}
|
|
72
71
|
/**
|
|
73
|
-
* The verification column
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
72
|
+
* The verification column: `status`'s own record state (`verificationState`),
|
|
73
|
+
* projected onto the cell — the record read, the files added up against the
|
|
74
|
+
* checklist just derived, the archived record read as frozen history — so
|
|
75
|
+
* `list`, `status` and `gate` compute one cell, for the reasons that function
|
|
76
|
+
* records. Reading the record is cheap but no longer one file read — a
|
|
77
|
+
* directory listing and one small read per attesting service, plus the
|
|
78
|
+
* one-file `verification.yaml` — and the checklist is derived only when there
|
|
79
|
+
* is a record to judge: on a fleet-sized repo most features have none, and
|
|
80
|
+
* deriving N checklists to print N dashes is the cost that would get this
|
|
81
|
+
* column dropped. `fleet` is the invocation's read index, so each derivation's
|
|
82
|
+
* enumerations and delta parse are memo hits rather than a walk per feature.
|
|
77
83
|
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
84
|
+
* A record that cannot be read is one dash like an absent one: this column
|
|
85
|
+
* shows one glyph either way. That includes a record whose summary
|
|
86
|
+
* contradicts its own claims (`verify.record-miscounted`), so no count from it
|
|
87
|
+
* is printed as fact — reading it as absent is the safe half of the truth, and
|
|
88
|
+
* `status` and `verify`, which can say why, report it as unreadable instead.
|
|
82
89
|
*/
|
|
83
|
-
export async function featureVerification(docsDir, f) {
|
|
84
|
-
const
|
|
85
|
-
if (
|
|
90
|
+
export async function featureVerification(docsDir, f, fleet) {
|
|
91
|
+
const { state: s } = await verificationState(docsDir, f, { fleet });
|
|
92
|
+
if (s.state === "absent" || s.state === "unreadable" || s.recorded === null)
|
|
86
93
|
return null;
|
|
87
|
-
const stale = f.archived
|
|
88
|
-
? false
|
|
89
|
-
: (await featureChecklist(docsDir, f.dir, f.id)).digest !== v.checklist;
|
|
90
|
-
const tally = tallyRecord(v);
|
|
91
94
|
return {
|
|
92
|
-
state:
|
|
93
|
-
recorded:
|
|
94
|
-
verdict:
|
|
95
|
-
confirmed:
|
|
96
|
-
claims:
|
|
97
|
-
attested:
|
|
98
|
-
answered:
|
|
95
|
+
state: s.state,
|
|
96
|
+
recorded: s.recorded,
|
|
97
|
+
verdict: s.verdict,
|
|
98
|
+
confirmed: s.confirmed,
|
|
99
|
+
claims: s.claims,
|
|
100
|
+
attested: s.attested,
|
|
101
|
+
answered: s.answered,
|
|
99
102
|
};
|
|
100
103
|
}
|
|
101
|
-
/* ------------------------------------------------------------------ */
|
|
102
|
-
/* Text */
|
|
103
|
-
/* ------------------------------------------------------------------ */
|
|
104
|
-
/**
|
|
105
|
-
* Fixed-width presence flags. `a` and `A` are deliberately the same letter in
|
|
106
|
-
* two cases: the arch spec and the API are the two things a reader most often
|
|
107
|
-
* mistakes for each other, and one is lowercase because it is optional.
|
|
108
|
-
*/
|
|
109
104
|
//# sourceMappingURL=views.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"views.js","sourceRoot":"","sources":["../../../src/commands/list/views.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * What the list actually reads: one row per service, one verification cell per\n * feature.\n *\n * Both are gathered before anything is printed or serialized, because the same\n * numbers appear in `--json` and in the worklist footer — a footer computed\n * from a second read is a footer that can contradict the rows above it.\n */\nimport { existsSync } from \"node:fs\";\nimport { loadFile, type LoadedDoc } from \"../../core/c4/likec4.js\";\nimport { serviceResolver } from \"../../core/c4/resolve/service.js\";\nimport {\n maturityGaps,\n serviceMaturity,\n type Maturity,\n type MaturityInput,\n} from \"../../core/vocabulary/maturity.js\";\nimport { type FeatureEntry, type ServiceEntry } from \"../../core/repo/entries.js\";\nimport { landscapePath, servicePathsAt } from \"../../core/repo/paths.js\";\nimport { featureChecklist } from \"../../core/verify/checklist.js\";\nimport { readVerification } from \"../../core/verify/file.js\";\nimport { type AnsweredBy } from \"../../core/verify/answers.js\";\nimport {\n confirmedProvenance,\n tallyRecord,\n type VerificationVerdict,\n verificationVerdict,\n} from \"../../core/verify/record.js\";\nimport type { DocsDir } from \"../../core/kernel/ids/dirs.js\";\n\nexport interface ServiceView extends MaturityInput {\n /** True when this service's `sources` can only be resolved from its own repo. */\n unverifiableFromHere: boolean;\n maturity: Maturity;\n /** What stands between this service and the next rung, in fix order. */\n missing: string[];\n}\n\n/**\n * Read the living landscape ONCE and decide, per service, whether an API is\n * expected of it. `serviceResolver` is handed the real service ids so an edge\n * drawn into a modelled container (`paymentService.api`) counts for the service\n * that owns it — the same resolution `validate` uses, so the two commands\n * cannot disagree about who is called.\n */\nexport async function serviceViews(\n docsDir: DocsDir,\n services: ServiceEntry[],\n boundService: string | undefined,\n preloaded?: LoadedDoc | null,\n): Promise<ServiceView[]> {\n const known = new Set(services.map((s) => s.id));\n const lp = landscapePath(docsDir);\n // `preloaded` is the landscape a caller already holds — `undefined` means\n // \"load it if it exists\", null \"there is none\", the convention\n // validateService's own `preloaded` follows. The fleet scorecard hands in\n // the batch-parsed doc because a healthy `validate --all` must spin ZERO\n // per-document workspaces (test/validate-batch-fallback.test.ts counts\n // them), and a second parse of a document that run already holds was the\n // exact per-path cost the batch loader exists to retire.\n const land = preloaded !== undefined ? preloaded : existsSync(lp) ? await loadFile(lp) : null;\n const called = new Set<string>();\n // Positive evidence only: with no landscape, or one that does not parse, we\n // know nothing about who is called and every service is assumed to have an\n // API — the conservative direction, because claiming a service needs no\n // contract is how a real one goes undocumented. This is the SAME rule\n // `landscapeEvidence` (core/vocabulary/maturity.ts) spells per service for\n // explore and the context pack — derived fleet-wide here in one pass (one\n // `called` set for every service) because calling the per-service partition\n // from this loop would re-walk the whole relationship list once per\n // service. If the rule moves, both spellings move.\n const proven = land !== null && land.errors.length === 0;\n if (proven) {\n const svcOf = serviceResolver(land.elements, known);\n for (const r of land.relationships) {\n // …including its self-edge rule: an edge whose two endpoints resolve to\n // one service is that service's own internal wiring, and counting it made\n // a service nobody calls owe an OpenAPI contract — `partial` here while\n // `loam context` (which reads the shared derivation) said `documented`.\n if (r.op !== undefined && svcOf(r.source) !== svcOf(r.target)) called.add(svcOf(r.target));\n }\n }\n\n return services.map((entry) => {\n const archSpec = existsSync(servicePathsAt(entry.dir).archSpec);\n const apiExpected = !proven || called.has(entry.id);\n const view = {\n entry,\n archSpec,\n apiExpected,\n unverifiableFromHere: entry.sources.declared && boundService !== entry.id,\n };\n const missing = maturityGaps(view);\n return { ...view, maturity: serviceMaturity(view), missing };\n });\n}\n\n/* ------------------------------------------------------------------ */\n/* JSON */\n/* ------------------------------------------------------------------ */\n\nexport interface VerificationCell {\n state: \"recorded\" | \"stale\";\n /** The day the record was written. */\n recorded: string;\n /**\n * `verify --json`'s own three-valued verdict, through `verificationVerdict` —\n * not a fourth opinion computed here. `attested` is the one this column got\n * wrong: a scenario confirmed on an agent's word printed as `11/11` under a\n * heading that said `verified`, and `list --json` is how the fleet is graded.\n */\n verdict: VerificationVerdict;\n /** Both counts are `tallyRecord`'s — the record's `claims[]`, never its `summary:` block. */\n confirmed: number;\n claims: number;\n /** Of the confirmed, the scenario claims on an agent's word (`attestedClaims`). */\n attested: number;\n /**\n * Of the confirmed, one count per `answered_by` value (`confirmedProvenance`), every key\n * present. A different question from `attested` above, which is why both are here: `attested`\n * asks only about `scenario.tested` because that is what the verdict turns on, while this asks\n * who answered EVERY kind — and reading the first as if it were the second is how the fleet\n * scorecard credited agent-answered permission and operation claims to a test runner.\n */\n answered: Record<AnsweredBy, number>;\n}\n\n/**\n * The verification column. `readVerification` is one file read; the checklist\n * is derived only when there is a record to judge — on a fleet-sized repo most\n * features have none, and deriving N checklists to print N dashes is the cost\n * that would get this column dropped.\n *\n * An archived feature's record is frozen history (see `core/verify/record.ts`): archive\n * merged the feature's operations into the living openapi, so a re-derived\n * checklist can only mismatch — the record is reported as recorded, never\n * judged stale.\n */\nexport async function featureVerification(\n docsDir: DocsDir,\n f: FeatureEntry,\n): Promise<VerificationCell | null> {\n const v = await readVerification(f.dir);\n if (v === null) return null;\n const stale = f.archived\n ? false\n : (await featureChecklist(docsDir, f.dir, f.id)).digest !== v.checklist;\n const tally = tallyRecord(v);\n return {\n state: stale ? \"stale\" : \"recorded\",\n recorded: v.recorded,\n verdict: verificationVerdict(tally, stale),\n confirmed: tally.confirmed,\n claims: tally.claims,\n attested: tally.attested,\n answered: confirmedProvenance(v.claims),\n };\n}\n\n/* ------------------------------------------------------------------ */\n/* Text */\n/* ------------------------------------------------------------------ */\n\n/**\n * Fixed-width presence flags. `a` and `A` are deliberately the same letter in\n * two cases: the arch spec and the API are the two things a reader most often\n * mistakes for each other, and one is lowercase because it is optional.\n */\n"]}
|
|
1
|
+
{"version":3,"file":"views.js","sourceRoot":"","sources":["../../../src/commands/list/views.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * What the list actually reads: one row per service, one verification cell per\n * feature.\n *\n * Both are gathered before anything is printed or serialized, because the same\n * numbers appear in `--json` and in the worklist footer — a footer computed\n * from a second read is a footer that can contradict the rows above it.\n */\nimport { existsSync } from \"node:fs\";\nimport { loadFile, type LoadedDoc } from \"../../core/c4/likec4.js\";\nimport { serviceResolver } from \"../../core/c4/resolve/service.js\";\nimport {\n maturityGaps,\n serviceMaturity,\n type Maturity,\n type MaturityInput,\n} from \"../../core/vocabulary/maturity.js\";\nimport { type FeatureEntry, type ServiceEntry } from \"../../core/repo/entries.js\";\nimport { landscapePath, servicePathsAt } from \"../../core/repo/paths.js\";\nimport type { FleetContext } from \"../../core/fleet-context.js\";\nimport { verificationState } from \"../../core/status/verification.js\";\nimport { type AnsweredBy } from \"../../core/verify/answers.js\";\nimport { type VerificationVerdict } from \"../../core/verify/record.js\";\nimport type { DocsDir } from \"../../core/kernel/ids/dirs.js\";\n\nexport interface ServiceView extends MaturityInput {\n /** True when this service's `sources` can only be resolved from its own repo. */\n unverifiableFromHere: boolean;\n maturity: Maturity;\n /**\n * What stands between this service and the next rung, in fix order — and at\n * `vouched`, the re-vouch a document that moved under its stamp owes\n * (`maturityGaps`). Empty exactly when nothing is owed, which is the\n * worklist's membership test.\n */\n missing: string[];\n}\n\n/**\n * Read the living landscape ONCE and decide, per service, whether an API is\n * expected of it. `serviceResolver` is handed the real service ids so an edge\n * drawn into a modelled container (`paymentService.api`) counts for the service\n * that owns it — the same resolution `validate` uses, so the two commands\n * cannot disagree about who is called.\n */\nexport async function serviceViews(\n docsDir: DocsDir,\n services: ServiceEntry[],\n boundService: string | undefined,\n preloaded?: LoadedDoc | null,\n): Promise<ServiceView[]> {\n const known = new Set(services.map((s) => s.id));\n const lp = landscapePath(docsDir);\n // `preloaded` is the landscape a caller already holds — `undefined` means\n // \"load it if it exists\", null \"there is none\", the convention\n // validateService's own `preloaded` follows. The fleet scorecard hands in\n // the batch-parsed doc because a healthy `validate --all` must spin ZERO\n // per-document workspaces (test/validate-batch-fallback.test.ts counts\n // them), and a second parse of a document that run already holds was the\n // exact per-path cost the batch loader exists to retire.\n const land = preloaded !== undefined ? preloaded : existsSync(lp) ? await loadFile(lp) : null;\n const called = new Set<string>();\n // Positive evidence only: with no landscape, or one that does not parse, we\n // know nothing about who is called and every service is assumed to have an\n // API — the conservative direction, because claiming a service needs no\n // contract is how a real one goes undocumented. This is the SAME rule\n // `landscapeEvidence` (core/vocabulary/maturity.ts) spells per service for\n // explore and the context pack — derived fleet-wide here in one pass (one\n // `called` set for every service) because calling the per-service partition\n // from this loop would re-walk the whole relationship list once per\n // service. If the rule moves, both spellings move.\n const proven = land !== null && land.errors.length === 0;\n if (proven) {\n const svcOf = serviceResolver(land.elements, known);\n for (const r of land.relationships) {\n // …including its self-edge rule: an edge whose two endpoints resolve to\n // one service is that service's own internal wiring, and counting it made\n // a service nobody calls owe an OpenAPI contract — `partial` here while\n // `loam context` (which reads the shared derivation) said `documented`.\n if (r.op !== undefined && svcOf(r.source) !== svcOf(r.target)) called.add(svcOf(r.target));\n }\n }\n\n return services.map((entry) => {\n const archSpec = existsSync(servicePathsAt(entry.dir).archSpec);\n const apiExpected = !proven || called.has(entry.id);\n const view = {\n entry,\n archSpec,\n apiExpected,\n unverifiableFromHere: entry.sources.declared && boundService !== entry.id,\n };\n const missing = maturityGaps(view);\n return { ...view, maturity: serviceMaturity(view), missing };\n });\n}\n\n/** What the features table says about verification without N `loam verify` runs. */\nexport interface VerificationCell {\n state: \"recorded\" | \"stale\";\n /** The day the record was written. */\n recorded: string;\n /**\n * `verify --json`'s own three-valued verdict, through `verificationVerdict` —\n * not a fourth opinion computed here. `attested` is the one this column got\n * wrong: a scenario confirmed on an agent's word printed as `11/11` under a\n * heading that said `verified`, and `list --json` is how the fleet is graded.\n */\n verdict: VerificationVerdict;\n /** Both counts are `tallyRecord`'s — the record's `claims[]`, never its `summary:` block. */\n confirmed: number;\n claims: number;\n /** Of the confirmed, the scenario claims on an agent's word (`attestedClaims`). */\n attested: number;\n /**\n * Of the confirmed, one count per `answered_by` value (`confirmedProvenance`), every key\n * present. A different question from `attested` above, which is why both are here: `attested`\n * asks only about `scenario.tested` because that is what the verdict turns on, while this asks\n * who answered EVERY kind — and reading the first as if it were the second is how the fleet\n * scorecard credited agent-answered permission and operation claims to a test runner.\n */\n answered: Record<AnsweredBy, number>;\n}\n\n/**\n * The verification column: `status`'s own record state (`verificationState`),\n * projected onto the cell — the record read, the files added up against the\n * checklist just derived, the archived record read as frozen history — so\n * `list`, `status` and `gate` compute one cell, for the reasons that function\n * records. Reading the record is cheap but no longer one file read — a\n * directory listing and one small read per attesting service, plus the\n * one-file `verification.yaml` — and the checklist is derived only when there\n * is a record to judge: on a fleet-sized repo most features have none, and\n * deriving N checklists to print N dashes is the cost that would get this\n * column dropped. `fleet` is the invocation's read index, so each derivation's\n * enumerations and delta parse are memo hits rather than a walk per feature.\n *\n * A record that cannot be read is one dash like an absent one: this column\n * shows one glyph either way. That includes a record whose summary\n * contradicts its own claims (`verify.record-miscounted`), so no count from it\n * is printed as fact — reading it as absent is the safe half of the truth, and\n * `status` and `verify`, which can say why, report it as unreadable instead.\n */\nexport async function featureVerification(\n docsDir: DocsDir,\n f: FeatureEntry,\n fleet: FleetContext,\n): Promise<VerificationCell | null> {\n const { state: s } = await verificationState(docsDir, f, { fleet });\n if (s.state === \"absent\" || s.state === \"unreadable\" || s.recorded === null) return null;\n return {\n state: s.state,\n recorded: s.recorded,\n verdict: s.verdict,\n confirmed: s.confirmed,\n claims: s.claims,\n attested: s.attested,\n answered: s.answered,\n };\n}\n"]}
|
|
@@ -31,11 +31,13 @@ import { registerStatus } from "../status/status.js";
|
|
|
31
31
|
import { registerList } from "../list/list.js";
|
|
32
32
|
import { registerShow } from "../show/show.js";
|
|
33
33
|
import { registerDelta } from "../delta/delta.js";
|
|
34
|
+
import { registerTasks } from "../tasks/tasks.js";
|
|
34
35
|
import { registerDiff } from "../diff/diff.js";
|
|
35
36
|
import { registerExplore } from "../explore.js";
|
|
36
37
|
import { registerDependencies } from "../dependencies.js";
|
|
37
38
|
import { registerDoctor } from "../doctor.js";
|
|
38
39
|
import { registerContext } from "../context/context.js";
|
|
40
|
+
import { registerAdopt } from "../adopt/adopt.js";
|
|
39
41
|
import { registerGate } from "../gate/gate.js";
|
|
40
42
|
import { registerExplain } from "../explain/explain.js";
|
|
41
43
|
import { registerInstructions } from "../instructions.js";
|
|
@@ -44,6 +46,7 @@ import { registerNew } from "../new/new.js";
|
|
|
44
46
|
import { registerRebase } from "../rebase/rebase.js";
|
|
45
47
|
import { registerGherkin } from "../gherkin/gherkin.js";
|
|
46
48
|
import { registerArchive } from "../archive/archive.js";
|
|
49
|
+
import { MCP_TOOLS } from "../../core/mcp/tools.js";
|
|
47
50
|
/**
|
|
48
51
|
* Every console method Node's Console writes to STDOUT — the protocol stream,
|
|
49
52
|
* where one stray line corrupts the transport. Hooking only `log` was the
|
|
@@ -93,13 +96,80 @@ export function captureConsole(sinks) {
|
|
|
93
96
|
/** Commander output codes whose printed output is the point, not a usage failure. */
|
|
94
97
|
const PASS_THROUGH = new Set(["commander.help", "commander.helpDisplayed", "commander.version"]);
|
|
95
98
|
/**
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
99
|
+
* Every command a tool table may name, keyed by the `command` its tool carries.
|
|
100
|
+
* A LOOKUP, not a roster: which of these a call's program registers is decided
|
|
101
|
+
* by the active tool table alone (`toolRunner`), so the authoring writers —
|
|
102
|
+
* new, rebase, gherkin, archive — exist in it only when `--author` put their
|
|
103
|
+
* tools in the table, and the table the read-only hints describe is the one
|
|
104
|
+
* place that gate is written.
|
|
105
|
+
*
|
|
106
|
+
* `instructions` and `steps` read no config at all, and are in MCP_TOOLS
|
|
107
|
+
* because an agent meets them BEFORE the repository is wired: every generated
|
|
108
|
+
* skill's first line points at `loam instructions <workflow>`. They are why
|
|
109
|
+
* registration follows the table. This list and MCP_TOOLS used to be two
|
|
110
|
+
* copies of one roster, and nothing counted them against each other until a
|
|
111
|
+
* tool advertised in `tools/list` and absent here answered a `tools/call` with
|
|
112
|
+
* `unknown command 'instructions'` — an envelope that reads as the caller's
|
|
113
|
+
* mistake. `test/mcp-serve.test.ts` calls every advertised tool, and checks
|
|
114
|
+
* this lookup against both tables directly.
|
|
115
|
+
*/
|
|
116
|
+
const REGISTRARS = new Map([
|
|
117
|
+
["validate", registerValidate],
|
|
118
|
+
["status", registerStatus],
|
|
119
|
+
["list", registerList],
|
|
120
|
+
["show", registerShow],
|
|
121
|
+
["delta", registerDelta],
|
|
122
|
+
["tasks", registerTasks],
|
|
123
|
+
["diff", registerDiff],
|
|
124
|
+
["explore", registerExplore],
|
|
125
|
+
["dependencies", registerDependencies],
|
|
126
|
+
["doctor", registerDoctor],
|
|
127
|
+
["context", registerContext],
|
|
128
|
+
["adopt", registerAdopt],
|
|
129
|
+
["gate", registerGate],
|
|
130
|
+
["explain", registerExplain],
|
|
131
|
+
["instructions", registerInstructions],
|
|
132
|
+
["steps", registerSteps],
|
|
133
|
+
["new", registerNew],
|
|
134
|
+
["rebase", registerRebase],
|
|
135
|
+
["gherkin", registerGherkin],
|
|
136
|
+
["archive", registerArchive],
|
|
137
|
+
]);
|
|
138
|
+
/**
|
|
139
|
+
* Resolve every command `tools` name to its registrar, or throw naming each
|
|
140
|
+
* one that has none. This is the server's STARTUP check, and it runs once per
|
|
141
|
+
* table: `toolRunner` calls it before the first frame is read, so a tool
|
|
142
|
+
* advertised with no registrar — the roster drift above, arriving again —
|
|
143
|
+
* stops `loam mcp` at launch, through cli.ts's catch like any other throw,
|
|
144
|
+
* before a client has been promised anything. It used to run inside every call's program build, where one
|
|
145
|
+
* missing entry turned EVERY `tools/call` into an `internal` envelope naming
|
|
146
|
+
* it, the tools that were wired correctly included: a call's outcome
|
|
147
|
+
* depended on a tool it never named.
|
|
148
|
+
*/
|
|
149
|
+
export function resolveRegistrars(tools) {
|
|
150
|
+
const resolved = [];
|
|
151
|
+
const missing = [];
|
|
152
|
+
for (const command of new Set(tools.map((tool) => tool.command))) {
|
|
153
|
+
const register = REGISTRARS.get(command);
|
|
154
|
+
if (register === undefined)
|
|
155
|
+
missing.push(`'${command}'`);
|
|
156
|
+
else
|
|
157
|
+
resolved.push(register);
|
|
158
|
+
}
|
|
159
|
+
if (missing.length > 0) {
|
|
160
|
+
throw new Error(`loam mcp: no command registered for MCP tool command ${missing.join(", ")} — add it to REGISTRARS in src/commands/mcp/dispatch.ts`);
|
|
161
|
+
}
|
|
162
|
+
return resolved;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* The scoped program, output routed into `sinks`, holding exactly the commands
|
|
166
|
+
* the resolved table names. configureOutput and exitOverride are set BEFORE the
|
|
167
|
+
* register calls on purpose: commander copies inherited settings onto a
|
|
168
|
+
* subcommand when it is created, so settings applied after registration would
|
|
169
|
+
* leave every subcommand writing to the real stdout — which for an MCP server
|
|
170
|
+
* is the protocol stream.
|
|
101
171
|
*/
|
|
102
|
-
function readProgram(sinks,
|
|
172
|
+
function readProgram(sinks, registrars) {
|
|
103
173
|
const program = new Command();
|
|
104
174
|
program.name("loam");
|
|
105
175
|
program.exitOverride();
|
|
@@ -107,49 +177,34 @@ function readProgram(sinks, authoring = false) {
|
|
|
107
177
|
writeOut: (text) => sinks.stdout.push(text),
|
|
108
178
|
writeErr: (text) => sinks.stderr.push(text),
|
|
109
179
|
});
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
registerList(program);
|
|
113
|
-
registerShow(program);
|
|
114
|
-
registerDelta(program);
|
|
115
|
-
registerDiff(program);
|
|
116
|
-
registerExplore(program);
|
|
117
|
-
registerDependencies(program);
|
|
118
|
-
registerDoctor(program);
|
|
119
|
-
registerContext(program);
|
|
120
|
-
registerGate(program);
|
|
121
|
-
registerExplain(program);
|
|
122
|
-
// The two that read no config at all. They are here for the same reason they
|
|
123
|
-
// are in MCP_TOOLS: an agent meets `instructions` and `steps` BEFORE the
|
|
124
|
-
// repository is wired, and every generated skill's first line points at
|
|
125
|
-
// `loam instructions <workflow>`. This list and MCP_TOOLS are two copies of
|
|
126
|
-
// one roster, and nothing counted them against each other until a tool
|
|
127
|
-
// advertised in `tools/list` and absent here answered a `tools/call` with
|
|
128
|
-
// `unknown command 'instructions'` — an envelope that reads as the caller's
|
|
129
|
-
// mistake. `test/mcp-serve.test.ts` now calls every advertised tool.
|
|
130
|
-
registerInstructions(program);
|
|
131
|
-
registerSteps(program);
|
|
132
|
-
if (authoring) {
|
|
133
|
-
registerNew(program);
|
|
134
|
-
registerRebase(program);
|
|
135
|
-
registerGherkin(program);
|
|
136
|
-
registerArchive(program);
|
|
137
|
-
}
|
|
180
|
+
for (const register of registrars)
|
|
181
|
+
register(program);
|
|
138
182
|
return program;
|
|
139
183
|
}
|
|
184
|
+
/**
|
|
185
|
+
* The runner `loam mcp` serves `tools` with. Throws HERE, at construction, when
|
|
186
|
+
* the table names a command with no registrar (`resolveRegistrars`); the
|
|
187
|
+
* runner it returns never throws, and builds each call's program from the
|
|
188
|
+
* registrars resolved once — so no call can fail for the sake of a tool it
|
|
189
|
+
* did not name.
|
|
190
|
+
*/
|
|
191
|
+
export function toolRunner(tools = MCP_TOOLS) {
|
|
192
|
+
const registrars = resolveRegistrars(tools);
|
|
193
|
+
return (argv) => runToolArgv(argv, registrars);
|
|
194
|
+
}
|
|
140
195
|
/**
|
|
141
196
|
* Run one tool argv. Never throws: whatever goes wrong becomes an envelope in
|
|
142
197
|
* `stdout` with `code` 1, so the serve loop's answer to THIS call is the only
|
|
143
198
|
* thing affected — one bad call must never take the server down, and a
|
|
144
199
|
* memoised rejection cannot exist because nothing here outlives the call.
|
|
145
200
|
*/
|
|
146
|
-
|
|
201
|
+
async function runToolArgv(argv, registrars) {
|
|
147
202
|
const sinks = { stdout: [], stderr: [] };
|
|
148
203
|
const previousExitCode = process.exitCode;
|
|
149
204
|
const restoreConsole = captureConsole(sinks);
|
|
150
205
|
process.exitCode = undefined;
|
|
151
206
|
try {
|
|
152
|
-
await readProgram(sinks,
|
|
207
|
+
await readProgram(sinks, registrars).parseAsync(["node", "loam", ...argv]);
|
|
153
208
|
}
|
|
154
209
|
catch (err) {
|
|
155
210
|
if (err instanceof CommanderError) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dispatch.js","sourceRoot":"","sources":["../../../src/commands/mcp/dispatch.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * In-process dispatch for one MCP tool call: build a fresh commander program\n * holding only the commands the selected tool table exposes, run the argv through\n * it, and hand back what the CLI would have printed.\n *\n * This is the proven harness pattern (test/helpers/harness.ts runLoamNow)\n * moved to the commands layer, minus the chdir: the server's own cwd IS the\n * workdir, so every call resolves loam.json exactly as the CLI would in that\n * directory — the server serves ONE repository, the one it started in, and\n * never changes directory. In-process rather than a child per call because a\n * child re-pays LikeC4/Langium startup on every validate, and because in dev\n * there is no built binary to exec.\n *\n * The capture is process-global (console, exitCode), which is safe under\n * exactly one condition the serve loop guarantees: calls are strictly\n * sequential — each one is awaited to completion before the next frame is\n * read — and `parseAsync` resolves only after the action has fully run (the\n * entire test suite rests on that same guarantee). Nothing here may be\n * module-level state: two serve sessions in one process (the tests do this)\n * must not see each other.\n *\n * NOTE for test/agents.test.ts's scan: registration happens through the\n * imported register functions; this module must never contain the literal\n * dot-command-paren call, comments included, or the CLI surface count breaks.\n */\nimport { Command, CommanderError } from \"commander\";\nimport { format } from \"node:util\";\nimport { emitJsonError } from \"../../core/envelope/json.js\";\nimport { registerValidate } from \"../validate/validate.js\";\nimport { registerStatus } from \"../status/status.js\";\nimport { registerList } from \"../list/list.js\";\nimport { registerShow } from \"../show/show.js\";\nimport { registerDelta } from \"../delta/delta.js\";\nimport { registerDiff } from \"../diff/diff.js\";\nimport { registerExplore } from \"../explore.js\";\nimport { registerDependencies } from \"../dependencies.js\";\nimport { registerDoctor } from \"../doctor.js\";\nimport { registerContext } from \"../context/context.js\";\nimport { registerGate } from \"../gate/gate.js\";\nimport { registerExplain } from \"../explain/explain.js\";\nimport { registerInstructions } from \"../instructions.js\";\nimport { registerSteps } from \"../steps/steps.js\";\nimport { registerNew } from \"../new/new.js\";\nimport { registerRebase } from \"../rebase/rebase.js\";\nimport { registerGherkin } from \"../gherkin/gherkin.js\";\nimport { registerArchive } from \"../archive/archive.js\";\n\nexport interface DispatchResult {\n readonly stdout: string;\n readonly stderr: string;\n readonly code: number;\n}\n\n/** Console sinks for one dispatch — a value, not module state, so calls cannot share buffers. */\nexport interface Sinks {\n readonly stdout: string[];\n readonly stderr: string[];\n}\n\n/**\n * Every console method Node's Console writes to STDOUT — the protocol stream,\n * where one stray line corrupts the transport. Hooking only `log` was the\n * reviewed defect here: a `console.info` in any command, or in a dependency a\n * command loads, would have written straight into the client's frame stream\n * with every suite green. `warn`/`error` go to stderr, captured for the\n * server log rather than the wire.\n */\nconst STDOUT_METHODS = [\n \"log\", \"info\", \"debug\", \"dir\", \"dirxml\", \"table\",\n \"group\", \"groupCollapsed\", \"groupEnd\", \"count\", \"countReset\", \"timeLog\", \"timeEnd\",\n] as const;\nconst STDERR_METHODS = [\"warn\", \"error\"] as const;\n\n/**\n * Replace the console methods named above with sink writers; the returned\n * function restores every original. Captured output is `format(...)`ed, so\n * `table`/`group` lose their layout — acceptable on purpose: the invariant\n * bought here is that their BYTES stay off the protocol stream, not that\n * their rendering survives.\n */\nexport function captureConsole(sinks: Sinks): () => void {\n const target = console as unknown as Record<string, unknown>;\n const writer = (sink: string[]) => (...parts: unknown[]): void => {\n sink.push(format(...parts));\n };\n const toStdout = writer(sinks.stdout);\n const toStderr = writer(sinks.stderr);\n const original = new Map<string, unknown>();\n for (const [names, replacement] of [\n [STDOUT_METHODS, toStdout],\n [STDERR_METHODS, toStderr],\n ] as const) {\n for (const name of names) {\n // A method absent on this Node build has nothing to capture — and\n // nothing to restore, so it must not enter the map either.\n if (typeof target[name] !== \"function\") continue;\n original.set(name, target[name]);\n target[name] = replacement;\n }\n }\n return () => {\n for (const [name, method] of original) target[name] = method;\n };\n}\n\n/** Commander output codes whose printed output is the point, not a usage failure. */\nconst PASS_THROUGH = new Set([\"commander.help\", \"commander.helpDisplayed\", \"commander.version\"]);\n\n/**\n * The scoped program, output routed into `sinks`. configureOutput and\n * exitOverride are set BEFORE the register calls on purpose: commander copies\n * inherited settings onto a subcommand when it is created, so settings applied\n * after registration would leave every subcommand writing to the real stdout —\n * which for an MCP server is the protocol stream.\n */\nfunction readProgram(sinks: Sinks, authoring = false): Command {\n const program = new Command();\n program.name(\"loam\");\n program.exitOverride();\n program.configureOutput({\n writeOut: (text) => sinks.stdout.push(text),\n writeErr: (text) => sinks.stderr.push(text),\n });\n registerValidate(program);\n registerStatus(program);\n registerList(program);\n registerShow(program);\n registerDelta(program);\n registerDiff(program);\n registerExplore(program);\n registerDependencies(program);\n registerDoctor(program);\n registerContext(program);\n registerGate(program);\n registerExplain(program);\n // The two that read no config at all. They are here for the same reason they\n // are in MCP_TOOLS: an agent meets `instructions` and `steps` BEFORE the\n // repository is wired, and every generated skill's first line points at\n // `loam instructions <workflow>`. This list and MCP_TOOLS are two copies of\n // one roster, and nothing counted them against each other until a tool\n // advertised in `tools/list` and absent here answered a `tools/call` with\n // `unknown command 'instructions'` — an envelope that reads as the caller's\n // mistake. `test/mcp-serve.test.ts` now calls every advertised tool.\n registerInstructions(program);\n registerSteps(program);\n if (authoring) {\n registerNew(program);\n registerRebase(program);\n registerGherkin(program);\n registerArchive(program);\n }\n return program;\n}\n\n/**\n * Run one tool argv. Never throws: whatever goes wrong becomes an envelope in\n * `stdout` with `code` 1, so the serve loop's answer to THIS call is the only\n * thing affected — one bad call must never take the server down, and a\n * memoised rejection cannot exist because nothing here outlives the call.\n */\nexport async function runToolArgv(\n argv: readonly string[],\n authoring = false,\n): Promise<DispatchResult> {\n const sinks: Sinks = { stdout: [], stderr: [] };\n const previousExitCode = process.exitCode;\n const restoreConsole = captureConsole(sinks);\n process.exitCode = undefined;\n try {\n await readProgram(sinks, authoring).parseAsync([\"node\", \"loam\", ...argv]);\n } catch (err) {\n if (err instanceof CommanderError) {\n if (PASS_THROUGH.has(err.code)) {\n // Unreachable through the tool schemas (the argv builder refuses every\n // '-'-prefixed string, so --help/--version cannot arrive), kept for the\n // day it is not: the output already sits in the sinks; keep its code.\n if (process.exitCode === undefined) process.exitCode = err.exitCode;\n } else {\n // The same envelope cli.ts's catch emits for a usage error, so an MCP\n // caller still receives machine-readable JSON, not a bare exception.\n emitJsonError(\"invalid-option\", err.message.replace(/^error: /, \"\"));\n }\n } else {\n // An unexpected throw from a command action: the CLI's own catch-all\n // (`internal`) — the one code with no stable meaning, carried so the\n // caller gets an envelope rather than a dead call.\n emitJsonError(\"internal\", err instanceof Error ? err.message : String(err));\n }\n } finally {\n restoreConsole();\n }\n const code = typeof process.exitCode === \"number\" ? process.exitCode : 0;\n process.exitCode = previousExitCode;\n return { stdout: sinks.stdout.join(\"\\n\"), stderr: sinks.stderr.join(\"\\n\"), code };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"dispatch.js","sourceRoot":"","sources":["../../../src/commands/mcp/dispatch.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * In-process dispatch for one MCP tool call: build a fresh commander program\n * holding only the commands the selected tool table exposes, run the argv through\n * it, and hand back what the CLI would have printed.\n *\n * This is the proven harness pattern (test/helpers/harness.ts runLoamNow)\n * moved to the commands layer, minus the chdir: the server's own cwd IS the\n * workdir, so every call resolves loam.json exactly as the CLI would in that\n * directory — the server serves ONE repository, the one it started in, and\n * never changes directory. In-process rather than a child per call because a\n * child re-pays LikeC4/Langium startup on every validate, and because in dev\n * there is no built binary to exec.\n *\n * The capture is process-global (console, exitCode), which is safe under\n * exactly one condition the serve loop guarantees: calls are strictly\n * sequential — each one is awaited to completion before the next frame is\n * read — and `parseAsync` resolves only after the action has fully run (the\n * entire test suite rests on that same guarantee). Nothing here may be\n * module-level state: two serve sessions in one process (the tests do this)\n * must not see each other.\n *\n * NOTE for test/agents.test.ts's scan: registration happens through the\n * imported register functions; this module must never contain the literal\n * dot-command-paren call, comments included, or the CLI surface count breaks.\n */\nimport { Command, CommanderError } from \"commander\";\nimport { format } from \"node:util\";\nimport { emitJsonError } from \"../../core/envelope/json.js\";\nimport { registerValidate } from \"../validate/validate.js\";\nimport { registerStatus } from \"../status/status.js\";\nimport { registerList } from \"../list/list.js\";\nimport { registerShow } from \"../show/show.js\";\nimport { registerDelta } from \"../delta/delta.js\";\nimport { registerTasks } from \"../tasks/tasks.js\";\nimport { registerDiff } from \"../diff/diff.js\";\nimport { registerExplore } from \"../explore.js\";\nimport { registerDependencies } from \"../dependencies.js\";\nimport { registerDoctor } from \"../doctor.js\";\nimport { registerContext } from \"../context/context.js\";\nimport { registerAdopt } from \"../adopt/adopt.js\";\nimport { registerGate } from \"../gate/gate.js\";\nimport { registerExplain } from \"../explain/explain.js\";\nimport { registerInstructions } from \"../instructions.js\";\nimport { registerSteps } from \"../steps/steps.js\";\nimport { registerNew } from \"../new/new.js\";\nimport { registerRebase } from \"../rebase/rebase.js\";\nimport { registerGherkin } from \"../gherkin/gherkin.js\";\nimport { registerArchive } from \"../archive/archive.js\";\nimport type { ToolRunOutcome } from \"../../core/mcp/protocol.js\";\nimport type { ToolRunner } from \"./serve.js\";\nimport { MCP_TOOLS, type McpTool } from \"../../core/mcp/tools.js\";\n\n/** Console sinks for one dispatch — a value, not module state, so calls cannot share buffers. */\nexport interface Sinks {\n readonly stdout: string[];\n readonly stderr: string[];\n}\n\n/**\n * Every console method Node's Console writes to STDOUT — the protocol stream,\n * where one stray line corrupts the transport. Hooking only `log` was the\n * reviewed defect here: a `console.info` in any command, or in a dependency a\n * command loads, would have written straight into the client's frame stream\n * with every suite green. `warn`/`error` go to stderr, captured for the\n * server log rather than the wire.\n */\nconst STDOUT_METHODS = [\n \"log\", \"info\", \"debug\", \"dir\", \"dirxml\", \"table\",\n \"group\", \"groupCollapsed\", \"groupEnd\", \"count\", \"countReset\", \"timeLog\", \"timeEnd\",\n] as const;\nconst STDERR_METHODS = [\"warn\", \"error\"] as const;\n\n/**\n * Replace the console methods named above with sink writers; the returned\n * function restores every original. Captured output is `format(...)`ed, so\n * `table`/`group` lose their layout — acceptable on purpose: the invariant\n * bought here is that their BYTES stay off the protocol stream, not that\n * their rendering survives.\n */\nexport function captureConsole(sinks: Sinks): () => void {\n const target = console as unknown as Record<string, unknown>;\n const writer = (sink: string[]) => (...parts: unknown[]): void => {\n sink.push(format(...parts));\n };\n const toStdout = writer(sinks.stdout);\n const toStderr = writer(sinks.stderr);\n const original = new Map<string, unknown>();\n for (const [names, replacement] of [\n [STDOUT_METHODS, toStdout],\n [STDERR_METHODS, toStderr],\n ] as const) {\n for (const name of names) {\n // A method absent on this Node build has nothing to capture — and\n // nothing to restore, so it must not enter the map either.\n if (typeof target[name] !== \"function\") continue;\n original.set(name, target[name]);\n target[name] = replacement;\n }\n }\n return () => {\n for (const [name, method] of original) target[name] = method;\n };\n}\n\n/** Commander output codes whose printed output is the point, not a usage failure. */\nconst PASS_THROUGH = new Set([\"commander.help\", \"commander.helpDisplayed\", \"commander.version\"]);\n\n/** One command's registration onto a program — the shape every `registerX` export has. */\ntype Registrar = (program: Command) => void;\n\n/**\n * Every command a tool table may name, keyed by the `command` its tool carries.\n * A LOOKUP, not a roster: which of these a call's program registers is decided\n * by the active tool table alone (`toolRunner`), so the authoring writers —\n * new, rebase, gherkin, archive — exist in it only when `--author` put their\n * tools in the table, and the table the read-only hints describe is the one\n * place that gate is written.\n *\n * `instructions` and `steps` read no config at all, and are in MCP_TOOLS\n * because an agent meets them BEFORE the repository is wired: every generated\n * skill's first line points at `loam instructions <workflow>`. They are why\n * registration follows the table. This list and MCP_TOOLS used to be two\n * copies of one roster, and nothing counted them against each other until a\n * tool advertised in `tools/list` and absent here answered a `tools/call` with\n * `unknown command 'instructions'` — an envelope that reads as the caller's\n * mistake. `test/mcp-serve.test.ts` calls every advertised tool, and checks\n * this lookup against both tables directly.\n */\nconst REGISTRARS = new Map<string, Registrar>([\n [\"validate\", registerValidate],\n [\"status\", registerStatus],\n [\"list\", registerList],\n [\"show\", registerShow],\n [\"delta\", registerDelta],\n [\"tasks\", registerTasks],\n [\"diff\", registerDiff],\n [\"explore\", registerExplore],\n [\"dependencies\", registerDependencies],\n [\"doctor\", registerDoctor],\n [\"context\", registerContext],\n [\"adopt\", registerAdopt],\n [\"gate\", registerGate],\n [\"explain\", registerExplain],\n [\"instructions\", registerInstructions],\n [\"steps\", registerSteps],\n [\"new\", registerNew],\n [\"rebase\", registerRebase],\n [\"gherkin\", registerGherkin],\n [\"archive\", registerArchive],\n]);\n\n/**\n * Resolve every command `tools` name to its registrar, or throw naming each\n * one that has none. This is the server's STARTUP check, and it runs once per\n * table: `toolRunner` calls it before the first frame is read, so a tool\n * advertised with no registrar — the roster drift above, arriving again —\n * stops `loam mcp` at launch, through cli.ts's catch like any other throw,\n * before a client has been promised anything. It used to run inside every call's program build, where one\n * missing entry turned EVERY `tools/call` into an `internal` envelope naming\n * it, the tools that were wired correctly included: a call's outcome\n * depended on a tool it never named.\n */\nexport function resolveRegistrars(tools: readonly McpTool[]): readonly Registrar[] {\n const resolved: Registrar[] = [];\n const missing: string[] = [];\n for (const command of new Set(tools.map((tool) => tool.command))) {\n const register = REGISTRARS.get(command);\n if (register === undefined) missing.push(`'${command}'`);\n else resolved.push(register);\n }\n if (missing.length > 0) {\n throw new Error(`loam mcp: no command registered for MCP tool command ${missing.join(\", \")} — add it to REGISTRARS in src/commands/mcp/dispatch.ts`);\n }\n return resolved;\n}\n\n/**\n * The scoped program, output routed into `sinks`, holding exactly the commands\n * the resolved table names. configureOutput and exitOverride are set BEFORE the\n * register calls on purpose: commander copies inherited settings onto a\n * subcommand when it is created, so settings applied after registration would\n * leave every subcommand writing to the real stdout — which for an MCP server\n * is the protocol stream.\n */\nfunction readProgram(sinks: Sinks, registrars: readonly Registrar[]): Command {\n const program = new Command();\n program.name(\"loam\");\n program.exitOverride();\n program.configureOutput({\n writeOut: (text) => sinks.stdout.push(text),\n writeErr: (text) => sinks.stderr.push(text),\n });\n for (const register of registrars) register(program);\n return program;\n}\n\n/**\n * The runner `loam mcp` serves `tools` with. Throws HERE, at construction, when\n * the table names a command with no registrar (`resolveRegistrars`); the\n * runner it returns never throws, and builds each call's program from the\n * registrars resolved once — so no call can fail for the sake of a tool it\n * did not name.\n */\nexport function toolRunner(tools: readonly McpTool[] = MCP_TOOLS): ToolRunner {\n const registrars = resolveRegistrars(tools);\n return (argv) => runToolArgv(argv, registrars);\n}\n\n/**\n * Run one tool argv. Never throws: whatever goes wrong becomes an envelope in\n * `stdout` with `code` 1, so the serve loop's answer to THIS call is the only\n * thing affected — one bad call must never take the server down, and a\n * memoised rejection cannot exist because nothing here outlives the call.\n */\nasync function runToolArgv(argv: readonly string[], registrars: readonly Registrar[]): Promise<ToolRunOutcome> {\n const sinks: Sinks = { stdout: [], stderr: [] };\n const previousExitCode = process.exitCode;\n const restoreConsole = captureConsole(sinks);\n process.exitCode = undefined;\n try {\n await readProgram(sinks, registrars).parseAsync([\"node\", \"loam\", ...argv]);\n } catch (err) {\n if (err instanceof CommanderError) {\n if (PASS_THROUGH.has(err.code)) {\n // Unreachable through the tool schemas (the argv builder refuses every\n // '-'-prefixed string, so --help/--version cannot arrive), kept for the\n // day it is not: the output already sits in the sinks; keep its code.\n if (process.exitCode === undefined) process.exitCode = err.exitCode;\n } else {\n // The same envelope cli.ts's catch emits for a usage error, so an MCP\n // caller still receives machine-readable JSON, not a bare exception.\n emitJsonError(\"invalid-option\", err.message.replace(/^error: /, \"\"));\n }\n } else {\n // An unexpected throw from a command action: the CLI's own catch-all\n // (`internal`) — the one code with no stable meaning, carried so the\n // caller gets an envelope rather than a dead call.\n emitJsonError(\"internal\", err instanceof Error ? err.message : String(err));\n }\n } finally {\n restoreConsole();\n }\n const code = typeof process.exitCode === \"number\" ? process.exitCode : 0;\n process.exitCode = previousExitCode;\n return { stdout: sinks.stdout.join(\"\\n\"), stderr: sinks.stderr.join(\"\\n\"), code };\n}\n"]}
|
package/dist/commands/mcp/mcp.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { toolRunner } from "./dispatch.js";
|
|
2
2
|
import { serve } from "./serve.js";
|
|
3
3
|
import { MCP_TOOLS } from "../../core/mcp/tools.js";
|
|
4
4
|
import { MCP_AUTHOR_TOOLS } from "../../core/mcp/author-tools.js";
|
|
@@ -15,20 +15,61 @@ export function registerMcp(program) {
|
|
|
15
15
|
// is JSON-RPC frames either way; there is no human rendering to switch off.
|
|
16
16
|
.option("--json", "emit the machine contract instead of the human view")
|
|
17
17
|
.action(async (opts) => {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
// `no-config`/`config-invalid` envelope in a tool result — readable, and
|
|
24
|
-
// actionable, by the agent on the other end. The server serves the
|
|
25
|
-
// repository it was started in (each call resolves loam.json through
|
|
26
|
-
// process.cwd()) and never changes directory.
|
|
27
|
-
const tools = opts.author ? [...MCP_TOOLS, ...MCP_AUTHOR_TOOLS] : MCP_TOOLS;
|
|
28
|
-
const run = opts.author
|
|
29
|
-
? (argv) => runToolArgv(argv, true)
|
|
30
|
-
: runToolArgv;
|
|
31
|
-
await serve({ input: process.stdin, output: process.stdout, log: process.stderr }, run, tools);
|
|
18
|
+
await startMcp(mcpToolTable(opts.author === true), {
|
|
19
|
+
input: process.stdin,
|
|
20
|
+
output: process.stdout,
|
|
21
|
+
log: process.stderr,
|
|
22
|
+
});
|
|
32
23
|
});
|
|
33
24
|
}
|
|
25
|
+
/**
|
|
26
|
+
* The table `loam mcp` serves: the read surface, and the bounded writers after
|
|
27
|
+
* it only when the server was launched with `--author`.
|
|
28
|
+
*
|
|
29
|
+
* A function of its own, exported, because nothing pinned it inline. The
|
|
30
|
+
* in-process tests hand `startMcp` a table directly and the stdio smoke
|
|
31
|
+
* spawned `loam mcp` only without the flag, so this choice written as
|
|
32
|
+
* `MCP_TOOLS` alone left every test green — while every config `loam init
|
|
33
|
+
* --mcp-author` writes launches `loam mcp --author`, and those hosts would
|
|
34
|
+
* have lost the authoring tools without a word. test/mcp-serve.test.ts holds
|
|
35
|
+
* both values here; test/mcp-stdio.test.ts spawns `loam mcp --author` and holds
|
|
36
|
+
* the flag to it.
|
|
37
|
+
*/
|
|
38
|
+
export function mcpToolTable(author) {
|
|
39
|
+
return author ? [...MCP_TOOLS, ...MCP_AUTHOR_TOOLS] : MCP_TOOLS;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* The action's body: serve `tools` on `io` until its input ends — or refuse
|
|
43
|
+
* before the first byte when the table itself is defective.
|
|
44
|
+
*
|
|
45
|
+
* Exported, with the stdio triple injected, so the startup refusal is pinned
|
|
46
|
+
* WHERE it happens. A test of `toolRunner` alone could not see the wiring
|
|
47
|
+
* here: rewritten as `(argv) => toolRunner(tools)(argv)`, every call would
|
|
48
|
+
* build its own runner, the refusal would move back to every `tools/call`, and
|
|
49
|
+
* a test that only called `toolRunner` would still pass.
|
|
50
|
+
*/
|
|
51
|
+
export function startMcp(tools, io) {
|
|
52
|
+
// No docs-repo gate at startup, deliberately (the add-command preamble
|
|
53
|
+
// decision): a launch-time refusal would print a non-JSON-RPC line into
|
|
54
|
+
// the client's protocol stream and kill the handshake with nothing a
|
|
55
|
+
// client can parse. Every tool call flows through the same command path
|
|
56
|
+
// the CLI uses, so a missing or broken loam.json arrives as the ordinary
|
|
57
|
+
// `no-config`/`config-invalid` envelope in a tool result — readable, and
|
|
58
|
+
// actionable, by the agent on the other end. The server serves the
|
|
59
|
+
// repository it was started in (each call resolves loam.json through
|
|
60
|
+
// process.cwd()) and never changes directory.
|
|
61
|
+
// One table decides both what `tools/list` advertises and which commands
|
|
62
|
+
// a `tools/call` can reach (`./dispatch.ts`'s `toolRunner`).
|
|
63
|
+
//
|
|
64
|
+
// `toolRunner` is the ONE launch-time refusal, and it is not the gate the
|
|
65
|
+
// paragraph above declines: it throws only when the table itself names a
|
|
66
|
+
// command with no registrar — a defect in the shipped binary that no
|
|
67
|
+
// repository state causes and no client can repair. Refusing here, before
|
|
68
|
+
// the first frame, costs the handshake; answering it per call instead
|
|
69
|
+
// turned every `tools/call` into an `internal` envelope, the correctly
|
|
70
|
+
// wired tools included. Without `--json` (how a client launches it) the
|
|
71
|
+
// message goes to stderr through cli.ts's catch, and stdout stays empty.
|
|
72
|
+
const run = toolRunner(tools);
|
|
73
|
+
return serve(io, run, tools);
|
|
74
|
+
}
|
|
34
75
|
//# sourceMappingURL=mcp.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mcp.js","sourceRoot":"","sources":["../../../src/commands/mcp/mcp.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"mcp.js","sourceRoot":"","sources":["../../../src/commands/mcp/mcp.ts"],"names":[],"mappings":"AAYA,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAC3C,OAAO,EAAE,KAAK,EAAgB,MAAM,YAAY,CAAC;AACjD,OAAO,EAAE,SAAS,EAAgB,MAAM,yBAAyB,CAAC;AAClE,OAAO,EAAE,gBAAgB,EAAE,MAAM,gCAAgC,CAAC;AAMlE,MAAM,UAAU,WAAW,CAAC,OAAgB;IAC1C,OAAO;SACJ,OAAO,CAAC,KAAK,CAAC;SACd,WAAW,CAAC,4DAA4D,CAAC;SACzE,MAAM,CAAC,UAAU,EAAE,4EAA4E,CAAC;QACjG,mEAAmE;QACnE,2EAA2E;QAC3E,4EAA4E;QAC5E,sEAAsE;QACtE,yEAAyE;QACzE,4EAA4E;SAC3E,MAAM,CAAC,QAAQ,EAAE,qDAAqD,CAAC;SACvE,MAAM,CAAC,KAAK,EAAE,IAAgB,EAAE,EAAE;QACjC,MAAM,QAAQ,CAAC,YAAY,CAAC,IAAI,CAAC,MAAM,KAAK,IAAI,CAAC,EAAE;YACjD,KAAK,EAAE,OAAO,CAAC,KAAK;YACpB,MAAM,EAAE,OAAO,CAAC,MAAM;YACtB,GAAG,EAAE,OAAO,CAAC,MAAM;SACpB,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;AACP,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,YAAY,CAAC,MAAe;IAC1C,OAAO,MAAM,CAAC,CAAC,CAAC,CAAC,GAAG,SAAS,EAAE,GAAG,gBAAgB,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;AAClE,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,QAAQ,CAAC,KAAyB,EAAE,EAAW;IAC7D,uEAAuE;IACvE,wEAAwE;IACxE,qEAAqE;IACrE,wEAAwE;IACxE,yEAAyE;IACzE,yEAAyE;IACzE,mEAAmE;IACnE,qEAAqE;IACrE,8CAA8C;IAC9C,yEAAyE;IACzE,6DAA6D;IAC7D,EAAE;IACF,0EAA0E;IAC1E,yEAAyE;IACzE,qEAAqE;IACrE,0EAA0E;IAC1E,sEAAsE;IACtE,uEAAuE;IACvE,wEAAwE;IACxE,yEAAyE;IACzE,MAAM,GAAG,GAAG,UAAU,CAAC,KAAK,CAAC,CAAC;IAC9B,OAAO,KAAK,CAAC,EAAE,EAAE,GAAG,EAAE,KAAK,CAAC,CAAC;AAC/B,CAAC","sourcesContent":["/**\n * `loam mcp` — serve the read surface, plus bounded writers under `--author`.\n *\n * The action asks `mcpToolTable` which table its flag selects and hands it,\n * with the real stdio triple, to `startMcp`, which resolves the table once and\n * serves it until stdin EOF; everything else with a decision in it lives in\n * `./serve.ts`, `./dispatch.ts` and `src/core/mcp/`. This module carries the\n * one command registration of the package (test/agents.test.ts counts\n * registrations by their literal spelling, so the sibling modules must never\n * spell one).\n */\nimport type { Command } from \"commander\";\nimport { toolRunner } from \"./dispatch.js\";\nimport { serve, type ServeIo } from \"./serve.js\";\nimport { MCP_TOOLS, type McpTool } from \"../../core/mcp/tools.js\";\nimport { MCP_AUTHOR_TOOLS } from \"../../core/mcp/author-tools.js\";\n\ninterface McpOptions {\n author?: boolean;\n}\n\nexport function registerMcp(program: Command): void {\n program\n .command(\"mcp\")\n .description(\"Serve loam commands as MCP tools over stdio (JSON-RPC 2.0)\")\n .option(\"--author\", \"opt in to safe authoring tools; vouch and committing archive stay excluded\")\n // Declared for the uniform surface every command owes (`--json` is\n // mandatory, test/agents.test.ts), and it is not inert: it governs the one\n // pre-loop surface this command has — a usage error like `loam mcp --bogus`\n // aborts before the action runs, and cli.ts's catch reads raw argv to\n // decide whether that refusal is an envelope. Once the loop runs, stdout\n // is JSON-RPC frames either way; there is no human rendering to switch off.\n .option(\"--json\", \"emit the machine contract instead of the human view\")\n .action(async (opts: McpOptions) => {\n await startMcp(mcpToolTable(opts.author === true), {\n input: process.stdin,\n output: process.stdout,\n log: process.stderr,\n });\n });\n}\n\n/**\n * The table `loam mcp` serves: the read surface, and the bounded writers after\n * it only when the server was launched with `--author`.\n *\n * A function of its own, exported, because nothing pinned it inline. The\n * in-process tests hand `startMcp` a table directly and the stdio smoke\n * spawned `loam mcp` only without the flag, so this choice written as\n * `MCP_TOOLS` alone left every test green — while every config `loam init\n * --mcp-author` writes launches `loam mcp --author`, and those hosts would\n * have lost the authoring tools without a word. test/mcp-serve.test.ts holds\n * both values here; test/mcp-stdio.test.ts spawns `loam mcp --author` and holds\n * the flag to it.\n */\nexport function mcpToolTable(author: boolean): readonly McpTool[] {\n return author ? [...MCP_TOOLS, ...MCP_AUTHOR_TOOLS] : MCP_TOOLS;\n}\n\n/**\n * The action's body: serve `tools` on `io` until its input ends — or refuse\n * before the first byte when the table itself is defective.\n *\n * Exported, with the stdio triple injected, so the startup refusal is pinned\n * WHERE it happens. A test of `toolRunner` alone could not see the wiring\n * here: rewritten as `(argv) => toolRunner(tools)(argv)`, every call would\n * build its own runner, the refusal would move back to every `tools/call`, and\n * a test that only called `toolRunner` would still pass.\n */\nexport function startMcp(tools: readonly McpTool[], io: ServeIo): Promise<void> {\n // No docs-repo gate at startup, deliberately (the add-command preamble\n // decision): a launch-time refusal would print a non-JSON-RPC line into\n // the client's protocol stream and kill the handshake with nothing a\n // client can parse. Every tool call flows through the same command path\n // the CLI uses, so a missing or broken loam.json arrives as the ordinary\n // `no-config`/`config-invalid` envelope in a tool result — readable, and\n // actionable, by the agent on the other end. The server serves the\n // repository it was started in (each call resolves loam.json through\n // process.cwd()) and never changes directory.\n // One table decides both what `tools/list` advertises and which commands\n // a `tools/call` can reach (`./dispatch.ts`'s `toolRunner`).\n //\n // `toolRunner` is the ONE launch-time refusal, and it is not the gate the\n // paragraph above declines: it throws only when the table itself names a\n // command with no registrar — a defect in the shipped binary that no\n // repository state causes and no client can repair. Refusing here, before\n // the first frame, costs the handshake; answering it per call instead\n // turned every `tools/call` into an `internal` envelope, the correctly\n // wired tools included. Without `--json` (how a client launches it) the\n // message goes to stderr through cli.ts's catch, and stdout stays empty.\n const run = toolRunner(tools);\n return serve(io, run, tools);\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"serve.js","sourceRoot":"","sources":["../../../src/commands/mcp/serve.ts"],"names":[],"mappings":"AA0BA,OAAO,EAAE,KAAK,EAAE,WAAW,EAAE,MAAM,2BAA2B,CAAC;AAC/D,OAAO,EACL,cAAc,EACd,aAAa,EACb,cAAc,EACd,SAAS,EACT,SAAS,GACV,MAAM,4BAA4B,CAAC;
|
|
1
|
+
{"version":3,"file":"serve.js","sourceRoot":"","sources":["../../../src/commands/mcp/serve.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The MCP stdio loop: read newline-delimited JSON-RPC frames from `input`,\n * answer on `output`, log on `log`. Streams are injected so the tests drive\n * a whole session in-process with PassThrough pairs; `loam mcp` passes the\n * real stdio triple.\n *\n * Two invariants everything else leans on:\n *\n * - `output` carries protocol frames and NOTHING else — every byte goes\n * through `frame()`. Command output cannot leak here: the dispatcher hooks\n * console and commander's own writers, and no command in src/ writes to\n * process.stdout directly (verified at implementation time; the dispatcher\n * comment records the guarantee it needs). Logging — the startup line, a\n * tool's captured stderr — goes to `log`, which MCP permits.\n *\n * - Frames are handled STRICTLY SEQUENTIALLY: each one is awaited before the\n * next is looked at. This is not an optimization opportunity — the\n * dispatcher's console capture is process-global, so two overlapping calls\n * would interleave their captured output into each other's envelopes.\n *\n * The loop ends when `input` ends: per the MCP stdio lifecycle the client\n * closes stdin to shut the server down, so EOF resolves this promise, the\n * action resolves, and cli.ts — the only process-exit decider — lets the\n * process drain to exit 0. No signal handling, no exit call here.\n */\nimport type { Readable, Writable } from \"node:stream\";\nimport { frame, splitFrames } from \"../../core/mcp/framing.js\";\nimport {\n INTERNAL_ERROR,\n errorResponse,\n resultResponse,\n routeLine,\n toolReply,\n} from \"../../core/mcp/protocol.js\";\nimport type { RouteOutcome, ToolRunOutcome } from \"../../core/mcp/protocol.js\";\nimport { MCP_TOOLS, type McpTool } from \"../../core/mcp/tools.js\";\n\nexport interface ServeIo {\n readonly input: Readable;\n readonly output: Writable;\n readonly log: Writable;\n}\n\n/** The dispatcher, as a parameter: serve() stays testable with a stub and owns no registration. */\nexport type ToolRunner = (argv: readonly string[]) => Promise<ToolRunOutcome>;\n\nexport async function serve(\n io: ServeIo,\n runTool: ToolRunner,\n tools: readonly McpTool[] = MCP_TOOLS,\n): Promise<void> {\n // One startup line, to stderr only — a client is entitled to a stdout that\n // has never carried anything but frames, from the very first byte.\n io.log.write(`loam mcp: serving ${process.cwd()} over stdio\\n`);\n // Decode at the stream, not per chunk: a UTF-8 code point split across two\n // chunks would corrupt if each chunk were stringified on its own.\n io.input.setEncoding(\"utf8\");\n let rest = \"\";\n for await (const chunk of io.input) {\n const split = splitFrames(rest + String(chunk));\n rest = split.rest;\n for (const line of split.lines) await handleLine(line, io, runTool, tools);\n }\n // A final frame the client sent without a trailing newline is still a frame:\n // EOF terminates it exactly as the delimiter would have.\n if (rest.trim() !== \"\") await handleLine(rest, io, runTool, tools);\n}\n\nasync function handleLine(\n line: string,\n io: ServeIo,\n runTool: ToolRunner,\n tools: readonly McpTool[],\n): Promise<void> {\n const outcome: RouteOutcome = routeLine(line, tools);\n if (outcome.kind === \"ignore\") return;\n if (outcome.kind === \"reply\") {\n io.output.write(frame(outcome.response));\n return;\n }\n await handleCall(outcome, io, runTool);\n}\n\n/** One tools/call, contained: whatever this call does, the loop survives it. */\nasync function handleCall(\n call: Extract<RouteOutcome, { kind: \"call\" }>,\n io: ServeIo,\n runTool: ToolRunner,\n): Promise<void> {\n try {\n const run = await runTool(call.argv);\n if (run.stderr.trim() !== \"\") {\n // A command's diagnostics are server-side logging, never protocol bytes.\n io.log.write(`loam mcp: ${call.toolName} stderr: ${run.stderr.trim()}\\n`);\n }\n const reply = toolReply(run);\n io.output.write(frame(\n reply.kind === \"result\"\n ? resultResponse(call.id, reply.result)\n : errorResponse(call.id, reply.code, reply.message),\n ));\n } catch (err) {\n // The dispatcher already converts everything it can into an envelope, so\n // reaching here means the failure was outside it (or in the wrapping\n // itself). It becomes a JSON-RPC error for THIS id — the caller learns\n // which call died, with the reason — and the loop reads the next frame.\n const detail = err instanceof Error ? err.message : String(err);\n io.output.write(frame(errorResponse(call.id, INTERNAL_ERROR, `${call.toolName} failed: ${detail}`)));\n }\n}\n"]}
|