@ybotok/loam 0.2.0-alpha.7 → 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 +1315 -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 +83 -7
- package/SCHEMA.md +569 -98
- 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 +33 -11
- 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 +21 -25
- package/dist/commands/seed/commit.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 +3 -5
- package/dist/commands/subsystem/edit.js.map +1 -1
- package/dist/commands/subsystem/move.js +5 -8
- 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 +14 -41
- package/dist/commands/subsystem/sync.js.map +1 -1
- package/dist/commands/subsystem/txn/txn.js +17 -38
- package/dist/commands/subsystem/txn/txn.js.map +1 -1
- package/dist/commands/subsystem/txn/views.js +32 -5
- 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/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/landscape.js +23 -23
- package/dist/commands/validate/fleet/landscape.js.map +1 -1
- package/dist/commands/validate/fleet/map/attest.js +6 -10
- package/dist/commands/validate/fleet/map/attest.js.map +1 -1
- package/dist/commands/validate/fleet/map/bindings.js +1 -3
- package/dist/commands/validate/fleet/map/bindings.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.map +1 -1
- package/dist/commands/validate/fleet/views/root-documents.js +8 -28
- package/dist/commands/validate/fleet/views/root-documents.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 +56 -101
- 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 +4 -3
- 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 +32 -18
- package/dist/core/agent/agents-md/refusals.js.map +1 -1
- package/dist/core/agent/agents-md.js +9 -3
- 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 +20 -16
- 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 +50 -18
- 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 +6 -18
- 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 +15 -5
- package/dist/core/brief/targets.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/documents.js +31 -6
- package/dist/core/c4/project/documents.js.map +1 -1
- package/dist/core/c4/project/load.js +6 -17
- package/dist/core/c4/project/load.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 +3 -6
- 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 +5 -2
- package/dist/core/gate/checks.js.map +1 -1
- package/dist/core/gate/partners.js +4 -2
- 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/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/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 +3 -0
- 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 +12 -2
- package/dist/core/vocabulary/maturity.js.map +1 -1
- package/examples/README.md +21 -6
- package/examples/docs/architecture/landscape.likec4 +8 -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/platform/notification-service/asyncapi.yaml +2 -1
- 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
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"]}
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* shapes this emits; `./living.js` is the same corpus read for the other half.
|
|
11
11
|
*/
|
|
12
12
|
import { join } from "node:path";
|
|
13
|
-
import {
|
|
13
|
+
import { openspecRequirements } from "../../../core/openspec/scan/shape.js";
|
|
14
14
|
import {} from "../../../core/document/spec.js";
|
|
15
15
|
import { ownValue } from "../../../core/kernel/records.js";
|
|
16
16
|
import {} from "../../../core/openspec/model/mapping.js";
|
|
@@ -22,6 +22,27 @@ import { safeArtifactRelative } from "../openspec/paths.js";
|
|
|
22
22
|
import { readSourceArtifact } from "../openspec/read.js";
|
|
23
23
|
import { featureSlug, migrationFrontmatter, renderAuthoredBundle, serializeDeltaRequirements, } from "./feature.js";
|
|
24
24
|
import { assertMaterializedRequirementIds, requirementWithCapability } from "./stage.js";
|
|
25
|
+
function isSettled(rename) {
|
|
26
|
+
return rename.from !== null && rename.to !== null && rename.requirementId !== null;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* A settled rename staged as the MODIFIED requirement it becomes: the new
|
|
30
|
+
* heading under the settled id, annotated once with the living requirement it
|
|
31
|
+
* renames. One construction for both places a rename materializes — over the
|
|
32
|
+
* delta's own MODIFIED body (already that kind, so the `kind` here changes
|
|
33
|
+
* nothing), and over the living requirement when the delta restates none.
|
|
34
|
+
*/
|
|
35
|
+
function renamedAsModified(base, rename) {
|
|
36
|
+
const annotation = `OpenSpec-Living-Source: ${rename.capability} :: ${rename.from}`;
|
|
37
|
+
return {
|
|
38
|
+
...base,
|
|
39
|
+
kind: "MODIFIED",
|
|
40
|
+
id: rename.requirementId,
|
|
41
|
+
name: rename.to,
|
|
42
|
+
text: base.text.includes(annotation) ? base.text : [...base.text, "", annotation],
|
|
43
|
+
section: "## MODIFIED Requirements",
|
|
44
|
+
};
|
|
45
|
+
}
|
|
25
46
|
export async function materializeActiveChanges(inventory, mapping, target, livingByCapability) {
|
|
26
47
|
const writes = [];
|
|
27
48
|
const inventoryArtifactsByPath = new Map(inventory.artifacts.map((artifact) => [artifact.path, artifact]));
|
|
@@ -97,7 +118,7 @@ export async function materializeActiveChanges(inventory, mapping, target, livin
|
|
|
97
118
|
deltaByService.set(service, existing);
|
|
98
119
|
};
|
|
99
120
|
for (const spec of change.specs) {
|
|
100
|
-
const requirements =
|
|
121
|
+
const requirements = openspecRequirements(await readSourceArtifact(inventory.root, spec.path))
|
|
101
122
|
.filter((requirement) => requirement.kind !== "BASE");
|
|
102
123
|
for (const parsed of requirements) {
|
|
103
124
|
const matchingRenames = changeRenames.filter((rename) => rename.capability === spec.capability
|
|
@@ -110,21 +131,14 @@ export async function materializeActiveChanges(inventory, mapping, target, livin
|
|
|
110
131
|
const rename = matchingRenames[0];
|
|
111
132
|
let requirement = parsed;
|
|
112
133
|
let routingName = parsed.name;
|
|
113
|
-
if (rename !== undefined && rename
|
|
134
|
+
if (rename !== undefined && isSettled(rename)) {
|
|
114
135
|
if (parsed.kind !== "MODIFIED") {
|
|
115
136
|
throw new OpenSpecCommandError("invalid-option", `Active change '${change.id}' combines RENAMED '${rename.from}' → '${rename.to}' with ${parsed.kind} '${parsed.name}'; review the conflicting delta before --apply.`);
|
|
116
137
|
}
|
|
117
138
|
if (parsed.id !== undefined && parsed.id !== rename.requirementId) {
|
|
118
139
|
throw new OpenSpecCommandError("invalid-option", `Active change '${change.id}' MODIFIED requirement '${parsed.name}' has Requirement-ID '${parsed.id}', but its RENAMED source resolves to '${rename.requirementId}'.`);
|
|
119
140
|
}
|
|
120
|
-
|
|
121
|
-
requirement = {
|
|
122
|
-
...parsed,
|
|
123
|
-
id: rename.requirementId,
|
|
124
|
-
name: rename.to,
|
|
125
|
-
text: parsed.text.includes(annotation) ? parsed.text : [...parsed.text, "", annotation],
|
|
126
|
-
section: "## MODIFIED Requirements",
|
|
127
|
-
};
|
|
141
|
+
requirement = renamedAsModified(parsed, rename);
|
|
128
142
|
routingName = rename.from;
|
|
129
143
|
materializedRenameKeys.add(rename.key);
|
|
130
144
|
}
|
|
@@ -140,22 +154,13 @@ export async function materializeActiveChanges(inventory, mapping, target, livin
|
|
|
140
154
|
for (const rename of changeRenames) {
|
|
141
155
|
if (materializedRenameKeys.has(rename.key))
|
|
142
156
|
continue;
|
|
143
|
-
if (rename
|
|
157
|
+
if (!isSettled(rename))
|
|
144
158
|
continue;
|
|
145
159
|
const source = (livingByCapability.get(rename.capability) ?? [])
|
|
146
160
|
.filter((requirement) => requirement.name === rename.from)[0];
|
|
147
161
|
if (source === undefined)
|
|
148
162
|
continue; // readiness prevents this on --apply
|
|
149
|
-
const
|
|
150
|
-
const text = source.text.includes(annotation) ? source.text : [...source.text, "", annotation];
|
|
151
|
-
const modified = {
|
|
152
|
-
...source,
|
|
153
|
-
kind: "MODIFIED",
|
|
154
|
-
id: rename.requirementId,
|
|
155
|
-
name: rename.to,
|
|
156
|
-
text,
|
|
157
|
-
section: "## MODIFIED Requirements",
|
|
158
|
-
};
|
|
163
|
+
const modified = renamedAsModified(source, rename);
|
|
159
164
|
for (const service of selectedServices(mapping, rename.capability, rename.from)) {
|
|
160
165
|
addDelta(service, requirementWithCapability(modified, rename.capability), rename.path);
|
|
161
166
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"active.js","sourceRoot":"","sources":["../../../../src/commands/migrate-openspec/materialize/active.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Each active OpenSpec change as a staged loam feature: its intent, the\n * authored artifacts a disposition kept, a verbatim copy of the source change\n * tree, and one delta `spec.md` per service its requirements routed to.\n *\n * This is the phase that resolves RENAMED pairs, and the reason it is the\n * longest: a rename is the only place where the living corpus, the delta and\n * the human's identity decision all have to agree, and every disagreement\n * between them is a refusal rather than a guess. `./feature.js` holds the file\n * shapes this emits; `./living.js` is the same corpus read for the other half.\n */\nimport { join } from \"node:path\";\nimport { parseRequirements } from \"../../../core/document/parse.js\";\nimport { type Requirement } from \"../../../core/document/spec.js\";\nimport { ownValue } from \"../../../core/kernel/records.js\";\nimport { type OpenSpecMapping } from \"../../../core/openspec/model/mapping.js\";\nimport { isOpenSpecArtifactDisposition, type OpenSpecInventory } from \"../../../core/openspec/model/model.js\";\nimport { planWrite, type PlannedWrite } from \"../../../core/staging/writes.js\";\nimport { selectedServices } from \"../openspec/decisions.js\";\nimport { OpenSpecCommandError } from \"../openspec/error.js\";\nimport { safeArtifactRelative } from \"../openspec/paths.js\";\nimport { readSourceArtifact } from \"../openspec/read.js\";\nimport {\n featureSlug,\n migrationFrontmatter,\n renderAuthoredBundle,\n serializeDeltaRequirements,\n type AuthoredArtifactCopy,\n} from \"./feature.js\";\nimport { assertMaterializedRequirementIds, requirementWithCapability } from \"./stage.js\";\n\nexport async function materializeActiveChanges(\n inventory: OpenSpecInventory,\n mapping: OpenSpecMapping,\n target: string,\n livingByCapability: ReadonlyMap<string, Requirement[]>,\n): Promise<PlannedWrite[]> {\n const writes: PlannedWrite[] = [];\n const inventoryArtifactsByPath = new Map(inventory.artifacts.map((artifact) => [artifact.path, artifact]));\n\n for (const change of inventory.changes.active) {\n const decision = inventory.changeDecisions.find((item) => item.change === change.id);\n const feature = ownValue(mapping.changes, change.id)?.feature ?? null;\n if (decision === undefined || feature === null) continue; // readiness prevents this on --apply\n const featureDir = join(target, \"features\", `${feature}-${featureSlug(decision.title, change.id)}`);\n\n // Preserve the complete authored source change tree, including metadata,\n // rename syntax, custom artifacts, and original delta section ordering.\n for (const artifactPath of change.artifacts) {\n const segments = safeArtifactRelative(artifactPath, `changes/${change.id}/`, `OpenSpec change '${change.id}'`);\n writes.push(planWrite(\n join(featureDir, \"legacy\", \"openspec\", ...segments),\n await readSourceArtifact(inventory.root, artifactPath),\n ));\n }\n\n const authored: Array<AuthoredArtifactCopy & { kind: \"proposal\" | \"tasks\" | \"change-design\" }> = [];\n for (const artifactDecision of inventory.artifactDecisions) {\n const artifact = inventoryArtifactsByPath.get(artifactDecision.path);\n if (artifact?.changeId !== change.id) continue;\n const disposition = ownValue(mapping.artifacts, artifactDecision.path);\n if (disposition === undefined) continue; // readiness prevents this on --apply\n // The mapping holds the authored string; everything below compares it\n // against disposition literals, so this is where the value has to become\n // one of them or stop being routed. `mapping.invalid-artifact-disposition`\n // already refuses an unknown one, so readiness prevents this too — but a\n // silent skip is the right failure for a value nothing here can act on.\n if (!isOpenSpecArtifactDisposition(disposition)) continue;\n authored.push({\n path: artifactDecision.path,\n kind: artifactDecision.kind,\n disposition,\n raw: await readSourceArtifact(inventory.root, artifactDecision.path),\n });\n }\n\n const convertedProposals = authored.filter(\n (artifact) => artifact.kind === \"proposal\" && artifact.disposition === \"convert-to-intent\",\n );\n const retainedProposals = authored.filter(\n (artifact) => artifact.kind === \"proposal\" && artifact.disposition !== \"convert-to-intent\",\n );\n const intentBody = convertedProposals.length > 0\n ? renderAuthoredBundle(\"## Migrated OpenSpec proposal\", convertedProposals)\n : \"No proposal was converted into this intent. Review the retained OpenSpec artifacts and migration-plan.json before promotion.\\n\";\n writes.push(planWrite(\n join(featureDir, \"intent.md\"),\n `${migrationFrontmatter(feature, decision.title)}\\n\\n# ${decision.title}\\n\\n<!-- Staged from OpenSpec change '${change.id}'. This feature is deliberately not validation-green yet. -->\\n\\n${intentBody}`,\n ));\n if (retainedProposals.length > 0) {\n writes.push(planWrite(\n join(featureDir, \"legacy\", \"proposal.md\"),\n renderAuthoredBundle(\"# Retained OpenSpec proposal\", retainedProposals),\n ));\n }\n\n const tasks = authored.filter((artifact) => artifact.kind === \"tasks\");\n if (tasks.length > 0) {\n writes.push(planWrite(\n join(featureDir, \"legacy\", \"tasks.md\"),\n renderAuthoredBundle(\"# OpenSpec task checklist\", tasks),\n ));\n }\n const reviewedDesigns = authored.filter(\n (artifact) => artifact.kind === \"change-design\" && artifact.disposition === \"review-as-feature-adr\",\n );\n if (reviewedDesigns.length > 0) {\n writes.push(planWrite(\n join(featureDir, \"adrs\", \"openspec-design.md\"),\n renderAuthoredBundle(\"# OpenSpec design (review required)\", reviewedDesigns),\n ));\n }\n const retainedDesigns = authored.filter(\n (artifact) => artifact.kind === \"change-design\" && artifact.disposition !== \"review-as-feature-adr\",\n );\n if (retainedDesigns.length > 0) {\n writes.push(planWrite(\n join(featureDir, \"legacy\", \"design.md\"),\n renderAuthoredBundle(\"# Retained OpenSpec design\", retainedDesigns),\n ));\n }\n\n if (change.metadata.skipSpecs) continue;\n const deltaByService = new Map<string, Requirement[]>();\n const changeRenames = inventory.renamed.filter(\n (item) => item.scope === \"active\" && item.changeId === change.id,\n );\n const materializedRenameKeys = new Set<string>();\n const addDelta = (service: string, requirement: Requirement, origin: string): void => {\n const existing = deltaByService.get(service) ?? [];\n const collision = existing.find((item) =>\n item.name === requirement.name\n || (item.id !== undefined && requirement.id !== undefined && item.id === requirement.id));\n if (collision !== undefined) {\n const identity = requirement.id === undefined ? requirement.name : requirement.id;\n throw new OpenSpecCommandError(\n \"invalid-option\",\n `Active change '${change.id}' produces colliding requirement '${identity}' in service '${service}' while materializing ${origin}.`,\n );\n }\n existing.push(requirement);\n deltaByService.set(service, existing);\n };\n\n for (const spec of change.specs) {\n const requirements = parseRequirements(await readSourceArtifact(inventory.root, spec.path))\n .filter((requirement) => requirement.kind !== \"BASE\");\n for (const parsed of requirements) {\n const matchingRenames = changeRenames.filter((rename) =>\n rename.capability === spec.capability\n && rename.from !== null\n && rename.to !== null\n && (parsed.name === rename.from || parsed.name === rename.to));\n if (matchingRenames.length > 1) {\n throw new OpenSpecCommandError(\n \"invalid-option\",\n `Active change '${change.id}' requirement '${parsed.name}' matches multiple RENAMED pairs.`,\n );\n }\n const rename = matchingRenames[0];\n let requirement = parsed;\n let routingName = parsed.name;\n if (rename !== undefined && rename.from !== null && rename.to !== null && rename.requirementId !== null) {\n if (parsed.kind !== \"MODIFIED\") {\n throw new OpenSpecCommandError(\n \"invalid-option\",\n `Active change '${change.id}' combines RENAMED '${rename.from}' → '${rename.to}' with ${parsed.kind} '${parsed.name}'; review the conflicting delta before --apply.`,\n );\n }\n if (parsed.id !== undefined && parsed.id !== rename.requirementId) {\n throw new OpenSpecCommandError(\n \"invalid-option\",\n `Active change '${change.id}' MODIFIED requirement '${parsed.name}' has Requirement-ID '${parsed.id}', but its RENAMED source resolves to '${rename.requirementId}'.`,\n );\n }\n const annotation = `OpenSpec-Living-Source: ${rename.capability} :: ${rename.from}`;\n requirement = {\n ...parsed,\n id: rename.requirementId,\n name: rename.to,\n text: parsed.text.includes(annotation) ? parsed.text : [...parsed.text, \"\", annotation],\n section: \"## MODIFIED Requirements\",\n };\n routingName = rename.from;\n materializedRenameKeys.add(rename.key);\n }\n // Every delta kind uniformly: REMOVED is harmless (every check exempts\n // it), and a MODIFIED body MUST carry the line or its wholesale\n // replace would strip the one the living side was staged with.\n const withCapability = requirementWithCapability(requirement, spec.capability);\n for (const service of selectedServices(mapping, spec.capability, routingName)) {\n addDelta(service, withCapability, spec.path);\n }\n }\n }\n for (const rename of changeRenames) {\n if (materializedRenameKeys.has(rename.key)) continue;\n if (rename.from === null || rename.to === null || rename.requirementId === null) continue;\n const source = (livingByCapability.get(rename.capability) ?? [])\n .filter((requirement) => requirement.name === rename.from)[0];\n if (source === undefined) continue; // readiness prevents this on --apply\n const annotation = `OpenSpec-Living-Source: ${rename.capability} :: ${rename.from}`;\n const text = source.text.includes(annotation) ? source.text : [...source.text, \"\", annotation];\n const modified: Requirement = {\n ...source,\n kind: \"MODIFIED\",\n id: rename.requirementId,\n name: rename.to,\n text,\n section: \"## MODIFIED Requirements\",\n };\n for (const service of selectedServices(mapping, rename.capability, rename.from)) {\n addDelta(service, requirementWithCapability(modified, rename.capability), rename.path);\n }\n }\n for (const [service, requirements] of [...deltaByService].sort(([a], [b]) => a.localeCompare(b))) {\n assertMaterializedRequirementIds(\n `Active change '${change.id}' mapped to service '${service}'`,\n requirements,\n );\n writes.push(planWrite(\n join(featureDir, \"specs\", service, \"spec.md\"),\n serializeDeltaRequirements(requirements),\n ));\n }\n }\n return writes;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"active.js","sourceRoot":"","sources":["../../../../src/commands/migrate-openspec/materialize/active.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Each active OpenSpec change as a staged loam feature: its intent, the\n * authored artifacts a disposition kept, a verbatim copy of the source change\n * tree, and one delta `spec.md` per service its requirements routed to.\n *\n * This is the phase that resolves RENAMED pairs, and the reason it is the\n * longest: a rename is the only place where the living corpus, the delta and\n * the human's identity decision all have to agree, and every disagreement\n * between them is a refusal rather than a guess. `./feature.js` holds the file\n * shapes this emits; `./living.js` is the same corpus read for the other half.\n */\nimport { join } from \"node:path\";\nimport { openspecRequirements } from \"../../../core/openspec/scan/shape.js\";\nimport { type Requirement } from \"../../../core/document/spec.js\";\nimport { ownValue } from \"../../../core/kernel/records.js\";\nimport { type OpenSpecMapping } from \"../../../core/openspec/model/mapping.js\";\nimport { isOpenSpecArtifactDisposition, type OpenSpecInventory, type OpenSpecRenamedUsage } from \"../../../core/openspec/model/model.js\";\nimport { planWrite, type PlannedWrite } from \"../../../core/staging/writes.js\";\nimport { selectedServices } from \"../openspec/decisions.js\";\nimport { OpenSpecCommandError } from \"../openspec/error.js\";\nimport { safeArtifactRelative } from \"../openspec/paths.js\";\nimport { readSourceArtifact } from \"../openspec/read.js\";\nimport {\n featureSlug,\n migrationFrontmatter,\n renderAuthoredBundle,\n serializeDeltaRequirements,\n type AuthoredArtifactCopy,\n} from \"./feature.js\";\nimport { assertMaterializedRequirementIds, requirementWithCapability } from \"./stage.js\";\n\n/** A RENAMED pair whose two headings and settled identity are all known — the only kind that materializes. */\ntype SettledRename = OpenSpecRenamedUsage & { from: string; to: string; requirementId: string };\n\nfunction isSettled(rename: OpenSpecRenamedUsage): rename is SettledRename {\n return rename.from !== null && rename.to !== null && rename.requirementId !== null;\n}\n\n/**\n * A settled rename staged as the MODIFIED requirement it becomes: the new\n * heading under the settled id, annotated once with the living requirement it\n * renames. One construction for both places a rename materializes — over the\n * delta's own MODIFIED body (already that kind, so the `kind` here changes\n * nothing), and over the living requirement when the delta restates none.\n */\nfunction renamedAsModified(base: Requirement, rename: SettledRename): Requirement {\n const annotation = `OpenSpec-Living-Source: ${rename.capability} :: ${rename.from}`;\n return {\n ...base,\n kind: \"MODIFIED\",\n id: rename.requirementId,\n name: rename.to,\n text: base.text.includes(annotation) ? base.text : [...base.text, \"\", annotation],\n section: \"## MODIFIED Requirements\",\n };\n}\n\nexport async function materializeActiveChanges(\n inventory: OpenSpecInventory,\n mapping: OpenSpecMapping,\n target: string,\n livingByCapability: ReadonlyMap<string, Requirement[]>,\n): Promise<PlannedWrite[]> {\n const writes: PlannedWrite[] = [];\n const inventoryArtifactsByPath = new Map(inventory.artifacts.map((artifact) => [artifact.path, artifact]));\n\n for (const change of inventory.changes.active) {\n const decision = inventory.changeDecisions.find((item) => item.change === change.id);\n const feature = ownValue(mapping.changes, change.id)?.feature ?? null;\n if (decision === undefined || feature === null) continue; // readiness prevents this on --apply\n const featureDir = join(target, \"features\", `${feature}-${featureSlug(decision.title, change.id)}`);\n\n // Preserve the complete authored source change tree, including metadata,\n // rename syntax, custom artifacts, and original delta section ordering.\n for (const artifactPath of change.artifacts) {\n const segments = safeArtifactRelative(artifactPath, `changes/${change.id}/`, `OpenSpec change '${change.id}'`);\n writes.push(planWrite(\n join(featureDir, \"legacy\", \"openspec\", ...segments),\n await readSourceArtifact(inventory.root, artifactPath),\n ));\n }\n\n const authored: Array<AuthoredArtifactCopy & { kind: \"proposal\" | \"tasks\" | \"change-design\" }> = [];\n for (const artifactDecision of inventory.artifactDecisions) {\n const artifact = inventoryArtifactsByPath.get(artifactDecision.path);\n if (artifact?.changeId !== change.id) continue;\n const disposition = ownValue(mapping.artifacts, artifactDecision.path);\n if (disposition === undefined) continue; // readiness prevents this on --apply\n // The mapping holds the authored string; everything below compares it\n // against disposition literals, so this is where the value has to become\n // one of them or stop being routed. `mapping.invalid-artifact-disposition`\n // already refuses an unknown one, so readiness prevents this too — but a\n // silent skip is the right failure for a value nothing here can act on.\n if (!isOpenSpecArtifactDisposition(disposition)) continue;\n authored.push({\n path: artifactDecision.path,\n kind: artifactDecision.kind,\n disposition,\n raw: await readSourceArtifact(inventory.root, artifactDecision.path),\n });\n }\n\n const convertedProposals = authored.filter(\n (artifact) => artifact.kind === \"proposal\" && artifact.disposition === \"convert-to-intent\",\n );\n const retainedProposals = authored.filter(\n (artifact) => artifact.kind === \"proposal\" && artifact.disposition !== \"convert-to-intent\",\n );\n const intentBody = convertedProposals.length > 0\n ? renderAuthoredBundle(\"## Migrated OpenSpec proposal\", convertedProposals)\n : \"No proposal was converted into this intent. Review the retained OpenSpec artifacts and migration-plan.json before promotion.\\n\";\n writes.push(planWrite(\n join(featureDir, \"intent.md\"),\n `${migrationFrontmatter(feature, decision.title)}\\n\\n# ${decision.title}\\n\\n<!-- Staged from OpenSpec change '${change.id}'. This feature is deliberately not validation-green yet. -->\\n\\n${intentBody}`,\n ));\n if (retainedProposals.length > 0) {\n writes.push(planWrite(\n join(featureDir, \"legacy\", \"proposal.md\"),\n renderAuthoredBundle(\"# Retained OpenSpec proposal\", retainedProposals),\n ));\n }\n\n const tasks = authored.filter((artifact) => artifact.kind === \"tasks\");\n if (tasks.length > 0) {\n writes.push(planWrite(\n join(featureDir, \"legacy\", \"tasks.md\"),\n renderAuthoredBundle(\"# OpenSpec task checklist\", tasks),\n ));\n }\n const reviewedDesigns = authored.filter(\n (artifact) => artifact.kind === \"change-design\" && artifact.disposition === \"review-as-feature-adr\",\n );\n if (reviewedDesigns.length > 0) {\n writes.push(planWrite(\n join(featureDir, \"adrs\", \"openspec-design.md\"),\n renderAuthoredBundle(\"# OpenSpec design (review required)\", reviewedDesigns),\n ));\n }\n const retainedDesigns = authored.filter(\n (artifact) => artifact.kind === \"change-design\" && artifact.disposition !== \"review-as-feature-adr\",\n );\n if (retainedDesigns.length > 0) {\n writes.push(planWrite(\n join(featureDir, \"legacy\", \"design.md\"),\n renderAuthoredBundle(\"# Retained OpenSpec design\", retainedDesigns),\n ));\n }\n\n if (change.metadata.skipSpecs) continue;\n const deltaByService = new Map<string, Requirement[]>();\n const changeRenames = inventory.renamed.filter(\n (item) => item.scope === \"active\" && item.changeId === change.id,\n );\n const materializedRenameKeys = new Set<string>();\n const addDelta = (service: string, requirement: Requirement, origin: string): void => {\n const existing = deltaByService.get(service) ?? [];\n const collision = existing.find((item) =>\n item.name === requirement.name\n || (item.id !== undefined && requirement.id !== undefined && item.id === requirement.id));\n if (collision !== undefined) {\n const identity = requirement.id === undefined ? requirement.name : requirement.id;\n throw new OpenSpecCommandError(\n \"invalid-option\",\n `Active change '${change.id}' produces colliding requirement '${identity}' in service '${service}' while materializing ${origin}.`,\n );\n }\n existing.push(requirement);\n deltaByService.set(service, existing);\n };\n\n for (const spec of change.specs) {\n const requirements = openspecRequirements(await readSourceArtifact(inventory.root, spec.path))\n .filter((requirement) => requirement.kind !== \"BASE\");\n for (const parsed of requirements) {\n const matchingRenames = changeRenames.filter((rename) =>\n rename.capability === spec.capability\n && rename.from !== null\n && rename.to !== null\n && (parsed.name === rename.from || parsed.name === rename.to));\n if (matchingRenames.length > 1) {\n throw new OpenSpecCommandError(\n \"invalid-option\",\n `Active change '${change.id}' requirement '${parsed.name}' matches multiple RENAMED pairs.`,\n );\n }\n const rename = matchingRenames[0];\n let requirement = parsed;\n let routingName = parsed.name;\n if (rename !== undefined && isSettled(rename)) {\n if (parsed.kind !== \"MODIFIED\") {\n throw new OpenSpecCommandError(\n \"invalid-option\",\n `Active change '${change.id}' combines RENAMED '${rename.from}' → '${rename.to}' with ${parsed.kind} '${parsed.name}'; review the conflicting delta before --apply.`,\n );\n }\n if (parsed.id !== undefined && parsed.id !== rename.requirementId) {\n throw new OpenSpecCommandError(\n \"invalid-option\",\n `Active change '${change.id}' MODIFIED requirement '${parsed.name}' has Requirement-ID '${parsed.id}', but its RENAMED source resolves to '${rename.requirementId}'.`,\n );\n }\n requirement = renamedAsModified(parsed, rename);\n routingName = rename.from;\n materializedRenameKeys.add(rename.key);\n }\n // Every delta kind uniformly: REMOVED is harmless (every check exempts\n // it), and a MODIFIED body MUST carry the line or its wholesale\n // replace would strip the one the living side was staged with.\n const withCapability = requirementWithCapability(requirement, spec.capability);\n for (const service of selectedServices(mapping, spec.capability, routingName)) {\n addDelta(service, withCapability, spec.path);\n }\n }\n }\n for (const rename of changeRenames) {\n if (materializedRenameKeys.has(rename.key)) continue;\n if (!isSettled(rename)) continue;\n const source = (livingByCapability.get(rename.capability) ?? [])\n .filter((requirement) => requirement.name === rename.from)[0];\n if (source === undefined) continue; // readiness prevents this on --apply\n const modified = renamedAsModified(source, rename);\n for (const service of selectedServices(mapping, rename.capability, rename.from)) {\n addDelta(service, requirementWithCapability(modified, rename.capability), rename.path);\n }\n }\n for (const [service, requirements] of [...deltaByService].sort(([a], [b]) => a.localeCompare(b))) {\n assertMaterializedRequirementIds(\n `Active change '${change.id}' mapped to service '${service}'`,\n requirements,\n );\n writes.push(planWrite(\n join(featureDir, \"specs\", service, \"spec.md\"),\n serializeDeltaRequirements(requirements),\n ));\n }\n }\n return writes;\n}\n"]}
|
|
@@ -19,10 +19,11 @@ import { findConfigPath, parseConfig } from "../../../core/envelope/config.js";
|
|
|
19
19
|
import { docsDirOf } from "../../../core/kernel/ids/dirs.js";
|
|
20
20
|
import { CAPABILITY_LINE_RE, requirementIdProblems } from "../../../core/document/spec.js";
|
|
21
21
|
import { decodeDocument } from "../../../core/kernel/document-bytes.js";
|
|
22
|
+
import { isPathInside } from "../../../core/kernel/path-safety.js";
|
|
22
23
|
import {} from "../../../core/openspec/model/model.js";
|
|
23
24
|
import {} from "../../../core/staging/writes.js";
|
|
24
25
|
import { OpenSpecCommandError } from "../openspec/error.js";
|
|
25
|
-
import { canonicalForCreate
|
|
26
|
+
import { canonicalForCreate } from "../openspec/paths.js";
|
|
26
27
|
/**
|
|
27
28
|
* The other half of "never writes into … live loam docs". Overlap with the
|
|
28
29
|
* OpenSpec source was the only thing checked, so `--target <docs>/features` on a
|
|
@@ -39,7 +40,7 @@ async function assertOutsideLoamDocs(stage) {
|
|
|
39
40
|
const governing = findConfigPath(canonicalStage);
|
|
40
41
|
if (governing !== null) {
|
|
41
42
|
const docsDir = await governingDocsDir(governing);
|
|
42
|
-
if (docsDir === null ||
|
|
43
|
+
if (docsDir === null || isPathInside(docsDir, canonicalStage)) {
|
|
43
44
|
throw new OpenSpecCommandError("invalid-option", `Refusing to stage a migration inside the live loam docs ${docsDir ?? `governed by ${governing}`}: ${stage}. Migrate into a standalone directory and cut over after review.`);
|
|
44
45
|
}
|
|
45
46
|
}
|
|
@@ -73,7 +74,7 @@ async function governingDocsDir(path) {
|
|
|
73
74
|
export async function assertEmptyNonOverlappingStage(inventory, stage) {
|
|
74
75
|
const canonicalStage = await canonicalForCreate(stage);
|
|
75
76
|
const canonicalSource = await realpath(inventory.inputRoot);
|
|
76
|
-
if (
|
|
77
|
+
if (isPathInside(canonicalSource, canonicalStage) || isPathInside(canonicalStage, canonicalSource)) {
|
|
77
78
|
throw new OpenSpecCommandError("invalid-option", `Staging directory must not overlap the OpenSpec source workspace: ${stage}`);
|
|
78
79
|
}
|
|
79
80
|
await assertOutsideLoamDocs(stage);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"stage.js","sourceRoot":"","sources":["../../../../src/commands/migrate-openspec/materialize/stage.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Every refusal that stands between a finished migration plan and the disk.\n *\n * Three questions, one module because all three are answered at the same\n * moment and none of them may be skipped: may the target hold these bytes at\n * all (it must be empty, not a symlink, outside the OpenSpec source and outside\n * any live loam fleet), do two planned writes claim one path once portable\n * case/Unicode folding is applied, and would the requirements about to be\n * serialized still parse as a loam spec.\n *\n * They sit beside the writers rather than under `../openspec/` because they\n * grade the *output*: `../openspec/paths.ts` holds the path arithmetic they are\n * built from, and knows nothing about a plan.\n */\nimport { existsSync } from \"node:fs\";\nimport { lstat, readFile, readdir, realpath, stat } from \"node:fs/promises\";\nimport { dirname, join, resolve } from \"node:path\";\nimport { findConfigPath, parseConfig } from \"../../../core/envelope/config.js\";\nimport { docsDirOf, type DocsDir } from \"../../../core/kernel/ids/dirs.js\";\nimport { CAPABILITY_LINE_RE, requirementIdProblems, type Requirement } from \"../../../core/document/spec.js\";\nimport { decodeDocument } from \"../../../core/kernel/document-bytes.js\";\nimport { type OpenSpecInventory } from \"../../../core/openspec/model/model.js\";\nimport { type PlannedWrite } from \"../../../core/staging/writes.js\";\nimport { OpenSpecCommandError } from \"../openspec/error.js\";\nimport { canonicalForCreate, contains } from \"../openspec/paths.js\";\n\n/**\n * The other half of \"never writes into … live loam docs\". Overlap with the\n * OpenSpec source was the only thing checked, so `--target <docs>/features` on a\n * fresh fleet passed and put phantom features into every `loam list`.\n *\n * The line is the docs tree, not the repository: every enumerating command reads\n * `<docsDir>/services` and `<docsDir>/features`, so a target under docsDir joins\n * the live fleet, while a sibling of it is merely a directory. A config that\n * governs the target but cannot be parsed is refused too — an unreadable\n * docsDir cannot be shown to be somewhere else.\n */\nasync function assertOutsideLoamDocs(stage: string): Promise<void> {\n const canonicalStage = await canonicalForCreate(stage);\n const governing = findConfigPath(canonicalStage);\n if (governing !== null) {\n const docsDir = await governingDocsDir(governing);\n if (docsDir === null || contains(docsDir, canonicalStage)) {\n throw new OpenSpecCommandError(\n \"invalid-option\",\n `Refusing to stage a migration inside the live loam docs ${docsDir ?? `governed by ${governing}`}: ${stage}. Migrate into a standalone directory and cut over after review.`,\n );\n }\n }\n // A docs repo checked out without its loam.json — the landscape is the file\n // no docs repo is without, and `loam init` scaffolds it before anything else.\n for (let dir = canonicalStage; ; dir = dirname(dir)) {\n if (existsSync(join(dir, \"architecture\", \"landscape.likec4\"))) {\n throw new OpenSpecCommandError(\n \"invalid-option\",\n `Refusing to stage a migration inside a loam docs repository: ${join(dir, \"architecture\", \"landscape.likec4\")} is above ${stage}. Migrate into a standalone directory and cut over after review.`,\n );\n }\n if (dirname(dir) === dir) return;\n }\n}\n\n/**\n * Canonical docsDir of the config at `path`, or null when it cannot be read —\n * including \"read as UTF-8 it would have said something else\", which is why the\n * bytes go through `decodeDocument`. Null is the conservative answer here: the\n * caller refuses to stage rather than accept a docsDir it cannot establish.\n */\nasync function governingDocsDir(path: string): Promise<DocsDir | null> {\n try {\n // Re-branded after canonicalization: the path came out of `parseConfig`\n // already resolved (a `DocsDir`), and `canonicalForCreate` only follows\n // symlinks — same root, same provenance, canonical spelling.\n return docsDirOf(await canonicalForCreate(\n parseConfig(decodeDocument(await readFile(path), path), dirname(path)).docsDir,\n ));\n } catch {\n return null;\n }\n}\n\nexport async function assertEmptyNonOverlappingStage(inventory: OpenSpecInventory, stage: string): Promise<void> {\n const canonicalStage = await canonicalForCreate(stage);\n const canonicalSource = await realpath(inventory.inputRoot);\n if (contains(canonicalSource, canonicalStage) || contains(canonicalStage, canonicalSource)) {\n throw new OpenSpecCommandError(\n \"invalid-option\",\n `Staging directory must not overlap the OpenSpec source workspace: ${stage}`,\n );\n }\n await assertOutsideLoamDocs(stage);\n try {\n if ((await lstat(stage)).isSymbolicLink()) {\n throw new OpenSpecCommandError(\n \"invalid-option\",\n `Staging target must not be a symbolic link: ${stage}`,\n );\n }\n } catch (error) {\n if (error instanceof OpenSpecCommandError) throw error;\n const code = typeof error === \"object\" && error !== null && \"code\" in error ? error.code : null;\n if (code !== \"ENOENT\") throw error;\n }\n if (!existsSync(stage)) return;\n const info = await stat(stage);\n if (!info.isDirectory() || (await readdir(stage)).length > 0) {\n throw new OpenSpecCommandError(\"already-exists\", `Staging target must be absent or an empty directory: ${stage}`);\n }\n}\n\nexport function assertDistinctPlannedPaths(writes: PlannedWrite[]): void {\n const writePaths = new Map<string, string>();\n for (const write of writes) {\n const collisionKey = resolve(write.path).normalize(\"NFC\").toLowerCase();\n const existing = writePaths.get(collisionKey);\n if (existing !== undefined) {\n throw new OpenSpecCommandError(\n \"invalid-option\",\n `Migration paths '${existing}' and '${write.path}' collide after portable case/Unicode normalization; refine mappings before apply.`,\n );\n }\n writePaths.set(collisionKey, write.path);\n }\n}\n\n/**\n * The requirement with a `Capability: <id>` body line — the join that keeps\n * the source capability's identity on every requirement it routed, instead of\n * dissolving it into service files and leaving the id only in legacy/. The\n * serializeRequirements Requirement-ID pattern: never a second line when the\n * author already wrote one (a rare but legal delta authored against a loam\n * target), and appended after the body the way active.ts appends its\n * OpenSpec-Living-Source annotation, so a MODIFIED requirement's wholesale\n * replace carries the line into the living document rather than stripping it.\n */\nexport function requirementWithCapability(requirement: Requirement, capabilityId: string): Requirement {\n if (requirement.text.some((line) => CAPABILITY_LINE_RE.test(line))) return requirement;\n const line = `Capability: ${capabilityId}`;\n return {\n ...requirement,\n capabilities: [capabilityId],\n text: requirement.text.length === 0 ? [line] : [...requirement.text, \"\", line],\n };\n}\n\nexport function assertMaterializedRequirementIds(where: string, requirements: Requirement[]): void {\n const problems = requirementIdProblems(requirements);\n if (problems.length === 0) return;\n const first = problems[0]!;\n const detail = first.kind === \"invalid\"\n ? `requirement '${first.requirement}' has invalid Requirement-ID '${first.value}'`\n : first.kind === \"repeated\"\n ? `requirement '${first.requirement}' declares Requirement-ID ${first.values.length} times`\n : `Requirement-ID '${first.id}' is shared by ${first.requirements.map((name) => `'${name}'`).join(\", \")}`;\n throw new OpenSpecCommandError(\n \"invalid-option\",\n `${where} would materialize an invalid loam spec: ${detail}. Repair the source or refine routing before apply.`,\n );\n}\n"]}
|
|
1
|
+
{"version":3,"file":"stage.js","sourceRoot":"","sources":["../../../../src/commands/migrate-openspec/materialize/stage.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Every refusal that stands between a finished migration plan and the disk.\n *\n * Three questions, one module because all three are answered at the same\n * moment and none of them may be skipped: may the target hold these bytes at\n * all (it must be empty, not a symlink, outside the OpenSpec source and outside\n * any live loam fleet), do two planned writes claim one path once portable\n * case/Unicode folding is applied, and would the requirements about to be\n * serialized still parse as a loam spec.\n *\n * They sit beside the writers rather than under `../openspec/` because they\n * grade the *output*: `../openspec/paths.ts` holds the path arithmetic they are\n * built from, and knows nothing about a plan.\n */\nimport { existsSync } from \"node:fs\";\nimport { lstat, readFile, readdir, realpath, stat } from \"node:fs/promises\";\nimport { dirname, join, resolve } from \"node:path\";\nimport { findConfigPath, parseConfig } from \"../../../core/envelope/config.js\";\nimport { docsDirOf, type DocsDir } from \"../../../core/kernel/ids/dirs.js\";\nimport { CAPABILITY_LINE_RE, requirementIdProblems, type Requirement } from \"../../../core/document/spec.js\";\nimport { decodeDocument } from \"../../../core/kernel/document-bytes.js\";\nimport { isPathInside } from \"../../../core/kernel/path-safety.js\";\nimport { type OpenSpecInventory } from \"../../../core/openspec/model/model.js\";\nimport { type PlannedWrite } from \"../../../core/staging/writes.js\";\nimport { OpenSpecCommandError } from \"../openspec/error.js\";\nimport { canonicalForCreate } from \"../openspec/paths.js\";\n\n/**\n * The other half of \"never writes into … live loam docs\". Overlap with the\n * OpenSpec source was the only thing checked, so `--target <docs>/features` on a\n * fresh fleet passed and put phantom features into every `loam list`.\n *\n * The line is the docs tree, not the repository: every enumerating command reads\n * `<docsDir>/services` and `<docsDir>/features`, so a target under docsDir joins\n * the live fleet, while a sibling of it is merely a directory. A config that\n * governs the target but cannot be parsed is refused too — an unreadable\n * docsDir cannot be shown to be somewhere else.\n */\nasync function assertOutsideLoamDocs(stage: string): Promise<void> {\n const canonicalStage = await canonicalForCreate(stage);\n const governing = findConfigPath(canonicalStage);\n if (governing !== null) {\n const docsDir = await governingDocsDir(governing);\n if (docsDir === null || isPathInside(docsDir, canonicalStage)) {\n throw new OpenSpecCommandError(\n \"invalid-option\",\n `Refusing to stage a migration inside the live loam docs ${docsDir ?? `governed by ${governing}`}: ${stage}. Migrate into a standalone directory and cut over after review.`,\n );\n }\n }\n // A docs repo checked out without its loam.json — the landscape is the file\n // no docs repo is without, and `loam init` scaffolds it before anything else.\n for (let dir = canonicalStage; ; dir = dirname(dir)) {\n if (existsSync(join(dir, \"architecture\", \"landscape.likec4\"))) {\n throw new OpenSpecCommandError(\n \"invalid-option\",\n `Refusing to stage a migration inside a loam docs repository: ${join(dir, \"architecture\", \"landscape.likec4\")} is above ${stage}. Migrate into a standalone directory and cut over after review.`,\n );\n }\n if (dirname(dir) === dir) return;\n }\n}\n\n/**\n * Canonical docsDir of the config at `path`, or null when it cannot be read —\n * including \"read as UTF-8 it would have said something else\", which is why the\n * bytes go through `decodeDocument`. Null is the conservative answer here: the\n * caller refuses to stage rather than accept a docsDir it cannot establish.\n */\nasync function governingDocsDir(path: string): Promise<DocsDir | null> {\n try {\n // Re-branded after canonicalization: the path came out of `parseConfig`\n // already resolved (a `DocsDir`), and `canonicalForCreate` only follows\n // symlinks — same root, same provenance, canonical spelling.\n return docsDirOf(await canonicalForCreate(\n parseConfig(decodeDocument(await readFile(path), path), dirname(path)).docsDir,\n ));\n } catch {\n return null;\n }\n}\n\nexport async function assertEmptyNonOverlappingStage(inventory: OpenSpecInventory, stage: string): Promise<void> {\n const canonicalStage = await canonicalForCreate(stage);\n const canonicalSource = await realpath(inventory.inputRoot);\n if (isPathInside(canonicalSource, canonicalStage) || isPathInside(canonicalStage, canonicalSource)) {\n throw new OpenSpecCommandError(\n \"invalid-option\",\n `Staging directory must not overlap the OpenSpec source workspace: ${stage}`,\n );\n }\n await assertOutsideLoamDocs(stage);\n try {\n if ((await lstat(stage)).isSymbolicLink()) {\n throw new OpenSpecCommandError(\n \"invalid-option\",\n `Staging target must not be a symbolic link: ${stage}`,\n );\n }\n } catch (error) {\n if (error instanceof OpenSpecCommandError) throw error;\n const code = typeof error === \"object\" && error !== null && \"code\" in error ? error.code : null;\n if (code !== \"ENOENT\") throw error;\n }\n if (!existsSync(stage)) return;\n const info = await stat(stage);\n if (!info.isDirectory() || (await readdir(stage)).length > 0) {\n throw new OpenSpecCommandError(\"already-exists\", `Staging target must be absent or an empty directory: ${stage}`);\n }\n}\n\nexport function assertDistinctPlannedPaths(writes: PlannedWrite[]): void {\n const writePaths = new Map<string, string>();\n for (const write of writes) {\n const collisionKey = resolve(write.path).normalize(\"NFC\").toLowerCase();\n const existing = writePaths.get(collisionKey);\n if (existing !== undefined) {\n throw new OpenSpecCommandError(\n \"invalid-option\",\n `Migration paths '${existing}' and '${write.path}' collide after portable case/Unicode normalization; refine mappings before apply.`,\n );\n }\n writePaths.set(collisionKey, write.path);\n }\n}\n\n/**\n * The requirement with a `Capability: <id>` body line — the join that keeps\n * the source capability's identity on every requirement it routed, instead of\n * dissolving it into service files and leaving the id only in legacy/. The\n * serializeRequirements Requirement-ID pattern: never a second line when the\n * author already wrote one (a rare but legal delta authored against a loam\n * target), and appended after the body the way active.ts appends its\n * OpenSpec-Living-Source annotation, so a MODIFIED requirement's wholesale\n * replace carries the line into the living document rather than stripping it.\n */\nexport function requirementWithCapability(requirement: Requirement, capabilityId: string): Requirement {\n if (requirement.text.some((line) => CAPABILITY_LINE_RE.test(line))) return requirement;\n const line = `Capability: ${capabilityId}`;\n return {\n ...requirement,\n capabilities: [capabilityId],\n text: requirement.text.length === 0 ? [line] : [...requirement.text, \"\", line],\n };\n}\n\nexport function assertMaterializedRequirementIds(where: string, requirements: Requirement[]): void {\n const problems = requirementIdProblems(requirements);\n if (problems.length === 0) return;\n const first = problems[0]!;\n const detail = first.kind === \"invalid\"\n ? `requirement '${first.requirement}' has invalid Requirement-ID '${first.value}'`\n : first.kind === \"repeated\"\n ? `requirement '${first.requirement}' declares Requirement-ID ${first.values.length} times`\n : `Requirement-ID '${first.id}' is shared by ${first.requirements.map((name) => `'${name}'`).join(\", \")}`;\n throw new OpenSpecCommandError(\n \"invalid-option\",\n `${where} would materialize an invalid loam spec: ${detail}. Repair the source or refine routing before apply.`,\n );\n}\n"]}
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
import { mkdir } from "node:fs/promises";
|
|
12
12
|
import { join } from "node:path";
|
|
13
13
|
import { stringify as stringifyYaml } from "yaml";
|
|
14
|
-
import {
|
|
14
|
+
import { openspecRequirements } from "../../../core/openspec/scan/shape.js";
|
|
15
15
|
import {} from "../../../core/document/spec.js";
|
|
16
16
|
import { DOCS_SUBDIRS, docsRepoFiles } from "../../../core/docs.js";
|
|
17
17
|
import { compareIds } from "../../../core/repo/entries.js";
|
|
@@ -35,7 +35,7 @@ export async function planMigrationWrites(inventory, mapping, target) {
|
|
|
35
35
|
// living spec built from the other.
|
|
36
36
|
const livingByCapability = new Map();
|
|
37
37
|
for (const capability of inventory.living.capabilities) {
|
|
38
|
-
livingByCapability.set(capability.id,
|
|
38
|
+
livingByCapability.set(capability.id, openspecRequirements(await readSourceArtifact(inventory.root, capability.files[0])));
|
|
39
39
|
}
|
|
40
40
|
const livingWrites = await materializeLivingSpecs(inventory, mapping, target, livingByCapability);
|
|
41
41
|
const vocabulary = capabilitiesVocabularyWrite(inventory, target);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"target.js","sourceRoot":"","sources":["../../../../src/commands/migrate-openspec/materialize/target.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The staged target: plan every byte first, then land them all or none.\n *\n * The two halves are one module because they are one transaction. Planning\n * reads the whole source corpus, so `writeMigrationTarget` re-hashes it\n * afterwards and refuses a snapshot that moved mid-flight; the plan then\n * becomes an exclusive staged write set that rolls back as a unit. Splitting\n * the planner from the writer would put that re-hash on one side of a module\n * boundary and the reads it guards on the other.\n */\nimport { mkdir } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport { stringify as stringifyYaml } from \"yaml\";\nimport { parseRequirements } from \"../../../core/document/parse.js\";\nimport { type Requirement } from \"../../../core/document/spec.js\";\nimport { DOCS_SUBDIRS, docsRepoFiles } from \"../../../core/docs.js\";\nimport { compareIds } from \"../../../core/repo/entries.js\";\nimport { ownValue } from \"../../../core/kernel/records.js\";\nimport { inventoryOpenSpec } from \"../../../core/openspec/inventory.js\";\nimport { type OpenSpecMapping } from \"../../../core/openspec/model/mapping.js\";\nimport { type OpenSpecInventory } from \"../../../core/openspec/model/model.js\";\nimport { rollbackError, rollbackStaged, stageWrites, swapStaged } from \"../../../core/staging/commit.js\";\nimport { planWrite, type PlannedWrite } from \"../../../core/staging/writes.js\";\nimport { normalizedMapping } from \"../openspec/decisions.js\";\nimport { OpenSpecCommandError } from \"../openspec/error.js\";\nimport { safeArtifactRelative } from \"../openspec/paths.js\";\nimport { readSourceArtifact } from \"../openspec/read.js\";\nimport { materializeActiveChanges } from \"./active.js\";\nimport { materializeLivingSpecs } from \"./living.js\";\nimport { assertDistinctPlannedPaths, assertEmptyNonOverlappingStage } from \"./stage.js\";\n\nexport async function planMigrationWrites(\n inventory: OpenSpecInventory,\n mapping: OpenSpecMapping,\n target: string,\n): Promise<{ livingWrites: PlannedWrite[]; activeWrites: PlannedWrite[]; legacyWrites: PlannedWrite[] }> {\n // One reading of the living corpus for both materializers. They each read and\n // parsed the whole of it independently, which is twice the work and, worse,\n // two readings: a rename resolved against one of them was written beside a\n // living spec built from the other.\n const livingByCapability = new Map<string, Requirement[]>();\n for (const capability of inventory.living.capabilities) {\n livingByCapability.set(\n capability.id,\n parseRequirements(await readSourceArtifact(inventory.root, capability.files[0]!)),\n );\n }\n const livingWrites = await materializeLivingSpecs(inventory, mapping, target, livingByCapability);\n const vocabulary = capabilitiesVocabularyWrite(inventory, target);\n if (vocabulary !== null) livingWrites.push(vocabulary);\n const activeWrites = await materializeActiveChanges(inventory, mapping, target, livingByCapability);\n const legacyWrites = await copyLivingTree(inventory, target);\n assertDistinctPlannedPaths([...livingWrites, ...activeWrites, ...legacyWrites]);\n return { livingWrites, activeWrites, legacyWrites };\n}\n\n/**\n * The staged fleet's capability vocabulary: the union of living and\n * active-horizon capability ids, each declared with an EMPTY body — no\n * description is invented, because the authored `## Purpose` prose stays\n * verbatim under legacy/ (the no-prose-moves rule), and an empty mapping is a\n * legal declaration. Keys sorted with compareIds over an insertion-ordered\n * object, so the bytes are machine-independent; nested ids stay one flat key\n * (`payments/refunds`), never collapsed. Null when the corpus names no\n * capability at all — an empty vocabulary FILE would opt the staged fleet into\n * an axis it has nothing to say on. assertDistinctPlannedPaths guards the path\n * against every other planned write; the docs scaffold does not write it.\n */\nfunction capabilitiesVocabularyWrite(inventory: OpenSpecInventory, target: string): PlannedWrite | null {\n const ids = new Set<string>(inventory.living.capabilities.map((capability) => capability.id));\n for (const change of inventory.changes.active) {\n for (const spec of change.specs) ids.add(spec.capability);\n }\n if (ids.size === 0) return null;\n const capabilities = Object.fromEntries([...ids].sort(compareIds).map((id) => [id, {}]));\n return planWrite(\n join(target, \"architecture\", \"capabilities.yaml\"),\n \"# Declared by `loam migrate-openspec`: every living and active-horizon OpenSpec\\n\" +\n \"# capability id, so capability identity survives migration as a checkable name\\n\" +\n \"# (each routed requirement carries a matching `Capability:` line). Descriptions\\n\" +\n \"# are deliberately absent — the authored Purpose prose is verbatim under\\n\" +\n \"# legacy/openspec/specs/, and no prose moves.\\n\" +\n stringifyYaml({ capabilities }, { lineWidth: 0 }),\n );\n}\n\n/**\n * The living tree, byte for byte, the way each change tree already gets\n * `legacy/openspec/`.\n *\n * `materializeLivingSpecs` emits frontmatter plus serialized requirements, so\n * everything a living spec holds AROUND its requirements has nowhere to land:\n * `## Purpose`, prose between `## Requirements` and the first requirement, and\n * the whole of `specs/<capability>/design.md` — which the summary counts under\n * `review-as-service-adr` while FOLLOW-UP.md tells the migrator to review prose\n * that had been written nowhere. (Prose inside a requirement body survives\n * serialization and is not the subject here.)\n */\nasync function copyLivingTree(inventory: OpenSpecInventory, target: string): Promise<PlannedWrite[]> {\n const writes: PlannedWrite[] = [];\n for (const artifact of inventory.artifacts) {\n if (artifact.scope !== \"living\") continue;\n const segments = safeArtifactRelative(artifact.path, \"specs/\", \"The OpenSpec living tree\");\n writes.push(planWrite(\n join(target, \"legacy\", \"openspec\", \"specs\", ...segments),\n await readSourceArtifact(inventory.root, artifact.path),\n ));\n }\n return writes;\n}\n\nexport async function writeMigrationTarget(\n inventory: OpenSpecInventory,\n mapping: OpenSpecMapping,\n target: string,\n planned: { livingWrites: PlannedWrite[]; activeWrites: PlannedWrite[]; legacyWrites: PlannedWrite[] },\n): Promise<{ directory: string; files: string[]; followUpBlockers: string[] }> {\n await assertEmptyNonOverlappingStage(inventory, target);\n // All migration content was read while planning. Re-hash after those reads so\n // a source edit between audit and materialization cannot produce a mixed snapshot.\n const refreshed = await inventoryOpenSpec(inventory.inputRoot, { mapping });\n if (!refreshed.ready || refreshed.inventoryDigest !== inventory.inventoryDigest) {\n throw new OpenSpecCommandError(\n \"invalid-option\",\n \"OpenSpec source changed while the migration plan was being materialized; re-audit and review a new mapping.\",\n );\n }\n const followUpBlockers = [\n \"Model every staged service in architecture/landscape.likec4 and bind it with metadata { service '<id>' }; the scaffolded landscape is deliberately empty.\",\n \"Add each service model.likec4 and OpenAPI contract; migrated requirements have no Operations/Covers links yet unless authored upstream.\",\n \"Set truthful sources from each service repository, validate, and have a human run loam vouch; staged specs remain status: draft.\",\n ...(inventory.changes.active.length === 0\n ? []\n : [\n `Review ${inventory.changes.active.length} staged feature(s), add their LikeC4/OpenAPI deltas, and resolve any retained or manual-review OpenSpec artifacts recorded in migration-plan.json.`,\n // OpenSpec has no equivalent of `Based-On:`, so every migrated\n // MODIFIED/REMOVED requirement arrives unpinned (`delta.baseline-missing`,\n // a warning that GATES archive — unpinned is the shape that reverts\n // someone else's landed change). The corpus is not stranded: one\n // rebase per staged feature pins everything, which is exactly the\n // step this line asks for.\n \"Run `loam rebase <FEAT>` per staged feature: OpenSpec deltas carry no Based-On, so no MODIFIED/REMOVED requirement is pinned to the living text it was written against.\",\n ]),\n \"Review config context/rules, Stores/references, custom schemas, and every artifact disposition in migration-plan.json.\",\n \"Review the verbatim OpenSpec living tree under legacy/openspec/specs/: Purpose prose, section prose, and capability design.md have no loam equivalent and were copied, not converted.\",\n // The cutover was undocumented, which is how a staged target that cannot\n // answer `loam validate` reads as finished work. Naming the two halves —\n // what is fleet content and what is review residue — is the whole procedure.\n \"Cut over only once `loam validate --all` here is green: services/ and features/ are the fleet content, while legacy/, mapping.yaml, migration-plan.json, FOLLOW-UP.md, and this repository's own loam.json/AGENTS.md are review residue that does not move.\",\n ];\n const plan = {\n version: 1,\n generatedBy: \"loam migrate-openspec\",\n source: { inputRoot: inventory.inputRoot, root: inventory.root },\n baselines: inventory.baselines,\n readiness: inventory.readiness,\n mapping: normalizedMapping(inventory, mapping),\n living: inventory.living,\n activeChanges: inventory.changes.active,\n archiveDiagnostics: inventory.archiveDiagnostics,\n artifactDisposition: inventory.artifacts.map((artifact) => ({\n ...artifact,\n disposition: ownValue(mapping.artifacts, artifact.path) ?? artifact.disposition,\n selectedDisposition: ownValue(mapping.artifacts, artifact.path) ?? null,\n })),\n followUpBlockers,\n note: \"Staged migration docs only. Living specs and mapped active changes were materialized without changing the OpenSpec source or live loam docs. The result is intentionally not expected to validate green until the listed architecture, API, provenance, and review work is completed.\",\n };\n const writes: PlannedWrite[] = [\n planWrite(join(target, \"migration-plan.json\"), `${JSON.stringify(plan, null, 2)}\\n`),\n planWrite(join(target, \"mapping.yaml\"), stringifyYaml(normalizedMapping(inventory, mapping), { lineWidth: 0 })),\n planWrite(join(target, \"FOLLOW-UP.md\"), renderFollowUp(followUpBlockers)),\n ...docsRepoScaffold(target),\n ...planned.livingWrites,\n ...planned.activeWrites,\n ...planned.legacyWrites,\n ];\n assertDistinctPlannedPaths(writes);\n for (const write of writes) write.exclusive = true;\n const staged = await stageWrites(writes);\n if (staged.some((item) => item.before !== null)) {\n await rollbackStaged(staged);\n throw new OpenSpecCommandError(\n \"already-exists\",\n \"Staging target changed after its emptiness check; refusing to overwrite a concurrently created file.\",\n );\n }\n try {\n await swapStaged(staged);\n } catch (error) {\n const failures = await rollbackStaged(staged);\n const code = typeof error === \"object\" && error !== null && \"code\" in error ? error.code : null;\n if (code === \"EEXIST\" && failures.length === 0) {\n throw new OpenSpecCommandError(\n \"already-exists\",\n \"Staging target changed during apply; the migration was rolled back without overwriting the concurrent file.\",\n );\n }\n throw rollbackError(error, failures);\n }\n // The skeleton `loam init` lays down, after the content that fills it. A docs\n // repo is recognised by `services/` plus AGENTS.md, and until now `services/`\n // existed only if some capability happened to route there: migrate a corpus\n // whose requirements all live under `changes/` — legitimate, and the shape a\n // greenfield OpenSpec workspace has — and the target came out with no\n // `services/` at all, so `loam init --docs <target> --service <id>` answered\n // \"not a docs repo\" and every read command refused it as `services-missing`.\n // Empty directories, created after the swap so a rolled-back apply leaves the\n // target as empty as it found it.\n for (const dir of DOCS_SUBDIRS) await mkdir(join(target, dir), { recursive: true });\n return { directory: target, files: writes.map((write) => write.path), followUpBlockers };\n}\n\n/**\n * The staged target is a real docs repo, not a folder of markdown.\n *\n * FOLLOW-UP.md tells the migrator to validate, rebase and vouch against this\n * directory, and every one of those commands resolves its fleet from loam.json:\n * without these three files none of the instructions it gives can be executed,\n * and the reviewer has no way to see what is still missing. The landscape is\n * laid down EMPTY for the same reason `loam init` lays it down empty — an absent\n * landscape silences every cross-service check, and a generated one would be a\n * guess presented as the map. Each staged service then shows up as exactly one\n * `landscape.service-unmodelled` error, which is the first follow-up item.\n *\n * The bytes come from `docs.ts`, so this IS the repo `loam init` makes rather\n * than a lookalike. Only the landscape's opening comment differs, and it differs\n * through a parameter: the migrator needs to be told why their map is empty,\n * and everything after that sentence is advice that does not depend on how the\n * repo was created.\n */\nfunction docsRepoScaffold(target: string): PlannedWrite[] {\n return docsRepoFiles({\n landscapePreamble: \"// Fleet map for a staged OpenSpec migration. It arrives EMPTY because\\n\"\n + \"// OpenSpec has no topology: loam has nothing to read here and will not\\n\"\n + \"// guess \\\"who calls whom\\\" into the source of truth. Every services/<id>/\\n\"\n + \"// staged beside this file needs an element and a binding below — until\\n\"\n + \"// then each one is a `landscape.service-unmodelled` error.\",\n }).map(([rel, content]) => planWrite(join(target, rel), content));\n}\n\nfunction renderFollowUp(blockers: string[]): string {\n return `# Staged OpenSpec migration: required follow-up\\n\\nThis target contains migrated living requirements, mapped active feature deltas, preserved source artifacts, and a review plan. It is deliberately not presented as a fully valid loam fleet. Complete every item before moving it into live docs.\\n\\n${blockers.map((item) => `- [ ] ${item}`).join(\"\\n\")}\\n`;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"target.js","sourceRoot":"","sources":["../../../../src/commands/migrate-openspec/materialize/target.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The staged target: plan every byte first, then land them all or none.\n *\n * The two halves are one module because they are one transaction. Planning\n * reads the whole source corpus, so `writeMigrationTarget` re-hashes it\n * afterwards and refuses a snapshot that moved mid-flight; the plan then\n * becomes an exclusive staged write set that rolls back as a unit. Splitting\n * the planner from the writer would put that re-hash on one side of a module\n * boundary and the reads it guards on the other.\n */\nimport { mkdir } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport { stringify as stringifyYaml } from \"yaml\";\nimport { openspecRequirements } from \"../../../core/openspec/scan/shape.js\";\nimport { type Requirement } from \"../../../core/document/spec.js\";\nimport { DOCS_SUBDIRS, docsRepoFiles } from \"../../../core/docs.js\";\nimport { compareIds } from \"../../../core/repo/entries.js\";\nimport { ownValue } from \"../../../core/kernel/records.js\";\nimport { inventoryOpenSpec } from \"../../../core/openspec/inventory.js\";\nimport { type OpenSpecMapping } from \"../../../core/openspec/model/mapping.js\";\nimport { type OpenSpecInventory } from \"../../../core/openspec/model/model.js\";\nimport { rollbackError, rollbackStaged, stageWrites, swapStaged } from \"../../../core/staging/commit.js\";\nimport { planWrite, type PlannedWrite } from \"../../../core/staging/writes.js\";\nimport { normalizedMapping } from \"../openspec/decisions.js\";\nimport { OpenSpecCommandError } from \"../openspec/error.js\";\nimport { safeArtifactRelative } from \"../openspec/paths.js\";\nimport { readSourceArtifact } from \"../openspec/read.js\";\nimport { materializeActiveChanges } from \"./active.js\";\nimport { materializeLivingSpecs } from \"./living.js\";\nimport { assertDistinctPlannedPaths, assertEmptyNonOverlappingStage } from \"./stage.js\";\n\nexport async function planMigrationWrites(\n inventory: OpenSpecInventory,\n mapping: OpenSpecMapping,\n target: string,\n): Promise<{ livingWrites: PlannedWrite[]; activeWrites: PlannedWrite[]; legacyWrites: PlannedWrite[] }> {\n // One reading of the living corpus for both materializers. They each read and\n // parsed the whole of it independently, which is twice the work and, worse,\n // two readings: a rename resolved against one of them was written beside a\n // living spec built from the other.\n const livingByCapability = new Map<string, Requirement[]>();\n for (const capability of inventory.living.capabilities) {\n livingByCapability.set(\n capability.id,\n openspecRequirements(await readSourceArtifact(inventory.root, capability.files[0]!)),\n );\n }\n const livingWrites = await materializeLivingSpecs(inventory, mapping, target, livingByCapability);\n const vocabulary = capabilitiesVocabularyWrite(inventory, target);\n if (vocabulary !== null) livingWrites.push(vocabulary);\n const activeWrites = await materializeActiveChanges(inventory, mapping, target, livingByCapability);\n const legacyWrites = await copyLivingTree(inventory, target);\n assertDistinctPlannedPaths([...livingWrites, ...activeWrites, ...legacyWrites]);\n return { livingWrites, activeWrites, legacyWrites };\n}\n\n/**\n * The staged fleet's capability vocabulary: the union of living and\n * active-horizon capability ids, each declared with an EMPTY body — no\n * description is invented, because the authored `## Purpose` prose stays\n * verbatim under legacy/ (the no-prose-moves rule), and an empty mapping is a\n * legal declaration. Keys sorted with compareIds over an insertion-ordered\n * object, so the bytes are machine-independent; nested ids stay one flat key\n * (`payments/refunds`), never collapsed. Null when the corpus names no\n * capability at all — an empty vocabulary FILE would opt the staged fleet into\n * an axis it has nothing to say on. assertDistinctPlannedPaths guards the path\n * against every other planned write; the docs scaffold does not write it.\n */\nfunction capabilitiesVocabularyWrite(inventory: OpenSpecInventory, target: string): PlannedWrite | null {\n const ids = new Set<string>(inventory.living.capabilities.map((capability) => capability.id));\n for (const change of inventory.changes.active) {\n for (const spec of change.specs) ids.add(spec.capability);\n }\n if (ids.size === 0) return null;\n const capabilities = Object.fromEntries([...ids].sort(compareIds).map((id) => [id, {}]));\n return planWrite(\n join(target, \"architecture\", \"capabilities.yaml\"),\n \"# Declared by `loam migrate-openspec`: every living and active-horizon OpenSpec\\n\" +\n \"# capability id, so capability identity survives migration as a checkable name\\n\" +\n \"# (each routed requirement carries a matching `Capability:` line). Descriptions\\n\" +\n \"# are deliberately absent — the authored Purpose prose is verbatim under\\n\" +\n \"# legacy/openspec/specs/, and no prose moves.\\n\" +\n stringifyYaml({ capabilities }, { lineWidth: 0 }),\n );\n}\n\n/**\n * The living tree, byte for byte, the way each change tree already gets\n * `legacy/openspec/`.\n *\n * `materializeLivingSpecs` emits frontmatter plus serialized requirements, so\n * everything a living spec holds AROUND its requirements has nowhere to land:\n * `## Purpose`, prose between `## Requirements` and the first requirement, and\n * the whole of `specs/<capability>/design.md` — which the summary counts under\n * `review-as-service-adr` while FOLLOW-UP.md tells the migrator to review prose\n * that had been written nowhere. (Prose inside a requirement body survives\n * serialization and is not the subject here.)\n */\nasync function copyLivingTree(inventory: OpenSpecInventory, target: string): Promise<PlannedWrite[]> {\n const writes: PlannedWrite[] = [];\n for (const artifact of inventory.artifacts) {\n if (artifact.scope !== \"living\") continue;\n const segments = safeArtifactRelative(artifact.path, \"specs/\", \"The OpenSpec living tree\");\n writes.push(planWrite(\n join(target, \"legacy\", \"openspec\", \"specs\", ...segments),\n await readSourceArtifact(inventory.root, artifact.path),\n ));\n }\n return writes;\n}\n\nexport async function writeMigrationTarget(\n inventory: OpenSpecInventory,\n mapping: OpenSpecMapping,\n target: string,\n planned: { livingWrites: PlannedWrite[]; activeWrites: PlannedWrite[]; legacyWrites: PlannedWrite[] },\n): Promise<{ directory: string; files: string[]; followUpBlockers: string[] }> {\n await assertEmptyNonOverlappingStage(inventory, target);\n // All migration content was read while planning. Re-hash after those reads so\n // a source edit between audit and materialization cannot produce a mixed snapshot.\n const refreshed = await inventoryOpenSpec(inventory.inputRoot, { mapping });\n if (!refreshed.ready || refreshed.inventoryDigest !== inventory.inventoryDigest) {\n throw new OpenSpecCommandError(\n \"invalid-option\",\n \"OpenSpec source changed while the migration plan was being materialized; re-audit and review a new mapping.\",\n );\n }\n const followUpBlockers = [\n \"Model every staged service in architecture/landscape.likec4 and bind it with metadata { service '<id>' }; the scaffolded landscape is deliberately empty.\",\n \"Add each service model.likec4 and OpenAPI contract; migrated requirements have no Operations/Covers links yet unless authored upstream.\",\n \"Set truthful sources from each service repository, validate, and have a human run loam vouch; staged specs remain status: draft.\",\n ...(inventory.changes.active.length === 0\n ? []\n : [\n `Review ${inventory.changes.active.length} staged feature(s), add their LikeC4/OpenAPI deltas, and resolve any retained or manual-review OpenSpec artifacts recorded in migration-plan.json.`,\n // OpenSpec has no equivalent of `Based-On:`, so every migrated\n // MODIFIED/REMOVED requirement arrives unpinned (`delta.baseline-missing`,\n // a warning that GATES archive — unpinned is the shape that reverts\n // someone else's landed change). The corpus is not stranded: one\n // rebase per staged feature pins everything, which is exactly the\n // step this line asks for.\n \"Run `loam rebase <FEAT>` per staged feature: OpenSpec deltas carry no Based-On, so no MODIFIED/REMOVED requirement is pinned to the living text it was written against.\",\n ]),\n \"Review config context/rules, Stores/references, custom schemas, and every artifact disposition in migration-plan.json.\",\n \"Review the verbatim OpenSpec living tree under legacy/openspec/specs/: Purpose prose, section prose, and capability design.md have no loam equivalent and were copied, not converted.\",\n // The cutover was undocumented, which is how a staged target that cannot\n // answer `loam validate` reads as finished work. Naming the two halves —\n // what is fleet content and what is review residue — is the whole procedure.\n \"Cut over only once `loam validate --all` here is green: services/ and features/ are the fleet content, while legacy/, mapping.yaml, migration-plan.json, FOLLOW-UP.md, and this repository's own loam.json/AGENTS.md are review residue that does not move.\",\n ];\n const plan = {\n version: 1,\n generatedBy: \"loam migrate-openspec\",\n source: { inputRoot: inventory.inputRoot, root: inventory.root },\n baselines: inventory.baselines,\n readiness: inventory.readiness,\n mapping: normalizedMapping(inventory, mapping),\n living: inventory.living,\n activeChanges: inventory.changes.active,\n archiveDiagnostics: inventory.archiveDiagnostics,\n artifactDisposition: inventory.artifacts.map((artifact) => ({\n ...artifact,\n disposition: ownValue(mapping.artifacts, artifact.path) ?? artifact.disposition,\n selectedDisposition: ownValue(mapping.artifacts, artifact.path) ?? null,\n })),\n followUpBlockers,\n note: \"Staged migration docs only. Living specs and mapped active changes were materialized without changing the OpenSpec source or live loam docs. The result is intentionally not expected to validate green until the listed architecture, API, provenance, and review work is completed.\",\n };\n const writes: PlannedWrite[] = [\n planWrite(join(target, \"migration-plan.json\"), `${JSON.stringify(plan, null, 2)}\\n`),\n planWrite(join(target, \"mapping.yaml\"), stringifyYaml(normalizedMapping(inventory, mapping), { lineWidth: 0 })),\n planWrite(join(target, \"FOLLOW-UP.md\"), renderFollowUp(followUpBlockers)),\n ...docsRepoScaffold(target),\n ...planned.livingWrites,\n ...planned.activeWrites,\n ...planned.legacyWrites,\n ];\n assertDistinctPlannedPaths(writes);\n for (const write of writes) write.exclusive = true;\n const staged = await stageWrites(writes);\n if (staged.some((item) => item.before !== null)) {\n await rollbackStaged(staged);\n throw new OpenSpecCommandError(\n \"already-exists\",\n \"Staging target changed after its emptiness check; refusing to overwrite a concurrently created file.\",\n );\n }\n try {\n await swapStaged(staged);\n } catch (error) {\n const failures = await rollbackStaged(staged);\n const code = typeof error === \"object\" && error !== null && \"code\" in error ? error.code : null;\n if (code === \"EEXIST\" && failures.length === 0) {\n throw new OpenSpecCommandError(\n \"already-exists\",\n \"Staging target changed during apply; the migration was rolled back without overwriting the concurrent file.\",\n );\n }\n throw rollbackError(error, failures);\n }\n // The skeleton `loam init` lays down, after the content that fills it. A docs\n // repo is recognised by `services/` plus AGENTS.md, and until now `services/`\n // existed only if some capability happened to route there: migrate a corpus\n // whose requirements all live under `changes/` — legitimate, and the shape a\n // greenfield OpenSpec workspace has — and the target came out with no\n // `services/` at all, so `loam init --docs <target> --service <id>` answered\n // \"not a docs repo\" and every read command refused it as `services-missing`.\n // Empty directories, created after the swap so a rolled-back apply leaves the\n // target as empty as it found it.\n for (const dir of DOCS_SUBDIRS) await mkdir(join(target, dir), { recursive: true });\n return { directory: target, files: writes.map((write) => write.path), followUpBlockers };\n}\n\n/**\n * The staged target is a real docs repo, not a folder of markdown.\n *\n * FOLLOW-UP.md tells the migrator to validate, rebase and vouch against this\n * directory, and every one of those commands resolves its fleet from loam.json:\n * without these three files none of the instructions it gives can be executed,\n * and the reviewer has no way to see what is still missing. The landscape is\n * laid down EMPTY for the same reason `loam init` lays it down empty — an absent\n * landscape silences every cross-service check, and a generated one would be a\n * guess presented as the map. Each staged service then shows up as exactly one\n * `landscape.service-unmodelled` error, which is the first follow-up item.\n *\n * The bytes come from `docs.ts`, so this IS the repo `loam init` makes rather\n * than a lookalike. Only the landscape's opening comment differs, and it differs\n * through a parameter: the migrator needs to be told why their map is empty,\n * and everything after that sentence is advice that does not depend on how the\n * repo was created.\n */\nfunction docsRepoScaffold(target: string): PlannedWrite[] {\n return docsRepoFiles({\n landscapePreamble: \"// Fleet map for a staged OpenSpec migration. It arrives EMPTY because\\n\"\n + \"// OpenSpec has no topology: loam has nothing to read here and will not\\n\"\n + \"// guess \\\"who calls whom\\\" into the source of truth. Every services/<id>/\\n\"\n + \"// staged beside this file needs an element and a binding below — until\\n\"\n + \"// then each one is a `landscape.service-unmodelled` error.\",\n }).map(([rel, content]) => planWrite(join(target, rel), content));\n}\n\nfunction renderFollowUp(blockers: string[]): string {\n return `# Staged OpenSpec migration: required follow-up\\n\\nThis target contains migrated living requirements, mapped active feature deltas, preserved source artifacts, and a review plan. It is deliberately not presented as a fully valid loam fleet. Complete every item before moving it into live docs.\\n\\n${blockers.map((item) => `- [ ] ${item}`).join(\"\\n\")}\\n`;\n}\n"]}
|