@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
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Five of the six files `loam new` scaffolds, and what each one says. The
|
|
3
|
+
* sixth, the `--capability` delta, is in `./capability-template.ts`.
|
|
3
4
|
*
|
|
4
5
|
* They are here rather than inline because every one of them is a document a
|
|
5
6
|
* person edits next, and the wording IS the instruction: the delta's comments
|
|
@@ -9,20 +10,32 @@
|
|
|
9
10
|
* file stops parsing.
|
|
10
11
|
*/
|
|
11
12
|
import { stringify as stringifyYaml } from "yaml";
|
|
12
|
-
import {
|
|
13
|
-
import { ARCH_REQUIREMENT_SENTINEL, ARCH_SHALL_SENTINEL,
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
13
|
+
import { serviceElementId } from "../../core/c4/seed/template.js";
|
|
14
|
+
import { ARCH_REQUIREMENT_SENTINEL, ARCH_SHALL_SENTINEL, REQUIREMENT_SENTINEL, SCENARIO_SENTINEL, SERVICE_DESCRIPTION_SENTINEL, GIVEN_SENTINEL, SHALL_SENTINEL, THEN_SENTINEL, WHEN_SENTINEL, } from "../../core/coherence/authoring/sentinels.js";
|
|
15
|
+
/**
|
|
16
|
+
* The LikeC4 identifier the delta declares a service under: the id the fleet
|
|
17
|
+
* map already draws it as (`drawn`), since a context id the map lacks lines up
|
|
18
|
+
* with nothing at archive, else `serviceElementId` — seed's `svc_<id>`, where
|
|
19
|
+
* this used to camel-case the name into a third spelling beside the seed's and
|
|
20
|
+
* the map's. A collision inside one delta takes the seed's `_<n>` suffix.
|
|
21
|
+
*/
|
|
22
|
+
function identifier(name, taken, drawn) {
|
|
23
|
+
if (drawn !== undefined)
|
|
24
|
+
return drawn;
|
|
25
|
+
const base = serviceElementId(name);
|
|
20
26
|
let id = base;
|
|
21
27
|
for (let n = 2; taken.has(id); n += 1)
|
|
22
|
-
id = `${base}${n}`;
|
|
28
|
+
id = `${base}_${n}`;
|
|
23
29
|
taken.add(id);
|
|
24
30
|
return id;
|
|
25
31
|
}
|
|
32
|
+
/** One context line. A nested id (`a.b`) is no top-level declaration, so it names its parent instead. */
|
|
33
|
+
function contextLine(id, name) {
|
|
34
|
+
const dot = id.lastIndexOf(".");
|
|
35
|
+
if (dot < 0)
|
|
36
|
+
return ` // ${id} = softwareSystem '${name}'`;
|
|
37
|
+
return ` // ${id.slice(dot + 1)} = softwareSystem '${name}' // inside ${id.slice(0, dot)} { }, as the map draws it; edges name it ${id}`;
|
|
38
|
+
}
|
|
26
39
|
/** `FEAT-101` -> `feat_101`: LikeC4 view names take no dashes. */
|
|
27
40
|
function viewName(featureId) {
|
|
28
41
|
return featureId.toLowerCase().replace(/[^a-z0-9]+/g, "_");
|
|
@@ -44,6 +57,11 @@ export function intentTemplate(featureId, title) {
|
|
|
44
57
|
// explicit null, which is a claim ("this feature has no owner") rather than
|
|
45
58
|
// the truth ("nobody has said yet") — and every reader then has to tell an
|
|
46
59
|
// absent key from a null one. The prompt survives as a comment.
|
|
60
|
+
//
|
|
61
|
+
// `## Open questions` is where loam-feature's analyze step writes, so it is
|
|
62
|
+
// scaffolded as a heading over a comment and nothing else. `intent.empty`
|
|
63
|
+
// (coherence/authoring/scaffold.ts) reads past the whole section, so neither
|
|
64
|
+
// the untouched comment nor a page of questions counts as the Why.
|
|
47
65
|
const frontmatter = stringifyYaml({ feature: featureId, ...(title === undefined ? {} : { title }), status: "proposed" }, { lineWidth: 0 }).trimEnd();
|
|
48
66
|
return `---
|
|
49
67
|
${frontmatter}
|
|
@@ -60,11 +78,18 @@ ${frontmatter}
|
|
|
60
78
|
## Scope
|
|
61
79
|
|
|
62
80
|
<!-- Which services this touches — and, more usefully, which it deliberately does not. -->
|
|
81
|
+
|
|
82
|
+
## Open questions
|
|
83
|
+
|
|
84
|
+
<!-- Optional, never graded. What the documents leave open: an ambiguous SHALL, a value or
|
|
85
|
+
window two services state differently, an edge with no stated timeout, retry or
|
|
86
|
+
compensation. A person answers each by editing this section. -->
|
|
63
87
|
`;
|
|
64
88
|
}
|
|
65
|
-
export function deltaTemplate(featureId, touched, created) {
|
|
66
|
-
|
|
67
|
-
const
|
|
89
|
+
export function deltaTemplate(featureId, touched, created, drawn = new Map()) {
|
|
90
|
+
// The map's ids are claimed first: a new service's id never collides with a box quoted as context.
|
|
91
|
+
const taken = new Set(drawn.values());
|
|
92
|
+
const touchedIds = touched.map((s) => [identifier(s, taken, drawn.get(s)), s]);
|
|
68
93
|
const createdIds = created.map((s) => [identifier(s, taken), s]);
|
|
69
94
|
const lines = [];
|
|
70
95
|
if (touchedIds.length > 0) {
|
|
@@ -75,11 +100,12 @@ export function deltaTemplate(featureId, touched, created) {
|
|
|
75
100
|
// which teaches people that the validator is noise. Uncomment a line when an
|
|
76
101
|
// edge below actually needs the identifier.
|
|
77
102
|
lines.push(" // Services this feature touches, as context for the diagram. Uncomment the");
|
|
78
|
-
lines.push(" // ones an edge below names
|
|
79
|
-
lines.push(" // architecture/
|
|
103
|
+
lines.push(" // ones an edge below names. A service the fleet map already draws keeps the");
|
|
104
|
+
lines.push(" // identifier architecture/ gives it, so the merge lines up; one it does not");
|
|
105
|
+
lines.push(" // draw yet gets svc_<id>, the convention `loam seed` writes. They stay UNTAGGED:");
|
|
80
106
|
lines.push(` // only #${featureId} is the change, and everything else here is context.`);
|
|
81
107
|
for (const [id, name] of touchedIds)
|
|
82
|
-
lines.push(
|
|
108
|
+
lines.push(contextLine(id, name));
|
|
83
109
|
lines.push("");
|
|
84
110
|
}
|
|
85
111
|
if (createdIds.length > 0) {
|
|
@@ -162,10 +188,12 @@ export function specTemplate(featureId, service) {
|
|
|
162
188
|
A \`Publishes:\` / \`Consumes:\` body line does the same for event messages:
|
|
163
189
|
it names what the requirement puts on or takes off the bus, checked
|
|
164
190
|
against the service's AsyncAPI contract. If ${featureId} changes that
|
|
165
|
-
contract, hand-create specs/${service}/asyncapi.yaml —
|
|
166
|
-
|
|
167
|
-
(loam scaffolds none: an event
|
|
168
|
-
\`loam rebase ${featureId}\`
|
|
191
|
+
contract, hand-create specs/${service}/asyncapi.yaml — an AsyncAPI 3.0
|
|
192
|
+
document holding only the slots this feature adds, changes or removes,
|
|
193
|
+
messages under \`components.messages\` (loam scaffolds none: an event
|
|
194
|
+
contract is genuinely optional) — and run \`loam rebase ${featureId}\`
|
|
195
|
+
when it edits a slot the living contract already has, so that slot pins.
|
|
196
|
+
The same holds for an openapi.yaml beside this file: only what changes.
|
|
169
197
|
|
|
170
198
|
Copy the block below out of this comment, unindent it, and replace the
|
|
171
199
|
TODOs and every <angle-bracket> fill-in — \`loam archive\` refuses the
|
|
@@ -230,110 +258,6 @@ export function archSpecTemplate(featureId, service) {
|
|
|
230
258
|
-->
|
|
231
259
|
`;
|
|
232
260
|
}
|
|
233
|
-
/**
|
|
234
|
-
* The BUSINESS axis, scaffolded by `--capability`: a delta against the living
|
|
235
|
-
* `capabilities/<id>/spec.md`.
|
|
236
|
-
*
|
|
237
|
-
* Same idiom as the two spec templates above and for the same reason — the
|
|
238
|
-
* example lives INSIDE an HTML comment, indented past the line-anchored
|
|
239
|
-
* `## ADDED Requirements` / `### Requirement:` patterns core/document/parse.ts
|
|
240
|
-
* matches on — so the scaffold declares nothing until a person copies the block
|
|
241
|
-
* out. That is what lets a freshly scaffolded feature validate clean while
|
|
242
|
-
* still being refused by `loam archive` the moment the block is copied out and
|
|
243
|
-
* left unedited (`scaffold.placeholder`, which reads
|
|
244
|
-
* `features/<FEAT>/capabilities/` as well as `features/<FEAT>/specs/`).
|
|
245
|
-
*
|
|
246
|
-
* WHAT THE COMMENT TEACHES IS THE ALTITUDE, because that is the mistake this
|
|
247
|
-
* document invites. A capability requirement is a promise a customer could
|
|
248
|
-
* check; the four service-scoped lines (`Operations:`, `Covers:`, `Publishes:`,
|
|
249
|
-
* `Consumes:`) and the axis's own two (`Capability:`, `Realizes:`) are all
|
|
250
|
-
* ERRORS here — `capability.requirement-service-scoped` and
|
|
251
|
-
* `capability.requirement-inert-join` — and every one of them parses, so an
|
|
252
|
-
* author who is not told writes one and finds out at archive. `Requirement-ID:`
|
|
253
|
-
* is spelled as mandatory for the same reason: a capability document outlives
|
|
254
|
-
* every service that realizes it, so identity by heading is refused
|
|
255
|
-
* (`capability.requirement-unidentified`).
|
|
256
|
-
*
|
|
257
|
-
* No nested HTML comments, exactly as `specTemplate` states: an inner `-->`
|
|
258
|
-
* would end the outer comment early and re-expose the example to the parser.
|
|
259
|
-
*/
|
|
260
|
-
export function capabilityDeltaTemplate(featureId, capability) {
|
|
261
|
-
return `# ${capability} — capability delta for ${featureId}
|
|
262
|
-
|
|
263
|
-
<!-- The business axis. This is a DELTA against the living
|
|
264
|
-
capabilities/${capability}/spec.md, not the document itself: sections are
|
|
265
|
-
ADDED / MODIFIED / REMOVED, delete the ones you do not need, and a MODIFIED
|
|
266
|
-
requirement carries its full new text rather than a diff. If the fleet has
|
|
267
|
-
no capabilities/${capability}/spec.md yet, this feature's archive creates it.
|
|
268
|
-
|
|
269
|
-
WHAT BELONGS HERE is a promise somebody outside the fleet could check —
|
|
270
|
-
'a refund reaches the customer's card within five days'. What does NOT is
|
|
271
|
-
the mechanism: \`Operations:\`, \`Covers:\`, \`Publishes:\` and \`Consumes:\`
|
|
272
|
-
all resolve against ONE service's own contract or model, so a requirement
|
|
273
|
-
carrying one is a service requirement filed at the wrong altitude and
|
|
274
|
-
\`loam validate\` says so. \`Capability:\` and \`Realizes:\` are refused here
|
|
275
|
-
too — they are the joins written on the SERVICE requirement that keeps this
|
|
276
|
-
promise, one directory over in specs/<svc>/spec.md, as
|
|
277
|
-
\`Realizes: ${capability}#<Requirement-ID>\`.
|
|
278
|
-
|
|
279
|
-
\`Requirement-ID:\` is mandatory on every requirement below. A capability
|
|
280
|
-
document outlives the services that realize it, so its requirements are
|
|
281
|
-
addressed by a stable id rather than by their heading — rewording a heading
|
|
282
|
-
would otherwise be a removal and an addition, and every \`Realizes:\` line
|
|
283
|
-
pointed at it would break in silence.
|
|
284
|
-
|
|
285
|
-
A MODIFIED or REMOVED requirement also needs a \`Based-On:\` pin quoting the
|
|
286
|
-
living text it rewrites, so two features touching one capability collide
|
|
287
|
-
loudly instead of overwriting each other. Run \`loam rebase ${featureId}\`
|
|
288
|
-
and the pins are written for you.
|
|
289
|
-
|
|
290
|
-
Copy the block below out of this comment, unindent it, and replace the TODO
|
|
291
|
-
and every <angle-bracket> fill-in — \`loam archive\` refuses the scaffold's
|
|
292
|
-
own wording (\`scaffold.placeholder\`):
|
|
293
|
-
|
|
294
|
-
## ADDED Requirements
|
|
295
|
-
|
|
296
|
-
### Requirement: ${CAPABILITY_REQUIREMENT_SENTINEL}
|
|
297
|
-
Requirement-ID: ${idHint(capability)}
|
|
298
|
-
|
|
299
|
-
The fleet SHALL ${CAPABILITY_SHALL_SENTINEL}.
|
|
300
|
-
|
|
301
|
-
#### Scenario: ${SCENARIO_SENTINEL}
|
|
302
|
-
- **Given** ${GIVEN_SENTINEL}
|
|
303
|
-
- **When** ${WHEN_SENTINEL}
|
|
304
|
-
- **Then** ${THEN_SENTINEL}
|
|
305
|
-
-->
|
|
306
|
-
`;
|
|
307
|
-
}
|
|
308
|
-
/**
|
|
309
|
-
* A plausible `Requirement-ID:` for a capability, offered as a shape rather than
|
|
310
|
-
* a name: `payments/refunds` -> `PAYMENTS-REFUNDS-1`.
|
|
311
|
-
*
|
|
312
|
-
* Deliberately NOT the feature id. A service requirement's scaffolded id is
|
|
313
|
-
* `<FEAT>.<svc>.requirement` because that requirement is created by, and lives
|
|
314
|
-
* and dies with, one feature; a capability requirement outlives every feature
|
|
315
|
-
* that touches it, so an id carrying the id of the feature that happened to
|
|
316
|
-
* introduce it is exactly the identity-by-accident this axis refuses. The
|
|
317
|
-
* suffix is `-1` and not a count of anything: it is a placeholder digit inside
|
|
318
|
-
* a placeholder id, and the author is being asked to name the promise.
|
|
319
|
-
*
|
|
320
|
-
* CHECKED AGAINST THE REAL GRAMMAR, never assumed to satisfy it. A capability id
|
|
321
|
-
* may begin with a digit (`3ds`, `2fa`, `1099-filing` — `dirNameHazard` allows an
|
|
322
|
-
* alphanumeric head) while `REQUIREMENT_ID_RE` demands a LETTER, so the obvious
|
|
323
|
-
* slug hands the author `3DS-1` and `loam validate` then refuses it with
|
|
324
|
-
* `delta.requirement-id-invalid` — the author refused for using the shape the
|
|
325
|
-
* scaffold offered. Length is the same class of failure at 128 characters. The
|
|
326
|
-
* fallback prefixes rather than truncates, because a truncated id is a plausible
|
|
327
|
-
* name that means something else.
|
|
328
|
-
*/
|
|
329
|
-
function idHint(capability) {
|
|
330
|
-
const slug = capability.toUpperCase().replace(/[^A-Z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
331
|
-
const candidate = `${slug}-1`;
|
|
332
|
-
if (REQUIREMENT_ID_RE.test(candidate))
|
|
333
|
-
return candidate;
|
|
334
|
-
const prefixed = `CAP-${candidate}`;
|
|
335
|
-
return REQUIREMENT_ID_RE.test(prefixed) ? prefixed : "CAP-1";
|
|
336
|
-
}
|
|
337
261
|
export function openapiTemplate(service) {
|
|
338
262
|
return `openapi: 3.1.0
|
|
339
263
|
info:
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"templates.js","sourceRoot":"","sources":["../../../src/commands/new/templates.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The six files `loam new` scaffolds, and what each one says.\n *\n * They are here rather than inline because every one of them is a document a\n * person edits next, and the wording IS the instruction: the delta's comments\n * teach the grammar `core/document/parse.ts` matches on, and the intent's\n * frontmatter is serialized rather than interpolated because a title is free\n * text somebody types — `Checkout: split payments` reopens the mapping and the\n * file stops parsing.\n */\nimport { stringify as stringifyYaml } from \"yaml\";\nimport { REQUIREMENT_ID_RE } from \"../../core/document/spec.js\";\nimport {\n ARCH_REQUIREMENT_SENTINEL,\n ARCH_SHALL_SENTINEL,\n CAPABILITY_REQUIREMENT_SENTINEL,\n CAPABILITY_SHALL_SENTINEL,\n REQUIREMENT_SENTINEL,\n SCENARIO_SENTINEL,\n SERVICE_DESCRIPTION_SENTINEL,\n GIVEN_SENTINEL,\n SHALL_SENTINEL,\n THEN_SENTINEL,\n WHEN_SENTINEL,\n} from \"../../core/coherence/authoring/sentinels.js\";\n\nfunction identifier(name: string, taken: Set<string>): string {\n const parts = name.split(/[^A-Za-z0-9]+/).filter((p) => p.length > 0);\n const head = (parts[0] ?? \"svc\").replace(/^\\d+/, \"\") || \"svc\";\n const base =\n head[0]!.toLowerCase() +\n head.slice(1) +\n parts.slice(1).map((p) => p[0]!.toUpperCase() + p.slice(1)).join(\"\");\n let id = base;\n for (let n = 2; taken.has(id); n += 1) id = `${base}${n}`;\n taken.add(id);\n return id;\n}\n\n/** `FEAT-101` -> `feat_101`: LikeC4 view names take no dashes. */\nfunction viewName(featureId: string): string {\n return featureId.toLowerCase().replace(/[^a-z0-9]+/g, \"_\");\n}\n\n/* ------------------------------------------------------------------ */\n/* Templates */\n/* ------------------------------------------------------------------ */\n\nexport function intentTemplate(featureId: string, title: string | undefined): string {\n const heading = title ?? featureId;\n // The frontmatter is SERIALIZED, never interpolated. A title is free text a\n // person types — `Checkout: split payments` used to be pasted straight after\n // `title: `, where the colon reopens the mapping and the file stops parsing;\n // `Orders #42 rework` lost everything from the `#` as a YAML comment, so the\n // feature quietly answered to a title nobody wrote. The yaml serializer is the\n // only thing that knows every case, and it is already how migrate-openspec\n // writes the same three keys.\n //\n // `owner` is deliberately NOT a key here. Scaffolding `owner:` writes an\n // explicit null, which is a claim (\"this feature has no owner\") rather than\n // the truth (\"nobody has said yet\") — and every reader then has to tell an\n // absent key from a null one. The prompt survives as a comment.\n const frontmatter = stringifyYaml(\n { feature: featureId, ...(title === undefined ? {} : { title }), status: \"proposed\" },\n { lineWidth: 0 },\n ).trimEnd();\n return `---\n${frontmatter}\n# owner: <the team or person who answers for this>\n---\n\n# ${heading}\n\n## Why\n\n<!-- The problem in business terms. This is what a reviewer reads first, and what\n the requirement deltas below have to be justified by. -->\n\n## Scope\n\n<!-- Which services this touches — and, more usefully, which it deliberately does not. -->\n`;\n}\n\nexport function deltaTemplate(featureId: string, touched: string[], created: string[]): string {\n const taken = new Set<string>();\n const touchedIds = touched.map((s) => [identifier(s, taken), s] as const);\n const createdIds = created.map((s) => [identifier(s, taken), s] as const);\n\n const lines: string[] = [];\n if (touchedIds.length > 0) {\n // Context elements ship COMMENTED OUT, exactly like the example edge below.\n // Declared-and-untagged is the one shape `delta.nothing-tagged` exists to\n // refuse (an author who forgot the tags), so writing these live made every\n // `loam new --touches X` fail its own validator on the very first run —\n // which teaches people that the validator is noise. Uncomment a line when an\n // edge below actually needs the identifier.\n lines.push(\" // Services this feature touches, as context for the diagram. Uncomment the\");\n lines.push(\" // ones an edge below names, and reuse the identifiers from\");\n lines.push(\" // architecture/landscape.likec4 so the merge lines up. They stay UNTAGGED:\");\n lines.push(` // only #${featureId} is the change, and everything else here is context.`);\n for (const [id, name] of touchedIds) lines.push(` // ${id} = softwareSystem '${name}'`);\n lines.push(\"\");\n }\n if (createdIds.length > 0) {\n lines.push(\" // Services this feature introduces. The tag is what `loam archive` folds\");\n lines.push(\" // into the living landscape.\");\n for (const [id, name] of createdIds) {\n lines.push(` ${id} = softwareSystem '${name}' {`);\n lines.push(` #${featureId}`);\n // Interpolated from sentinels.ts, never spelled here: the placeholder\n // gate refuses this exact string at archive, and a second spelling is\n // how a template rewording silently retires the check that guards it.\n lines.push(` description '${SERVICE_DESCRIPTION_SENTINEL}'`);\n lines.push(\" }\");\n }\n lines.push(\"\");\n }\n\n // The example edge uses identifiers already declared above when there are two\n // to join; otherwise placeholders, so it never invents a service.\n const from = touchedIds[0]?.[0] ?? createdIds[0]?.[0] ?? \"consumer\";\n const to = createdIds[0]?.[0] ?? touchedIds[1]?.[0] ?? \"provider\";\n lines.push(\" // New calls. `metadata { op }` is the spine: it names the OpenAPI operationId\");\n lines.push(\" // the call uses, and `loam validate` checks it against the target's contract.\");\n lines.push(\" // Uncomment and adjust (both endpoints have to be declared above):\");\n lines.push(\" //\");\n lines.push(` // ${from} -> ${to} 'Calls createSplit' {`);\n lines.push(` // #${featureId}`);\n lines.push(\" // metadata { op 'createSplit' }\");\n lines.push(\" // }\");\n\n return `// ${featureId} — architecture delta.\n//\n// Everything tagged #${featureId} is exactly what \\`loam archive\\` folds into\n// architecture/landscape.likec4. Everything else here is context for the diagram.\n//\n// If ${featureId} changes no architecture, DELETE this file: a requirements-only\n// feature is complete without it, and an empty \\`model {}\\` is just as legal.\n\nspecification {\n element softwareSystem\n tag ${featureId}\n}\n\nmodel {\n${lines.join(\"\\n\")}\n}\n\nviews {\n view ${viewName(featureId)} {\n include *\n }\n}\n`;\n}\n\n/**\n * The business axis, scaffolded with its example INSIDE the comment — the same\n * mechanism as `archSpecTemplate` below, for the same reason: this template\n * used to ship a live `### Requirement: TODO — name the behaviour` that\n * parsed as a real requirement, validated clean, and archived a literal TODO\n * into the living spec at exit 0. The indented headings sit past the\n * line-anchored patterns core/document/parse.ts matches on, so the scaffold\n * declares nothing until a person copies the block out and writes over the\n * fill-ins — and `scaffold.placeholder` gates the archive if the fill-ins\n * survive the copy.\n *\n * No nested HTML comments: the `Operations:` guidance that used to be its own\n * comment lives in this one's prose, because an inner `-->` would end the\n * outer comment early and re-expose the example to the parser.\n */\nexport function specTemplate(featureId: string, service: string): string {\n return `# ${service} — requirement delta for ${featureId}\n\n<!-- Sections: ADDED / MODIFIED / REMOVED. Delete the ones you do not need.\n A MODIFIED requirement carries its full new text, not a diff.\n Every requirement needs at least one scenario — \\`loam validate\\` gates on it.\n\n An \\`Operations:\\` body line names the operationIds the requirement\n governs. \\`loam validate\\` checks each one against the service's OpenAPI,\n and \\`loam archive\\` refuses to merge a requirement that governs an\n operation nobody defines — add the line once the contract exists.\n\n A \\`Publishes:\\` / \\`Consumes:\\` body line does the same for event messages:\n it names what the requirement puts on or takes off the bus, checked\n against the service's AsyncAPI contract. If ${featureId} changes that\n contract, hand-create specs/${service}/asyncapi.yaml — a complete\n AsyncAPI 3.0 document, messages declared under \\`components.messages\\`\n (loam scaffolds none: an event contract is genuinely optional) — and run\n \\`loam rebase ${featureId}\\` so every restated slot pins as a quote.\n\n Copy the block below out of this comment, unindent it, and replace the\n TODOs and every <angle-bracket> fill-in — \\`loam archive\\` refuses the\n scaffold's own wording (\\`scaffold.placeholder\\`):\n\n ## ADDED Requirements\n\n ### Requirement: ${REQUIREMENT_SENTINEL}\n Requirement-ID: ${featureId}.${service}.requirement\n\n The service SHALL ${SHALL_SENTINEL}.\n\n #### Scenario: ${SCENARIO_SENTINEL}\n - **Given** ${GIVEN_SENTINEL}\n - **When** ${WHEN_SENTINEL}\n - **Then** ${THEN_SENTINEL}\n-->\n`;\n}\n\n/**\n * The architecture axis, scaffolded EMPTY on purpose.\n *\n * Same grammar as spec.md, so the template body has to be unparseable as\n * requirements or the scaffold would ship a requirement nobody wrote — the\n * headings are therefore indented inside an HTML comment, which puts them past\n * the line-anchored `## ADDED Requirements` / `### Requirement:` patterns\n * core/document/parse.ts matches on. Copy the block out of the comment and unindent it.\n */\nexport function archSpecTemplate(featureId: string, service: string): string {\n return `# ${service} — architecture requirement delta for ${featureId}\n\n<!-- The architecture axis: the obligations no business scenario was ever going\n to mention — the outbox, the retries, the timeouts, the alerts.\n\n \\`Covers:\\` is to an arch requirement what \\`Operations:\\` is to a business\n one — it names the MODEL OBJECTS the scenarios below exercise, so coverage\n is derived instead of trusted. Three forms, comma-separated:\n\n Covers: ${service} a C4 element — its id, or the service a\n bound/titled element stands for\n Covers: consumer -> ${service} an edge, each side resolved the same way\n Covers: alert:<id>, sli:<id> a signal declared in health.yaml\n\n Every tagged element and edge in delta.likec4 wants one (\\`c4.uncovered\\`).\n Delete this file if ${featureId} adds no architectural obligation.\n\n Copy the block below out of this comment and unindent it:\n\n ## ADDED Requirements\n\n ### Requirement: ${ARCH_REQUIREMENT_SENTINEL}\n Requirement-ID: ${featureId}.${service}.arch\n\n The service SHALL ${ARCH_SHALL_SENTINEL}.\n\n Covers: ${service}\n\n #### Scenario: ${SCENARIO_SENTINEL}\n - **Given** ${GIVEN_SENTINEL}\n - **When** ${WHEN_SENTINEL}\n - **Then** ${THEN_SENTINEL}\n-->\n`;\n}\n\n/**\n * The BUSINESS axis, scaffolded by `--capability`: a delta against the living\n * `capabilities/<id>/spec.md`.\n *\n * Same idiom as the two spec templates above and for the same reason — the\n * example lives INSIDE an HTML comment, indented past the line-anchored\n * `## ADDED Requirements` / `### Requirement:` patterns core/document/parse.ts\n * matches on — so the scaffold declares nothing until a person copies the block\n * out. That is what lets a freshly scaffolded feature validate clean while\n * still being refused by `loam archive` the moment the block is copied out and\n * left unedited (`scaffold.placeholder`, which reads\n * `features/<FEAT>/capabilities/` as well as `features/<FEAT>/specs/`).\n *\n * WHAT THE COMMENT TEACHES IS THE ALTITUDE, because that is the mistake this\n * document invites. A capability requirement is a promise a customer could\n * check; the four service-scoped lines (`Operations:`, `Covers:`, `Publishes:`,\n * `Consumes:`) and the axis's own two (`Capability:`, `Realizes:`) are all\n * ERRORS here — `capability.requirement-service-scoped` and\n * `capability.requirement-inert-join` — and every one of them parses, so an\n * author who is not told writes one and finds out at archive. `Requirement-ID:`\n * is spelled as mandatory for the same reason: a capability document outlives\n * every service that realizes it, so identity by heading is refused\n * (`capability.requirement-unidentified`).\n *\n * No nested HTML comments, exactly as `specTemplate` states: an inner `-->`\n * would end the outer comment early and re-expose the example to the parser.\n */\nexport function capabilityDeltaTemplate(featureId: string, capability: string): string {\n return `# ${capability} — capability delta for ${featureId}\n\n<!-- The business axis. This is a DELTA against the living\n capabilities/${capability}/spec.md, not the document itself: sections are\n ADDED / MODIFIED / REMOVED, delete the ones you do not need, and a MODIFIED\n requirement carries its full new text rather than a diff. If the fleet has\n no capabilities/${capability}/spec.md yet, this feature's archive creates it.\n\n WHAT BELONGS HERE is a promise somebody outside the fleet could check —\n 'a refund reaches the customer's card within five days'. What does NOT is\n the mechanism: \\`Operations:\\`, \\`Covers:\\`, \\`Publishes:\\` and \\`Consumes:\\`\n all resolve against ONE service's own contract or model, so a requirement\n carrying one is a service requirement filed at the wrong altitude and\n \\`loam validate\\` says so. \\`Capability:\\` and \\`Realizes:\\` are refused here\n too — they are the joins written on the SERVICE requirement that keeps this\n promise, one directory over in specs/<svc>/spec.md, as\n \\`Realizes: ${capability}#<Requirement-ID>\\`.\n\n \\`Requirement-ID:\\` is mandatory on every requirement below. A capability\n document outlives the services that realize it, so its requirements are\n addressed by a stable id rather than by their heading — rewording a heading\n would otherwise be a removal and an addition, and every \\`Realizes:\\` line\n pointed at it would break in silence.\n\n A MODIFIED or REMOVED requirement also needs a \\`Based-On:\\` pin quoting the\n living text it rewrites, so two features touching one capability collide\n loudly instead of overwriting each other. Run \\`loam rebase ${featureId}\\`\n and the pins are written for you.\n\n Copy the block below out of this comment, unindent it, and replace the TODO\n and every <angle-bracket> fill-in — \\`loam archive\\` refuses the scaffold's\n own wording (\\`scaffold.placeholder\\`):\n\n ## ADDED Requirements\n\n ### Requirement: ${CAPABILITY_REQUIREMENT_SENTINEL}\n Requirement-ID: ${idHint(capability)}\n\n The fleet SHALL ${CAPABILITY_SHALL_SENTINEL}.\n\n #### Scenario: ${SCENARIO_SENTINEL}\n - **Given** ${GIVEN_SENTINEL}\n - **When** ${WHEN_SENTINEL}\n - **Then** ${THEN_SENTINEL}\n-->\n`;\n}\n\n/**\n * A plausible `Requirement-ID:` for a capability, offered as a shape rather than\n * a name: `payments/refunds` -> `PAYMENTS-REFUNDS-1`.\n *\n * Deliberately NOT the feature id. A service requirement's scaffolded id is\n * `<FEAT>.<svc>.requirement` because that requirement is created by, and lives\n * and dies with, one feature; a capability requirement outlives every feature\n * that touches it, so an id carrying the id of the feature that happened to\n * introduce it is exactly the identity-by-accident this axis refuses. The\n * suffix is `-1` and not a count of anything: it is a placeholder digit inside\n * a placeholder id, and the author is being asked to name the promise.\n *\n * CHECKED AGAINST THE REAL GRAMMAR, never assumed to satisfy it. A capability id\n * may begin with a digit (`3ds`, `2fa`, `1099-filing` — `dirNameHazard` allows an\n * alphanumeric head) while `REQUIREMENT_ID_RE` demands a LETTER, so the obvious\n * slug hands the author `3DS-1` and `loam validate` then refuses it with\n * `delta.requirement-id-invalid` — the author refused for using the shape the\n * scaffold offered. Length is the same class of failure at 128 characters. The\n * fallback prefixes rather than truncates, because a truncated id is a plausible\n * name that means something else.\n */\nfunction idHint(capability: string): string {\n const slug = capability.toUpperCase().replace(/[^A-Z0-9]+/g, \"-\").replace(/^-+|-+$/g, \"\");\n const candidate = `${slug}-1`;\n if (REQUIREMENT_ID_RE.test(candidate)) return candidate;\n const prefixed = `CAP-${candidate}`;\n return REQUIREMENT_ID_RE.test(prefixed) ? prefixed : \"CAP-1\";\n}\n\nexport function openapiTemplate(service: string): string {\n return `openapi: 3.1.0\ninfo:\n title: ${service}\n version: \"0.1\"\n\n# The operations this feature adds. \\`operationId\\` is the token the C4 edge\n# (metadata { op }) and the requirement (Operations:) both point at — keep the\n# three spellings identical or \\`loam validate\\` will say so.\npaths: {}\n# /splits:\n# post:\n# operationId: createSplit\n# responses:\n# \"201\":\n# description: Created\n`;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"templates.js","sourceRoot":"","sources":["../../../src/commands/new/templates.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Five of the six files `loam new` scaffolds, and what each one says. The\n * sixth, the `--capability` delta, is in `./capability-template.ts`.\n *\n * They are here rather than inline because every one of them is a document a\n * person edits next, and the wording IS the instruction: the delta's comments\n * teach the grammar `core/document/parse.ts` matches on, and the intent's\n * frontmatter is serialized rather than interpolated because a title is free\n * text somebody types — `Checkout: split payments` reopens the mapping and the\n * file stops parsing.\n */\nimport { stringify as stringifyYaml } from \"yaml\";\nimport { serviceElementId } from \"../../core/c4/seed/template.js\";\nimport {\n ARCH_REQUIREMENT_SENTINEL,\n ARCH_SHALL_SENTINEL,\n REQUIREMENT_SENTINEL,\n SCENARIO_SENTINEL,\n SERVICE_DESCRIPTION_SENTINEL,\n GIVEN_SENTINEL,\n SHALL_SENTINEL,\n THEN_SENTINEL,\n WHEN_SENTINEL,\n} from \"../../core/coherence/authoring/sentinels.js\";\n\n/**\n * The LikeC4 identifier the delta declares a service under: the id the fleet\n * map already draws it as (`drawn`), since a context id the map lacks lines up\n * with nothing at archive, else `serviceElementId` — seed's `svc_<id>`, where\n * this used to camel-case the name into a third spelling beside the seed's and\n * the map's. A collision inside one delta takes the seed's `_<n>` suffix.\n */\nfunction identifier(name: string, taken: Set<string>, drawn?: string): string {\n if (drawn !== undefined) return drawn;\n const base = serviceElementId(name);\n let id = base;\n for (let n = 2; taken.has(id); n += 1) id = `${base}_${n}`;\n taken.add(id);\n return id;\n}\n\n/** One context line. A nested id (`a.b`) is no top-level declaration, so it names its parent instead. */\nfunction contextLine(id: string, name: string): string {\n const dot = id.lastIndexOf(\".\");\n if (dot < 0) return ` // ${id} = softwareSystem '${name}'`;\n return ` // ${id.slice(dot + 1)} = softwareSystem '${name}' // inside ${id.slice(0, dot)} { }, as the map draws it; edges name it ${id}`;\n}\n\n/** `FEAT-101` -> `feat_101`: LikeC4 view names take no dashes. */\nfunction viewName(featureId: string): string {\n return featureId.toLowerCase().replace(/[^a-z0-9]+/g, \"_\");\n}\n\n/* ------------------------------------------------------------------ */\n/* Templates */\n/* ------------------------------------------------------------------ */\n\nexport function intentTemplate(featureId: string, title: string | undefined): string {\n const heading = title ?? featureId;\n // The frontmatter is SERIALIZED, never interpolated. A title is free text a\n // person types — `Checkout: split payments` used to be pasted straight after\n // `title: `, where the colon reopens the mapping and the file stops parsing;\n // `Orders #42 rework` lost everything from the `#` as a YAML comment, so the\n // feature quietly answered to a title nobody wrote. The yaml serializer is the\n // only thing that knows every case, and it is already how migrate-openspec\n // writes the same three keys.\n //\n // `owner` is deliberately NOT a key here. Scaffolding `owner:` writes an\n // explicit null, which is a claim (\"this feature has no owner\") rather than\n // the truth (\"nobody has said yet\") — and every reader then has to tell an\n // absent key from a null one. The prompt survives as a comment.\n //\n // `## Open questions` is where loam-feature's analyze step writes, so it is\n // scaffolded as a heading over a comment and nothing else. `intent.empty`\n // (coherence/authoring/scaffold.ts) reads past the whole section, so neither\n // the untouched comment nor a page of questions counts as the Why.\n const frontmatter = stringifyYaml(\n { feature: featureId, ...(title === undefined ? {} : { title }), status: \"proposed\" },\n { lineWidth: 0 },\n ).trimEnd();\n return `---\n${frontmatter}\n# owner: <the team or person who answers for this>\n---\n\n# ${heading}\n\n## Why\n\n<!-- The problem in business terms. This is what a reviewer reads first, and what\n the requirement deltas below have to be justified by. -->\n\n## Scope\n\n<!-- Which services this touches — and, more usefully, which it deliberately does not. -->\n\n## Open questions\n\n<!-- Optional, never graded. What the documents leave open: an ambiguous SHALL, a value or\n window two services state differently, an edge with no stated timeout, retry or\n compensation. A person answers each by editing this section. -->\n`;\n}\n\nexport function deltaTemplate(featureId: string, touched: string[], created: string[], drawn: ReadonlyMap<string, string> = new Map()): string {\n // The map's ids are claimed first: a new service's id never collides with a box quoted as context.\n const taken = new Set<string>(drawn.values());\n const touchedIds = touched.map((s) => [identifier(s, taken, drawn.get(s)), s] as const);\n const createdIds = created.map((s) => [identifier(s, taken), s] as const);\n\n const lines: string[] = [];\n if (touchedIds.length > 0) {\n // Context elements ship COMMENTED OUT, exactly like the example edge below.\n // Declared-and-untagged is the one shape `delta.nothing-tagged` exists to\n // refuse (an author who forgot the tags), so writing these live made every\n // `loam new --touches X` fail its own validator on the very first run —\n // which teaches people that the validator is noise. Uncomment a line when an\n // edge below actually needs the identifier.\n lines.push(\" // Services this feature touches, as context for the diagram. Uncomment the\");\n lines.push(\" // ones an edge below names. A service the fleet map already draws keeps the\");\n lines.push(\" // identifier architecture/ gives it, so the merge lines up; one it does not\");\n lines.push(\" // draw yet gets svc_<id>, the convention `loam seed` writes. They stay UNTAGGED:\");\n lines.push(` // only #${featureId} is the change, and everything else here is context.`);\n for (const [id, name] of touchedIds) lines.push(contextLine(id, name));\n lines.push(\"\");\n }\n if (createdIds.length > 0) {\n lines.push(\" // Services this feature introduces. The tag is what `loam archive` folds\");\n lines.push(\" // into the living landscape.\");\n for (const [id, name] of createdIds) {\n lines.push(` ${id} = softwareSystem '${name}' {`);\n lines.push(` #${featureId}`);\n // Interpolated from sentinels.ts, never spelled here: the placeholder\n // gate refuses this exact string at archive, and a second spelling is\n // how a template rewording silently retires the check that guards it.\n lines.push(` description '${SERVICE_DESCRIPTION_SENTINEL}'`);\n lines.push(\" }\");\n }\n lines.push(\"\");\n }\n\n // The example edge uses identifiers already declared above when there are two\n // to join; otherwise placeholders, so it never invents a service.\n const from = touchedIds[0]?.[0] ?? createdIds[0]?.[0] ?? \"consumer\";\n const to = createdIds[0]?.[0] ?? touchedIds[1]?.[0] ?? \"provider\";\n lines.push(\" // New calls. `metadata { op }` is the spine: it names the OpenAPI operationId\");\n lines.push(\" // the call uses, and `loam validate` checks it against the target's contract.\");\n lines.push(\" // Uncomment and adjust (both endpoints have to be declared above):\");\n lines.push(\" //\");\n lines.push(` // ${from} -> ${to} 'Calls createSplit' {`);\n lines.push(` // #${featureId}`);\n lines.push(\" // metadata { op 'createSplit' }\");\n lines.push(\" // }\");\n\n return `// ${featureId} — architecture delta.\n//\n// Everything tagged #${featureId} is exactly what \\`loam archive\\` folds into\n// architecture/landscape.likec4. Everything else here is context for the diagram.\n//\n// If ${featureId} changes no architecture, DELETE this file: a requirements-only\n// feature is complete without it, and an empty \\`model {}\\` is just as legal.\n\nspecification {\n element softwareSystem\n tag ${featureId}\n}\n\nmodel {\n${lines.join(\"\\n\")}\n}\n\nviews {\n view ${viewName(featureId)} {\n include *\n }\n}\n`;\n}\n\n/**\n * The business axis, scaffolded with its example INSIDE the comment — the same\n * mechanism as `archSpecTemplate` below, for the same reason: this template\n * used to ship a live `### Requirement: TODO — name the behaviour` that\n * parsed as a real requirement, validated clean, and archived a literal TODO\n * into the living spec at exit 0. The indented headings sit past the\n * line-anchored patterns core/document/parse.ts matches on, so the scaffold\n * declares nothing until a person copies the block out and writes over the\n * fill-ins — and `scaffold.placeholder` gates the archive if the fill-ins\n * survive the copy.\n *\n * No nested HTML comments: the `Operations:` guidance that used to be its own\n * comment lives in this one's prose, because an inner `-->` would end the\n * outer comment early and re-expose the example to the parser.\n */\nexport function specTemplate(featureId: string, service: string): string {\n return `# ${service} — requirement delta for ${featureId}\n\n<!-- Sections: ADDED / MODIFIED / REMOVED. Delete the ones you do not need.\n A MODIFIED requirement carries its full new text, not a diff.\n Every requirement needs at least one scenario — \\`loam validate\\` gates on it.\n\n An \\`Operations:\\` body line names the operationIds the requirement\n governs. \\`loam validate\\` checks each one against the service's OpenAPI,\n and \\`loam archive\\` refuses to merge a requirement that governs an\n operation nobody defines — add the line once the contract exists.\n\n A \\`Publishes:\\` / \\`Consumes:\\` body line does the same for event messages:\n it names what the requirement puts on or takes off the bus, checked\n against the service's AsyncAPI contract. If ${featureId} changes that\n contract, hand-create specs/${service}/asyncapi.yaml — an AsyncAPI 3.0\n document holding only the slots this feature adds, changes or removes,\n messages under \\`components.messages\\` (loam scaffolds none: an event\n contract is genuinely optional) — and run \\`loam rebase ${featureId}\\`\n when it edits a slot the living contract already has, so that slot pins.\n The same holds for an openapi.yaml beside this file: only what changes.\n\n Copy the block below out of this comment, unindent it, and replace the\n TODOs and every <angle-bracket> fill-in — \\`loam archive\\` refuses the\n scaffold's own wording (\\`scaffold.placeholder\\`):\n\n ## ADDED Requirements\n\n ### Requirement: ${REQUIREMENT_SENTINEL}\n Requirement-ID: ${featureId}.${service}.requirement\n\n The service SHALL ${SHALL_SENTINEL}.\n\n #### Scenario: ${SCENARIO_SENTINEL}\n - **Given** ${GIVEN_SENTINEL}\n - **When** ${WHEN_SENTINEL}\n - **Then** ${THEN_SENTINEL}\n-->\n`;\n}\n\n/**\n * The architecture axis, scaffolded EMPTY on purpose.\n *\n * Same grammar as spec.md, so the template body has to be unparseable as\n * requirements or the scaffold would ship a requirement nobody wrote — the\n * headings are therefore indented inside an HTML comment, which puts them past\n * the line-anchored `## ADDED Requirements` / `### Requirement:` patterns\n * core/document/parse.ts matches on. Copy the block out of the comment and unindent it.\n */\nexport function archSpecTemplate(featureId: string, service: string): string {\n return `# ${service} — architecture requirement delta for ${featureId}\n\n<!-- The architecture axis: the obligations no business scenario was ever going\n to mention — the outbox, the retries, the timeouts, the alerts.\n\n \\`Covers:\\` is to an arch requirement what \\`Operations:\\` is to a business\n one — it names the MODEL OBJECTS the scenarios below exercise, so coverage\n is derived instead of trusted. Three forms, comma-separated:\n\n Covers: ${service} a C4 element — its id, or the service a\n bound/titled element stands for\n Covers: consumer -> ${service} an edge, each side resolved the same way\n Covers: alert:<id>, sli:<id> a signal declared in health.yaml\n\n Every tagged element and edge in delta.likec4 wants one (\\`c4.uncovered\\`).\n Delete this file if ${featureId} adds no architectural obligation.\n\n Copy the block below out of this comment and unindent it:\n\n ## ADDED Requirements\n\n ### Requirement: ${ARCH_REQUIREMENT_SENTINEL}\n Requirement-ID: ${featureId}.${service}.arch\n\n The service SHALL ${ARCH_SHALL_SENTINEL}.\n\n Covers: ${service}\n\n #### Scenario: ${SCENARIO_SENTINEL}\n - **Given** ${GIVEN_SENTINEL}\n - **When** ${WHEN_SENTINEL}\n - **Then** ${THEN_SENTINEL}\n-->\n`;\n}\n\nexport function openapiTemplate(service: string): string {\n return `openapi: 3.1.0\ninfo:\n title: ${service}\n version: \"0.1\"\n\n# The operations this feature adds. \\`operationId\\` is the token the C4 edge\n# (metadata { op }) and the requirement (Operations:) both point at — keep the\n# three spellings identical or \\`loam validate\\` will say so.\npaths: {}\n# /splits:\n# post:\n# operationId: createSplit\n# responses:\n# \"201\":\n# description: Created\n`;\n}\n"]}
|
package/dist/commands/open.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { statSync } from "node:fs";
|
|
2
2
|
import { writeFile } from "node:fs/promises";
|
|
3
3
|
import { dirname, resolve } from "node:path";
|
|
4
|
-
import {
|
|
5
|
-
import { emitJson, fail, reportNoConfig } from "../core/envelope/json.js";
|
|
4
|
+
import { configOrRefuse, emitJson, fail } from "../core/envelope/json.js";
|
|
6
5
|
import { DocsRepoUnavailableError } from "../core/repo/state.js";
|
|
7
6
|
import { discoverMembers } from "../core/workspace/discover.js";
|
|
8
7
|
import { renderWorkspace } from "../core/workspace/render.js";
|
|
@@ -41,12 +40,9 @@ export function registerOpen(program) {
|
|
|
41
40
|
.option("--json", "emit the machine contract instead of the human view")
|
|
42
41
|
.action(async (opts) => {
|
|
43
42
|
const json = opts.json === true;
|
|
44
|
-
const
|
|
45
|
-
if (
|
|
46
|
-
reportNoConfig(json, loaded);
|
|
43
|
+
const config = await configOrRefuse(json);
|
|
44
|
+
if (config === null)
|
|
47
45
|
return;
|
|
48
|
-
}
|
|
49
|
-
const config = loaded.config;
|
|
50
46
|
const { docsDir } = config;
|
|
51
47
|
// "docs", not "services": this command enumerates checkouts on the LOCAL
|
|
52
48
|
// filesystem, not services/ in the docs repo, and a docs repo nobody has
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"open.js","sourceRoot":"","sources":["../../src/commands/open.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Command } from \"commander\";\nimport { statSync } from \"node:fs\";\nimport { writeFile } from \"node:fs/promises\";\nimport { dirname, resolve } from \"node:path\";\nimport { loadConfig } from \"../core/envelope/config.js\";\nimport { emitJson, fail, reportNoConfig } from \"../core/envelope/json.js\";\nimport { DocsRepoUnavailableError } from \"../core/repo/state.js\";\nimport { discoverMembers, type Discovery } from \"../core/workspace/discover.js\";\nimport { renderWorkspace, type RenderedFolder } from \"../core/workspace/render.js\";\nimport { docsRepoReady, reportDocsRepoError } from \"./policy/gate.js\";\n\ninterface OpenOptions {\n json?: boolean;\n /** --root, collected: each value REPLACES the default scan roots. */\n root?: string[];\n out?: string;\n force?: boolean;\n}\n\n/**\n * A fixed default name, not `<basename(docsDir)>.code-workspace`: predictable\n * beats collision-proof for a file that is regenerable on demand, and `--out`\n * is the lever for anyone holding two fleets' workspaces in one directory.\n */\nconst DEFAULT_OUT = \"loam.code-workspace\";\n\n/**\n * Is there a directory at `path`? A probe that cannot answer says \"no\": for a\n * value the user typed, \"cannot be read\" earns the same `invalid-option`\n * refusal as \"is not there\" — never an `internal` escape. throwIfNoEntry folds\n * the absent case into the same answer with no window between probe and use;\n * the catch covers the errnos statSync can still raise (EACCES, ENOTDIR — a\n * path routed through a file).\n */\nfunction directoryAt(path: string): boolean {\n try {\n return statSync(path, { throwIfNoEntry: false })?.isDirectory() === true;\n } catch {\n return false;\n }\n}\n\n/** The write-path errnos that mean the OUT PATH is wrong, not loam. */\nconst OUT_PATH_ERRNOS = new Set([\"EISDIR\", \"EACCES\", \"EPERM\", \"ENOTDIR\", \"EINVAL\"]);\n\nexport function registerOpen(program: Command): void {\n program\n .command(\"open\")\n .description(\"Write a .code-workspace joining the docs repo and every bound service checkout\")\n .option(\n \"--root <dir>\",\n \"scan this directory for bound checkouts instead of the defaults; repeatable\",\n (value: string, prev: string[] = []) => [...prev, value],\n )\n .option(\"--out <file>\", `workspace file to write (default ${DEFAULT_OUT} in the current directory)`)\n .option(\"--force\", \"overwrite the workspace file if it already exists\")\n .option(\"--json\", \"emit the machine contract instead of the human view\")\n .action(async (opts: OpenOptions) => {\n const json = opts.json === true;\n const loaded = await loadConfig();\n if (loaded.kind !== \"loaded\") {\n reportNoConfig(json, loaded);\n return;\n }\n const config = loaded.config;\n const { docsDir } = config;\n // \"docs\", not \"services\": this command enumerates checkouts on the LOCAL\n // filesystem, not services/ in the docs repo, and a docs repo nobody has\n // run `loam adopt` against is still worth opening an editor on.\n if (!docsRepoReady(json, docsDir, \"docs\")) return;\n\n // Both typed paths are validated before any discovery runs: a --root or\n // --out typo is a fact about the INVOCATION, not the fleet, and it must\n // refuse before the scan — not after verdicts were computed over\n // directories nobody meant, and not from the write at the very end.\n const roots = (opts.root ?? []).map((dir) => resolve(dir));\n for (const dir of roots) {\n if (!directoryAt(dir)) {\n fail(json, \"invalid-option\", `--root does not name a readable directory: ${dir}`);\n return;\n }\n }\n const out = resolve(opts.out ?? DEFAULT_OUT);\n const parent = dirname(out);\n if (!directoryAt(parent)) {\n fail(json, \"invalid-option\", `--out points into a directory that does not exist: ${parent}`);\n return;\n }\n\n let discovery: Discovery;\n try {\n discovery = await discoverMembers({\n docsDir,\n // parseConfig always derives `root`; the fallback only documents\n // which absence the optional type would otherwise leave a guess.\n configRoot: config.root ?? process.cwd(),\n ...(config.service === undefined ? {} : { service: config.service }),\n ...(roots.length === 0 ? {} : { roots }),\n });\n } catch (err) {\n // The gate above passed, so this is the repo vanishing mid-run —\n // the same breach the enumeration reports, spelled the same way.\n if (!(err instanceof DocsRepoUnavailableError)) throw err;\n reportDocsRepoError(json, err);\n return;\n }\n\n // An explicit --root that cannot be LISTED refuses: the caller named the\n // directory, so \"I could not look\" must never come back as an empty\n // scan. The default roots keep degrading to empty — a CI sandbox that\n // denies the docs repo's parent is survivable, and both views still\n // report the degrade below.\n if (roots.length > 0 && discovery.unreadableRoots.length > 0) {\n const listing = discovery.unreadableRoots\n .map((entry) => `${entry.root} (${entry.problem})`)\n .join(\"; \");\n fail(json, \"invalid-option\", `--root cannot be scanned: ${listing}`);\n return;\n }\n\n if (discovery.duplicates.length > 0) {\n const claims = discovery.duplicates.map(\n (d) => `'${d.service}' is declared by ${d.paths.join(\" and \")}`,\n );\n fail(\n json,\n \"binding-duplicate\",\n `Two checkouts declare the same service for this docs repo: ${claims.join(\"; \")}. ` +\n \"loam will not guess which checkout speaks for the service — narrow the scan \" +\n \"with --root, or fix the stray loam.json, then re-run.\",\n );\n return;\n }\n // Only reachable from the docs repo itself — a service-repo invocation\n // always contributes its own checkout as a member. Skipped candidates\n // ride the message: a checkout whose binding would not parse is the\n // likeliest reason the scan came up empty, and the real fix is that\n // file, not --root.\n if (!discovery.members.some((member) => member.via !== \"docs-repo\")) {\n const unread = discovery.skipped.length === 0\n ? \"\"\n : ` ${discovery.skipped.length} candidate binding(s) could not be read and did not` +\n ` count: ${discovery.skipped.map((s) => `${s.path} (${s.problem})`).join(\"; \")}.`;\n fail(\n json,\n \"no-members\",\n `No service checkout bound to ${docsDir} was found under ` +\n `${discovery.scannedRoots.join(\", \")} — the workspace would hold only the docs ` +\n \"repo. Clone a bound service repo beside it, run `loam open` from a service \" +\n `repo, or pass --root naming the directory the checkouts live in.${unread}`,\n );\n return;\n }\n\n const rendered = renderWorkspace(discovery.members, out);\n try {\n // \"wx\" makes probe-and-write one operation, so a concurrent writer\n // cannot land between an existsSync and the write. --force is honest\n // here where init's generated files needed skip-forever: a workspace\n // file is wholly derived, so overwriting it loses nothing authored.\n await writeFile(out, rendered.text, {\n encoding: \"utf8\",\n flag: opts.force === true ? \"w\" : \"wx\",\n });\n } catch (err) {\n const e = err as NodeJS.ErrnoException;\n if (e.code === \"EEXIST\") {\n fail(\n json,\n \"already-exists\",\n `${out} already exists. Pass --out naming another file, or --force to overwrite it.`,\n );\n return;\n }\n // The path-shaped failures: --out names a directory (EISDIR — the\n // exact target the EEXIST remedy above just told the caller to\n // --force), or a place this process may not write. Each is a fact\n // about the out path, so it earns invalid-option, not internal.\n if (typeof e.code === \"string\" && OUT_PATH_ERRNOS.has(e.code)) {\n fail(\n json,\n \"invalid-option\",\n `The workspace file cannot be written at ${out}: ${e.message}. ` +\n \"Pass --out naming a writable file path.\",\n );\n return;\n }\n throw err;\n }\n\n if (json) {\n emitJson({\n command: \"open\",\n docsDir,\n out,\n written: true,\n members: rendered.folders.map((folder) => ({\n path: folder.member.path,\n // The exact spelling written into the workspace file, so a\n // consumer never re-derives (and mis-derives) the relative form.\n folder: folder.path,\n name: folder.name,\n service: folder.member.service,\n via: folder.member.via,\n ...(folder.member.via === \"scan\" ? { root: folder.member.root } : {}),\n })),\n scannedRoots: discovery.scannedRoots,\n unreadableRoots: discovery.unreadableRoots,\n skipped: discovery.skipped,\n });\n return;\n }\n printWorkspace(out, discovery, rendered.folders);\n });\n}\n\nfunction viaText(folder: RenderedFolder): string {\n const member = folder.member;\n if (member.via === \"docs-repo\") return \"docs repo\";\n if (member.via === \"current-repo\") return \"this repo\";\n return `found under ${member.root}`;\n}\n\nfunction printWorkspace(out: string, discovery: Discovery, folders: RenderedFolder[]): void {\n console.log(`workspace: ${out} (${folders.length} folders)`);\n for (const folder of folders) {\n console.log(` ${folder.name} ${folder.path} (${viaText(folder)})`);\n }\n // Reported beside the result, not folded into it: an unreadable binding or\n // an unlistable default root proves nothing, but silence about either would\n // read as \"not bound\" / \"nothing there\".\n for (const entry of discovery.unreadableRoots) {\n console.log(` ! could not scan ${entry.root}: ${entry.problem}`);\n }\n for (const entry of discovery.skipped) {\n console.log(` ! skipped ${entry.path}: ${entry.problem}`);\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"open.js","sourceRoot":"","sources":["../../src/commands/open.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Command } from \"commander\";\nimport { statSync } from \"node:fs\";\nimport { writeFile } from \"node:fs/promises\";\nimport { dirname, resolve } from \"node:path\";\nimport { configOrRefuse, emitJson, fail } from \"../core/envelope/json.js\";\nimport { DocsRepoUnavailableError } from \"../core/repo/state.js\";\nimport { discoverMembers, type Discovery } from \"../core/workspace/discover.js\";\nimport { renderWorkspace, type RenderedFolder } from \"../core/workspace/render.js\";\nimport { docsRepoReady, reportDocsRepoError } from \"./policy/gate.js\";\n\ninterface OpenOptions {\n json?: boolean;\n /** --root, collected: each value REPLACES the default scan roots. */\n root?: string[];\n out?: string;\n force?: boolean;\n}\n\n/**\n * A fixed default name, not `<basename(docsDir)>.code-workspace`: predictable\n * beats collision-proof for a file that is regenerable on demand, and `--out`\n * is the lever for anyone holding two fleets' workspaces in one directory.\n */\nconst DEFAULT_OUT = \"loam.code-workspace\";\n\n/**\n * Is there a directory at `path`? A probe that cannot answer says \"no\": for a\n * value the user typed, \"cannot be read\" earns the same `invalid-option`\n * refusal as \"is not there\" — never an `internal` escape. throwIfNoEntry folds\n * the absent case into the same answer with no window between probe and use;\n * the catch covers the errnos statSync can still raise (EACCES, ENOTDIR — a\n * path routed through a file).\n */\nfunction directoryAt(path: string): boolean {\n try {\n return statSync(path, { throwIfNoEntry: false })?.isDirectory() === true;\n } catch {\n return false;\n }\n}\n\n/** The write-path errnos that mean the OUT PATH is wrong, not loam. */\nconst OUT_PATH_ERRNOS = new Set([\"EISDIR\", \"EACCES\", \"EPERM\", \"ENOTDIR\", \"EINVAL\"]);\n\nexport function registerOpen(program: Command): void {\n program\n .command(\"open\")\n .description(\"Write a .code-workspace joining the docs repo and every bound service checkout\")\n .option(\n \"--root <dir>\",\n \"scan this directory for bound checkouts instead of the defaults; repeatable\",\n (value: string, prev: string[] = []) => [...prev, value],\n )\n .option(\"--out <file>\", `workspace file to write (default ${DEFAULT_OUT} in the current directory)`)\n .option(\"--force\", \"overwrite the workspace file if it already exists\")\n .option(\"--json\", \"emit the machine contract instead of the human view\")\n .action(async (opts: OpenOptions) => {\n const json = opts.json === true;\n const config = await configOrRefuse(json);\n if (config === null) return;\n const { docsDir } = config;\n // \"docs\", not \"services\": this command enumerates checkouts on the LOCAL\n // filesystem, not services/ in the docs repo, and a docs repo nobody has\n // run `loam adopt` against is still worth opening an editor on.\n if (!docsRepoReady(json, docsDir, \"docs\")) return;\n\n // Both typed paths are validated before any discovery runs: a --root or\n // --out typo is a fact about the INVOCATION, not the fleet, and it must\n // refuse before the scan — not after verdicts were computed over\n // directories nobody meant, and not from the write at the very end.\n const roots = (opts.root ?? []).map((dir) => resolve(dir));\n for (const dir of roots) {\n if (!directoryAt(dir)) {\n fail(json, \"invalid-option\", `--root does not name a readable directory: ${dir}`);\n return;\n }\n }\n const out = resolve(opts.out ?? DEFAULT_OUT);\n const parent = dirname(out);\n if (!directoryAt(parent)) {\n fail(json, \"invalid-option\", `--out points into a directory that does not exist: ${parent}`);\n return;\n }\n\n let discovery: Discovery;\n try {\n discovery = await discoverMembers({\n docsDir,\n // parseConfig always derives `root`; the fallback only documents\n // which absence the optional type would otherwise leave a guess.\n configRoot: config.root ?? process.cwd(),\n ...(config.service === undefined ? {} : { service: config.service }),\n ...(roots.length === 0 ? {} : { roots }),\n });\n } catch (err) {\n // The gate above passed, so this is the repo vanishing mid-run —\n // the same breach the enumeration reports, spelled the same way.\n if (!(err instanceof DocsRepoUnavailableError)) throw err;\n reportDocsRepoError(json, err);\n return;\n }\n\n // An explicit --root that cannot be LISTED refuses: the caller named the\n // directory, so \"I could not look\" must never come back as an empty\n // scan. The default roots keep degrading to empty — a CI sandbox that\n // denies the docs repo's parent is survivable, and both views still\n // report the degrade below.\n if (roots.length > 0 && discovery.unreadableRoots.length > 0) {\n const listing = discovery.unreadableRoots\n .map((entry) => `${entry.root} (${entry.problem})`)\n .join(\"; \");\n fail(json, \"invalid-option\", `--root cannot be scanned: ${listing}`);\n return;\n }\n\n if (discovery.duplicates.length > 0) {\n const claims = discovery.duplicates.map(\n (d) => `'${d.service}' is declared by ${d.paths.join(\" and \")}`,\n );\n fail(\n json,\n \"binding-duplicate\",\n `Two checkouts declare the same service for this docs repo: ${claims.join(\"; \")}. ` +\n \"loam will not guess which checkout speaks for the service — narrow the scan \" +\n \"with --root, or fix the stray loam.json, then re-run.\",\n );\n return;\n }\n // Only reachable from the docs repo itself — a service-repo invocation\n // always contributes its own checkout as a member. Skipped candidates\n // ride the message: a checkout whose binding would not parse is the\n // likeliest reason the scan came up empty, and the real fix is that\n // file, not --root.\n if (!discovery.members.some((member) => member.via !== \"docs-repo\")) {\n const unread = discovery.skipped.length === 0\n ? \"\"\n : ` ${discovery.skipped.length} candidate binding(s) could not be read and did not` +\n ` count: ${discovery.skipped.map((s) => `${s.path} (${s.problem})`).join(\"; \")}.`;\n fail(\n json,\n \"no-members\",\n `No service checkout bound to ${docsDir} was found under ` +\n `${discovery.scannedRoots.join(\", \")} — the workspace would hold only the docs ` +\n \"repo. Clone a bound service repo beside it, run `loam open` from a service \" +\n `repo, or pass --root naming the directory the checkouts live in.${unread}`,\n );\n return;\n }\n\n const rendered = renderWorkspace(discovery.members, out);\n try {\n // \"wx\" makes probe-and-write one operation, so a concurrent writer\n // cannot land between an existsSync and the write. --force is honest\n // here where init's generated files needed skip-forever: a workspace\n // file is wholly derived, so overwriting it loses nothing authored.\n await writeFile(out, rendered.text, {\n encoding: \"utf8\",\n flag: opts.force === true ? \"w\" : \"wx\",\n });\n } catch (err) {\n const e = err as NodeJS.ErrnoException;\n if (e.code === \"EEXIST\") {\n fail(\n json,\n \"already-exists\",\n `${out} already exists. Pass --out naming another file, or --force to overwrite it.`,\n );\n return;\n }\n // The path-shaped failures: --out names a directory (EISDIR — the\n // exact target the EEXIST remedy above just told the caller to\n // --force), or a place this process may not write. Each is a fact\n // about the out path, so it earns invalid-option, not internal.\n if (typeof e.code === \"string\" && OUT_PATH_ERRNOS.has(e.code)) {\n fail(\n json,\n \"invalid-option\",\n `The workspace file cannot be written at ${out}: ${e.message}. ` +\n \"Pass --out naming a writable file path.\",\n );\n return;\n }\n throw err;\n }\n\n if (json) {\n emitJson({\n command: \"open\",\n docsDir,\n out,\n written: true,\n members: rendered.folders.map((folder) => ({\n path: folder.member.path,\n // The exact spelling written into the workspace file, so a\n // consumer never re-derives (and mis-derives) the relative form.\n folder: folder.path,\n name: folder.name,\n service: folder.member.service,\n via: folder.member.via,\n ...(folder.member.via === \"scan\" ? { root: folder.member.root } : {}),\n })),\n scannedRoots: discovery.scannedRoots,\n unreadableRoots: discovery.unreadableRoots,\n skipped: discovery.skipped,\n });\n return;\n }\n printWorkspace(out, discovery, rendered.folders);\n });\n}\n\nfunction viaText(folder: RenderedFolder): string {\n const member = folder.member;\n if (member.via === \"docs-repo\") return \"docs repo\";\n if (member.via === \"current-repo\") return \"this repo\";\n return `found under ${member.root}`;\n}\n\nfunction printWorkspace(out: string, discovery: Discovery, folders: RenderedFolder[]): void {\n console.log(`workspace: ${out} (${folders.length} folders)`);\n for (const folder of folders) {\n console.log(` ${folder.name} ${folder.path} (${viaText(folder)})`);\n }\n // Reported beside the result, not folded into it: an unreadable binding or\n // an unlistable default root proves nothing, but silence about either would\n // read as \"not bound\" / \"nothing there\".\n for (const entry of discovery.unreadableRoots) {\n console.log(` ! could not scan ${entry.root}: ${entry.problem}`);\n }\n for (const entry of discovery.skipped) {\n console.log(` ! skipped ${entry.path}: ${entry.problem}`);\n }\n}\n"]}
|
|
@@ -13,8 +13,13 @@
|
|
|
13
13
|
* writes to stdout or stderr and sets the exit code. That is a decision about
|
|
14
14
|
* how this process ends, and core modules compute answers rather than end
|
|
15
15
|
* processes.
|
|
16
|
+
*
|
|
17
|
+
* A fourth sits at the bottom for the commands that read the docs repo AT a
|
|
18
|
+
* git ref — `baseOrRefuse`, the same doctrine one step on: a ref git cannot
|
|
19
|
+
* answer for is "could not look", never an empty base.
|
|
16
20
|
*/
|
|
17
21
|
import { fail } from "../../core/envelope/json.js";
|
|
22
|
+
import { resolveBase } from "../../core/diff/base-git.js";
|
|
18
23
|
import { docsRepoState } from "../../core/repo/state.js";
|
|
19
24
|
/**
|
|
20
25
|
* The refusal every READ command owes a `docsDir` that is not a docs repo.
|
|
@@ -101,4 +106,26 @@ export function reportRepositoryUnavailable(json, err, consequence, docsDir) {
|
|
|
101
106
|
`A file under ${docsDir} could not be read, so ${consequence}. ${why}. ` +
|
|
102
107
|
`The failure named no path — that is how Node reports a directory sitting where a file belongs.`);
|
|
103
108
|
}
|
|
109
|
+
/**
|
|
110
|
+
* The resolved base, or null after refusing: `repository-unavailable` when
|
|
111
|
+
* the docs repo is not somewhere git can answer from, `unknown-target` when
|
|
112
|
+
* the ref names no commit there (an option-shaped ref included —
|
|
113
|
+
* `resolveBase` refuses it before it can reach an argv).
|
|
114
|
+
*/
|
|
115
|
+
export async function baseOrRefuse(json, docsDir, req) {
|
|
116
|
+
const base = await resolveBase(docsDir, req.ref);
|
|
117
|
+
if (base.kind === "no-git") {
|
|
118
|
+
fail(json, "repository-unavailable", `The docs repo at ${docsDir} is not somewhere git can answer from (${base.detail}) — ` +
|
|
119
|
+
`${req.reads} with read-only git questions (rev-parse/ls-tree/show), so it needs the docs repo ` +
|
|
120
|
+
`to be a git checkout with the ${req.flag} ref present. Clone the docs repo with its history, then re-run.`);
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
if (base.kind === "no-ref") {
|
|
124
|
+
fail(json, "unknown-target", `${req.flag} ${req.ref} does not resolve to a commit in the docs repo at ${docsDir}` +
|
|
125
|
+
`${base.detail === "" ? "" : ` (${base.detail})`}. ` +
|
|
126
|
+
`Pass a ref that exists there — main, origin/main, a tag or a commit sha — fetching first if it lives on the remote.`);
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
return { docsDir, commit: base.commit, prefix: base.prefix };
|
|
130
|
+
}
|
|
104
131
|
//# sourceMappingURL=gate.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"gate.js","sourceRoot":"","sources":["../../../src/commands/policy/gate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,OAAO,EAAE,IAAI,EAAE,MAAM,6BAA6B,CAAC;AACnD,OAAO,EAAE,aAAa,EAAiC,MAAM,0BAA0B,CAAC;AAExF;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,MAAM,UAAU,aAAa,CAAC,IAAa,EAAE,OAAe,EAAE,IAAyB;IACrF,MAAM,KAAK,GAAG,aAAa,CAAC,OAAO,CAAC,CAAC;IACrC,IAAI,KAAK,CAAC,IAAI,KAAK,IAAI,IAAI,CAAC,KAAK,CAAC,IAAI,KAAK,aAAa,IAAI,IAAI,KAAK,MAAM,CAAC;QAAE,OAAO,IAAI,CAAC;IAC1F,IAAI,KAAK,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;QAC7B,IAAI,CACF,IAAI,EACJ,cAAc,EACd,4CAA4C,KAAK,CAAC,IAAI,IAAI;YACxD,yDAAyD;YACzD,6DAA6D,CAChE,CAAC;QACF,OAAO,KAAK,CAAC;IACf,CAAC;IACD,IAAI,CACF,IAAI,EACJ,kBAAkB,EAClB,GAAG,KAAK,CAAC,IAAI,uDAAuD;QAClE,wDAAwD;QACxD,uDAAuD,CAC1D,CAAC;IACF,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,mBAAmB,CAAC,IAAa,EAAE,GAA6B;IAC9E,IAAI,CAAC,IAAI,EAAE,GAAG,CAAC,KAAK,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,cAAc,CAAC,CAAC,CAAC,kBAAkB,EAAE,GAAG,CAAC,OAAO,CAAC,CAAC;AAC9F,CAAC;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,UAAU,2BAA2B,CACzC,IAAa,EACb,GAAY,EACZ,WAAmB,EACnB,OAAe;IAEf,MAAM,CAAC,GAAG,GAA4B,CAAC;IACvC,IAAI,CAAC,CAAC,IAAI,KAAK,SAAS,IAAI,OAAO,CAAC,CAAC,KAAK,KAAK,QAAQ;QAAE,MAAM,GAAG,CAAC;IACnE,MAAM,GAAG,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;IAC7D,IAAI,CACF,IAAI,EACJ,wBAAwB,EACxB,CAAC,CAAC,IAAI,KAAK,SAAS;QAClB,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,0BAA0B,WAAW,KAAK,GAAG,EAAE;QAC1D,CAAC,CAAC,uEAAuE;YACvE,wEAAwE;YACxE,wEAAwE;YACxE,wCAAwC;YACxC,gBAAgB,OAAO,0BAA0B,WAAW,KAAK,GAAG,IAAI;gBACxE,gGAAgG,CACrG,CAAC;AACJ,CAAC","sourcesContent":["/**\n * The three refusals every command owes a docs repo it cannot read.\n *\n * One doctrine, three ways of reaching it: the gate a command passes before it\n * enumerates anything, the error the enumeration throws when the repo goes away\n * underneath it, and the errno that comes back when the repo is real but one\n * file under it will not open. Every command that reads a docs repo reaches\n * for one or another of them (`rg docsRepoReady` is the always-current list),\n * and the whole point of collecting them is that they all answer with the same\n * sentences.\n *\n * They live in `commands/` rather than in `core/`: each one calls `fail`, which\n * writes to stdout or stderr and sets the exit code. That is a decision about\n * how this process ends, and core modules compute answers rather than end\n * processes.\n */\nimport { fail } from \"../../core/envelope/json.js\";\nimport { docsRepoState, type DocsRepoUnavailableError } from \"../../core/repo/state.js\";\n\n/**\n * The refusal every READ command owes a `docsDir` that is not a docs repo.\n *\n * It is not a formality. `validate --all` and `list` used to answer a docsDir\n * that does not exist with a green report over zero services, which is the one\n * output a fleet gate must never produce: \"nothing is wrong\" and \"I could not\n * look\" are opposite facts, and in CI the first one merges. The two codes are\n * kept apart because the fixes point in different directions — a wrong path in\n * loam.json versus a docs repo that was never cloned or scaffolded — exactly\n * the way `no-config` and `config-invalid` are kept apart.\n *\n * \"Zero services\" stays reachable, and only from a REAL docs repo whose\n * `services/` is empty: that is a legitimate state (before the first\n * `loam adopt`) and it must keep exiting 0.\n *\n * `need` says how much of a docs repo this particular run has to have. A run\n * that only reads features genuinely does not need `services/` — repo.ts takes\n * the same position for `listFeatures` — and refusing there would turn one\n * diagnosis into two contradictory ones. A run that enumerates services needs\n * the directory that IS the list of services.\n *\n * It used to live in validate.ts, on the reasoning that validate is the fleet\n * gate this doctrine is about. Enough commands import it now that the doctrine\n * outgrew its host: `loam show` was structurally depending on the whole\n * validate implementation for twenty lines of policy. The rule is the module\n * now, and validate is one of its callers like the rest.\n */\nexport function docsRepoReady(json: boolean, docsDir: string, need: \"docs\" | \"services\"): boolean {\n const state = docsRepoState(docsDir);\n if (state.kind === \"ok\" || (state.kind === \"no-services\" && need === \"docs\")) return true;\n if (state.kind === \"missing\") {\n fail(\n json,\n \"docs-missing\",\n `The configured docs repo does not exist: ${state.path}. ` +\n \"Fix `docsDir` in loam.json, clone the docs repo there, \" +\n \"or run `loam init --docs <dir> --create` to make a new one.\",\n );\n return false;\n }\n fail(\n json,\n \"services-missing\",\n `${state.path} is not a docs repo — it has no services/ directory. ` +\n \"Point `docsDir` in loam.json at the shared docs repo, \" +\n \"or run `loam init --docs <dir> --create` to make one.\",\n );\n return false;\n}\n\n/**\n * The same refusal, reached the other way: `listServices`/`listFeatures` throw\n * `DocsRepoUnavailableError` when the repo goes away mid-run (or when a caller\n * reaches enumeration without passing the gate above). Mapping it back onto the\n * same two codes keeps one breach spelled one way, instead of surfacing as the\n * `internal` catch-all in cli.ts.\n */\nexport function reportDocsRepoError(json: boolean, err: DocsRepoUnavailableError): void {\n fail(json, err.state.kind === \"missing\" ? \"docs-missing\" : \"services-missing\", err.message);\n}\n\n/**\n * The third refusal a reading command owes: the docs repo IS a docs repo, but\n * one file under it cannot be read. `list`, `show`, `status` and `validate` all\n * walk a directory tree to answer, so all four owe the same sentence — and all\n * four kept their own copy of it, which is exactly how three of them ended up\n * wrong. Sharing it here, beside `reportDocsRepoError`, is the same trade: one\n * breach spelled one way instead of four that drift.\n *\n * A filesystem failure is recognised by its errno, NOT by carrying a `path`.\n * Node reports EISDIR from `read()` with no path at all — the directory opened,\n * the read failed — so a guard that looked only for `path` was defeated by the\n * commonest way a malformed docs repo breaks: one directory sitting where a\n * file belongs escaped as a bare `internal: EISDIR`, naming neither the file\n * nor the target. Anything carrying no errno at all is a real bug, and still\n * escapes untouched.\n *\n * `consequence` is the clause after \"so\" — what the caller could not answer\n * because of this. It is the only part that differs between the four.\n */\nexport function reportRepositoryUnavailable(\n json: boolean,\n err: unknown,\n consequence: string,\n docsDir: string,\n): void {\n const e = err as NodeJS.ErrnoException;\n if (e.path === undefined && typeof e.errno !== \"number\") throw err;\n const why = err instanceof Error ? err.message : String(err);\n fail(\n json,\n \"repository-unavailable\",\n e.path !== undefined\n ? `${e.path} could not be read, so ${consequence}. ${why}`\n : // Naming the target's own directory here would be a guess, and a guess\n // that is wrong precisely when it matters: one malformed artifact makes\n // every OTHER target's answer fail too, and each would have accused its\n // own files. Say what is known instead.\n `A file under ${docsDir} could not be read, so ${consequence}. ${why}. ` +\n `The failure named no path — that is how Node reports a directory sitting where a file belongs.`,\n );\n}\n"]}
|
|
1
|
+
{"version":3,"file":"gate.js","sourceRoot":"","sources":["../../../src/commands/policy/gate.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The three refusals every command owes a docs repo it cannot read.\n *\n * One doctrine, three ways of reaching it: the gate a command passes before it\n * enumerates anything, the error the enumeration throws when the repo goes away\n * underneath it, and the errno that comes back when the repo is real but one\n * file under it will not open. Every command that reads a docs repo reaches\n * for one or another of them (`rg docsRepoReady` is the always-current list),\n * and the whole point of collecting them is that they all answer with the same\n * sentences.\n *\n * They live in `commands/` rather than in `core/`: each one calls `fail`, which\n * writes to stdout or stderr and sets the exit code. That is a decision about\n * how this process ends, and core modules compute answers rather than end\n * processes.\n *\n * A fourth sits at the bottom for the commands that read the docs repo AT a\n * git ref — `baseOrRefuse`, the same doctrine one step on: a ref git cannot\n * answer for is \"could not look\", never an empty base.\n */\nimport { fail } from \"../../core/envelope/json.js\";\nimport { resolveBase, type ResolvedBase } from \"../../core/diff/base-git.js\";\nimport type { DocsDir } from \"../../core/kernel/ids/dirs.js\";\nimport { docsRepoState, type DocsRepoUnavailableError } from \"../../core/repo/state.js\";\n\n/**\n * The refusal every READ command owes a `docsDir` that is not a docs repo.\n *\n * It is not a formality. `validate --all` and `list` used to answer a docsDir\n * that does not exist with a green report over zero services, which is the one\n * output a fleet gate must never produce: \"nothing is wrong\" and \"I could not\n * look\" are opposite facts, and in CI the first one merges. The two codes are\n * kept apart because the fixes point in different directions — a wrong path in\n * loam.json versus a docs repo that was never cloned or scaffolded — exactly\n * the way `no-config` and `config-invalid` are kept apart.\n *\n * \"Zero services\" stays reachable, and only from a REAL docs repo whose\n * `services/` is empty: that is a legitimate state (before the first\n * `loam adopt`) and it must keep exiting 0.\n *\n * `need` says how much of a docs repo this particular run has to have. A run\n * that only reads features genuinely does not need `services/` — repo.ts takes\n * the same position for `listFeatures` — and refusing there would turn one\n * diagnosis into two contradictory ones. A run that enumerates services needs\n * the directory that IS the list of services.\n *\n * It used to live in validate.ts, on the reasoning that validate is the fleet\n * gate this doctrine is about. Enough commands import it now that the doctrine\n * outgrew its host: `loam show` was structurally depending on the whole\n * validate implementation for twenty lines of policy. The rule is the module\n * now, and validate is one of its callers like the rest.\n */\nexport function docsRepoReady(json: boolean, docsDir: string, need: \"docs\" | \"services\"): boolean {\n const state = docsRepoState(docsDir);\n if (state.kind === \"ok\" || (state.kind === \"no-services\" && need === \"docs\")) return true;\n if (state.kind === \"missing\") {\n fail(\n json,\n \"docs-missing\",\n `The configured docs repo does not exist: ${state.path}. ` +\n \"Fix `docsDir` in loam.json, clone the docs repo there, \" +\n \"or run `loam init --docs <dir> --create` to make a new one.\",\n );\n return false;\n }\n fail(\n json,\n \"services-missing\",\n `${state.path} is not a docs repo — it has no services/ directory. ` +\n \"Point `docsDir` in loam.json at the shared docs repo, \" +\n \"or run `loam init --docs <dir> --create` to make one.\",\n );\n return false;\n}\n\n/**\n * The same refusal, reached the other way: `listServices`/`listFeatures` throw\n * `DocsRepoUnavailableError` when the repo goes away mid-run (or when a caller\n * reaches enumeration without passing the gate above). Mapping it back onto the\n * same two codes keeps one breach spelled one way, instead of surfacing as the\n * `internal` catch-all in cli.ts.\n */\nexport function reportDocsRepoError(json: boolean, err: DocsRepoUnavailableError): void {\n fail(json, err.state.kind === \"missing\" ? \"docs-missing\" : \"services-missing\", err.message);\n}\n\n/**\n * The third refusal a reading command owes: the docs repo IS a docs repo, but\n * one file under it cannot be read. `list`, `show`, `status` and `validate` all\n * walk a directory tree to answer, so all four owe the same sentence — and all\n * four kept their own copy of it, which is exactly how three of them ended up\n * wrong. Sharing it here, beside `reportDocsRepoError`, is the same trade: one\n * breach spelled one way instead of four that drift.\n *\n * A filesystem failure is recognised by its errno, NOT by carrying a `path`.\n * Node reports EISDIR from `read()` with no path at all — the directory opened,\n * the read failed — so a guard that looked only for `path` was defeated by the\n * commonest way a malformed docs repo breaks: one directory sitting where a\n * file belongs escaped as a bare `internal: EISDIR`, naming neither the file\n * nor the target. Anything carrying no errno at all is a real bug, and still\n * escapes untouched.\n *\n * `consequence` is the clause after \"so\" — what the caller could not answer\n * because of this. It is the only part that differs between the four.\n */\nexport function reportRepositoryUnavailable(\n json: boolean,\n err: unknown,\n consequence: string,\n docsDir: string,\n): void {\n const e = err as NodeJS.ErrnoException;\n if (e.path === undefined && typeof e.errno !== \"number\") throw err;\n const why = err instanceof Error ? err.message : String(err);\n fail(\n json,\n \"repository-unavailable\",\n e.path !== undefined\n ? `${e.path} could not be read, so ${consequence}. ${why}`\n : // Naming the target's own directory here would be a guess, and a guess\n // that is wrong precisely when it matters: one malformed artifact makes\n // every OTHER target's answer fail too, and each would have accused its\n // own files. Say what is known instead.\n `A file under ${docsDir} could not be read, so ${consequence}. ${why}. ` +\n `The failure named no path — that is how Node reports a directory sitting where a file belongs.`,\n );\n}\n\n/**\n * The refusals a command owes a docs-repo git ref it cannot read at.\n *\n * Two commands read the docs repo AT a ref: `loam diff --base <ref>` (the\n * whole fleet, to diff against) and `loam gate --since <ref>` (which features\n * had already shipped, so the ones archived after it are graded too). Both\n * REFUSE when git cannot answer, unlike provenance's \"git will not say\": a\n * missing base is not an empty one, and reading it as empty is a fail-open in\n * both — diff would report the whole fleet as added, gate would pass a deploy\n * over every feature archived since a ref it never resolved. One helper, so\n * the two refusals are one sentence each with the flag swapped in, rather\n * than two copies drifting.\n */\nexport interface BaseRequest {\n /** The flag the ref arrived on, as the user typed it: `--base`, `--since`. */\n flag: string;\n ref: string;\n /** What the command reads at the ref, as the subject of a sentence: \"loam diff reads the base state\". */\n reads: string;\n}\n\n/**\n * The resolved base, or null after refusing: `repository-unavailable` when\n * the docs repo is not somewhere git can answer from, `unknown-target` when\n * the ref names no commit there (an option-shaped ref included —\n * `resolveBase` refuses it before it can reach an argv).\n */\nexport async function baseOrRefuse(\n json: boolean,\n docsDir: DocsDir,\n req: BaseRequest,\n): Promise<ResolvedBase | null> {\n const base = await resolveBase(docsDir, req.ref);\n if (base.kind === \"no-git\") {\n fail(\n json,\n \"repository-unavailable\",\n `The docs repo at ${docsDir} is not somewhere git can answer from (${base.detail}) — ` +\n `${req.reads} with read-only git questions (rev-parse/ls-tree/show), so it needs the docs repo ` +\n `to be a git checkout with the ${req.flag} ref present. Clone the docs repo with its history, then re-run.`,\n );\n return null;\n }\n if (base.kind === \"no-ref\") {\n fail(\n json,\n \"unknown-target\",\n `${req.flag} ${req.ref} does not resolve to a commit in the docs repo at ${docsDir}` +\n `${base.detail === \"\" ? \"\" : ` (${base.detail})`}. ` +\n `Pass a ref that exists there — main, origin/main, a tag or a commit sha — fetching first if it lives on the remote.`,\n );\n return null;\n }\n return { docsDir, commit: base.commit, prefix: base.prefix };\n}\n"]}
|
|
@@ -127,7 +127,7 @@ export async function planOpenapi(repo, service, openapiPath) {
|
|
|
127
127
|
// writes one file once. Order is safe: a surface digest never includes an
|
|
128
128
|
// operation pin (`restatedSurfaces` skips the methods entirely), and the
|
|
129
129
|
// root record never enters an operation digest.
|
|
130
|
-
baselines = planOpenapiBaselines(plan.text ?? delta, living
|
|
130
|
+
baselines = planOpenapiBaselines(plan.text ?? delta, living);
|
|
131
131
|
}
|
|
132
132
|
catch (err) {
|
|
133
133
|
// A document loam cannot read is validate's diagnosis to make
|
|
@@ -193,7 +193,7 @@ export async function planAsyncapi(repo, service, asyncapiPath) {
|
|
|
193
193
|
// directions: a surface digest never includes a slot pin (`asyncapiSurfaces`
|
|
194
194
|
// skips `components.messages` entirely, and no other section is a
|
|
195
195
|
// surface), and the root record never enters a slot digest.
|
|
196
|
-
baselines = planAsyncapiBaselines(plan.text ?? delta, living
|
|
196
|
+
baselines = planAsyncapiBaselines(plan.text ?? delta, living);
|
|
197
197
|
}
|
|
198
198
|
catch (err) {
|
|
199
199
|
// A document loam cannot read is validate's diagnosis to make
|