@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 +1 @@
|
|
|
1
|
-
{"version":3,"file":"stamp.js","sourceRoot":"","sources":["../../../src/core/provenance/stamp.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The stamp vocabulary: what `loam vouch` computes for a frontmatter stamp and\n * what a later check reads back out of one — `sources_digest` and the per-file\n * `sources_files` index beside it, the document's own `content_digest`, and\n * the local calendar day `last_verified` carries.\n */\nimport { createHash } from \"node:crypto\";\nimport { readFile } from \"node:fs/promises\";\nimport { rawBody } from \"../document/frontmatter.js\";\nimport { collectSources, type SkippedSource } from \"./walk.js\";\n\n/**\n * The local calendar day, for the two commands that date a stamp. A vouch or a\n * verification record is a person saying \"today I read this\", so it is their\n * date, not UTC's — `toISOString` files an evening in the Americas under\n * tomorrow. It sits here because this module already owns the stamp vocabulary\n * the date is written into.\n */\nexport function today(now: Date): string {\n const pad = (n: number): string => String(n).padStart(2, \"0\");\n return `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}`;\n}\n\n/* ------------------------------------------------------------------ */\n/* The content digest */\n/* ------------------------------------------------------------------ */\n\n/** How much of the sha256 is written into the document. */\nconst DIGEST_LENGTH = 16;\n\n/** How many source files the digest reads at once — see `sourcesDigest`. */\nconst DIGEST_READ_BATCH = 32;\n\nexport interface SourcesDigest extends SourcesExpansion {\n /** The stamp that goes in `sources_digest`. */\n digest: string;\n /**\n * Per file, its own short sha — the record `sources_files` carries so a later\n * `sources.stale` can name what moved instead of guessing. Same order as `files`.\n */\n index: SourceIndexEntry[];\n}\n\n/** One file's share of the stamp: what it is called, and what it said. */\nexport interface SourceIndexEntry {\n /** Repo-relative, `/`-separated. */\n path: string;\n /** sha256 of its bytes, truncated the way `sources_digest` is. */\n sha: string;\n}\n\n/** What a `sources` list covers, and what it does not. */\nexport interface SourcesExpansion {\n /** Repo-relative paths, sorted — exactly what a digest would hash. */\n files: string[];\n /** Everything the walk refused to follow. Empty in the ordinary case. */\n skipped: SkippedSource[];\n /**\n * Set only when `files` is empty: the one sentence that says the list covers\n * nothing. `loam vouch` refuses with it, `loam validate` grades it — see\n * emptySourcesMessage for why they must be the same words.\n */\n empty?: string;\n}\n\n/**\n * Digest the CONTENT of the files `sources` names. The recipe is spelled out\n * because the value is written into documents, and anything reading them has to\n * be able to reproduce it:\n *\n * 1. expand each entry to repo-relative file paths (`/` separators) — a file\n * is itself, a directory everything beneath it, minus what the repository\n * does not consider its own (see collectSources) — sorted and de-duplicated;\n * 2. per file, `sha256(bytes)`;\n * 3. feed `<path>\\0<hex>\\n` for each file, in that order, into an outer\n * sha256;\n * 4. keep the first 16 hex characters.\n *\n * Content, not mtime: git does not preserve modification times, so after a\n * fresh clone every file looks changed and the check would be false positives\n * end to end. Bytes survive the clone. Hashing the path alongside the content\n * means a rename registers, which is what a reader of the doc would want.\n *\n * 64 bits is a change detector, not a seal — it answers \"did this move?\", and\n * an adversary who wants a collision can have one.\n */\nexport async function sourcesDigest(repoDir: string, sources: string[]): Promise<SourcesDigest> {\n const { found, skipped } = await collectSources(repoDir, sources);\n const outer = createHash(\"sha256\");\n const index: SourceIndexEntry[] = [];\n // Read in fixed batches, hash in array order. `found` is already sorted and\n // every file's own sha depends on nothing but its own bytes, so the sequence\n // fed to `outer` — and therefore the digest — is unchanged by construction;\n // only the waiting overlaps. A `sources: [src/]` over a real service repo is\n // thousands of files, and one-await-per-file spent nearly all of its time\n // idle. The batch is bounded rather than a single `Promise.all` over the\n // whole list because an unbounded fan-out over a source tree runs the process\n // out of file descriptors, which would turn a stamp into an EMFILE.\n for (let start = 0; start < found.length; start += DIGEST_READ_BATCH) {\n const hashed = await Promise.all(\n found.slice(start, start + DIGEST_READ_BATCH).map(async (file) => ({\n rel: file.rel,\n content: createHash(\"sha256\").update(await readFile(file.abs)).digest(\"hex\"),\n })),\n );\n for (const { rel, content } of hashed) {\n outer.update(`${rel}\\0${content}\\n`);\n index.push({ path: rel, sha: content.slice(0, DIGEST_LENGTH) });\n }\n }\n // `empty` is deliberately not set here: it is a sentence about a named\n // document, and a digest has no name to put in it. Callers that need it say\n // emptySourcesMessage(label, sources) — the same words, from one definition.\n return { digest: outer.digest(\"hex\").slice(0, DIGEST_LENGTH), files: index.map((e) => e.path), index, skipped };\n}\n\n/**\n * What a `sources` list actually covers — the digest's file set without the\n * hashing.\n *\n * Exported because two commands must give the SAME answer to \"does this list\n * cover anything?\". `loam vouch` refuses to stamp an expansion that covers no\n * files (a digest over nothing never changes, so the stamp would read as\n * current forever), and until `loam validate` could say the same thing in the\n * same words, an author got a green validate followed by a vouch that refused —\n * two commands contradicting each other about one document, with nothing in the\n * green run hinting at it.\n */\nexport async function expandSourceFiles(\n repoDir: string,\n sources: string[],\n label: string,\n): Promise<SourcesExpansion> {\n const { found, skipped } = await collectSources(repoDir, sources);\n const files = found.map((f) => f.rel);\n return { files, skipped, ...(files.length === 0 ? { empty: emptySourcesMessage(label, sources) } : {}) };\n}\n\n/**\n * The one sentence for \"these paths exist and cover no file\". One definition,\n * because `vouch`'s refusal and `validate`'s finding are the same diagnosis\n * about the same document and must not drift into two descriptions of it — the\n * author fixes it once, in the sources list.\n */\nexport function emptySourcesMessage(label: string, sources: string[]): string {\n return `${label}: the sources listed match no files — ${sources.join(\", \")}. A digest over nothing would read as current forever.`;\n}\n\n/**\n * The stamp `loam vouch` writes into `content_digest`: sha256 of the\n * document's own BODY — every byte after the frontmatter block (below the\n * closing `---` line and its newline; `rawBody` is the one definition of that\n * cut) — first 16 hex characters, the sources recipe's length.\n *\n * Byte-exact, no normalization. Body-only is load-bearing: vouch itself\n * rewrites the frontmatter as it stamps, and a later frontmatter-only edit\n * (another vouch, a corrected owner) must not read as the document moving.\n */\nexport function contentDigest(source: string): string {\n return createHash(\"sha256\").update(rawBody(source), \"utf8\").digest(\"hex\").slice(0, DIGEST_LENGTH);\n}\n\n/* ------------------------------------------------------------------ */\n/* The per-file index: what `sources.stale` names */\n/* ------------------------------------------------------------------ */\n\n/**\n * How many files `sources_files` will list one by one before it gives up and\n * records only how many there were.\n *\n * A readability budget, not a correctness one: the index lives in the header of\n * a document a person reads, and a `sources: [src/]` over a legacy service can\n * expand to thousands of files. Past the limit `sources.stale` falls back to\n * repeating the `sources` entries — worse advice, but a spec.md whose\n * frontmatter is longer than its requirements is worse still.\n */\nconst SOURCE_INDEX_LIMIT = 100;\n\n/**\n * The `sources_files` value `loam vouch` stamps beside `sources_digest`: one\n * `<sha> <path>` line per file (sha256sum's layout, and its column order, so\n * the path may contain spaces), or just the file count once the list would run\n * past SOURCE_INDEX_LIMIT.\n *\n * Its whole purpose is the next `sources.stale`. `sources_digest` can say THAT\n * the code moved and never which part of it; with the index a reader gets the\n * added, removed and changed paths, which is the difference between re-reading\n * one file and re-reading a directory. It is also a readable git diff in the\n * docs repo: the vouch commit shows exactly which source files the stamp now\n * stands over.\n */\nexport function encodeSourceIndex(index: SourceIndexEntry[]): string {\n if (index.length > SOURCE_INDEX_LIMIT) return String(index.length);\n return index.map((e) => `${e.sha} ${e.path}`).join(\"\\n\");\n}\n\n/** What a stamped `sources_files` says: the per-file shas, the count, or nothing. */\ninterface StampedIndex {\n /** path -> sha, or null when the stamp did not record them. */\n entries: Map<string, string> | null;\n /** How many files the stamp covered, when that is knowable. */\n count?: number;\n}\n\n/**\n * Read back what `encodeSourceIndex` wrote. Anything unrecognised reads as \"the\n * stamp said nothing\" rather than as an empty index: a header nobody can parse\n * must not turn into a `sources.stale` claiming every file was deleted.\n */\nexport function decodeSourceIndex(stamped: string | undefined): StampedIndex {\n if (stamped === undefined) return { entries: null };\n const text = stamped.trim();\n if (text.length === 0) return { entries: null };\n if (/^\\d+$/.test(text)) return { entries: null, count: Number(text) };\n\n const entries = new Map<string, string>();\n for (const line of text.split(\"\\n\")) {\n const match = /^([0-9a-f]{16})\\s+(.+)$/.exec(line.trim());\n if (match === null) return { entries: null };\n entries.set(match[2]!, match[1]!);\n }\n return { entries, count: entries.size };\n}\n\n/** The paths that moved between a stamp and now, with a phrase for the message. */\ninterface MovedSources {\n /** Annotated paths, or undefined when the stamp did not record enough to say. */\n paths?: string[];\n /** Appended to the `sources.stale` message; empty when there is nothing to add. */\n summary: string;\n}\n\n/**\n * The structured half of the answer: which paths were added, changed and\n * removed between a stamp and the current index — or null when the stamp\n * cannot say. Null covers two distinct silences on purpose, both of which mean\n * \"do not print a path list\": a stamp that recorded no per-file entries (the\n * count-only >100-file fallback, or a header nobody could parse), and an index\n * that AGREES with the current tree while the caller's digest comparison said\n * stale — a contradiction (a truncation collision, or a hand-edited header),\n * where an empty delta under a staleness warning would read as proof that\n * nothing moved. Callers that need the counts anyway read `stamp.count`.\n *\n * Extracted from `movedSources` (which now consumes it) when the re-vouch\n * reading pack became a second reader: the pack wants the three lists as\n * data, the stale finding wants them annotated in one column.\n */\nexport function sourceDelta(\n stamp: StampedIndex,\n now: SourceIndexEntry[],\n): { added: string[]; changed: string[]; removed: string[] } | null {\n if (stamp.entries === null) return null;\n const added: string[] = [];\n const changed: string[] = [];\n const current = new Map(now.map((e) => [e.path, e.sha]));\n for (const [path, sha] of current) {\n const before = stamp.entries.get(path);\n if (before === undefined) added.push(path);\n else if (before !== sha) changed.push(path);\n }\n const removed = [...stamp.entries.keys()].filter((path) => !current.has(path));\n if (added.length + changed.length + removed.length === 0) return null;\n added.sort();\n changed.sort();\n removed.sort();\n return { added, changed, removed };\n}\n\n/**\n * added / removed / changed, from the stamped index against the current one.\n * The annotation travels IN the detail line rather than in three separate\n * lists: a reader scanning a stale report wants the paths in one column, and\n * \"which of the three\" is one word wide.\n */\nexport function movedSources(stamp: StampedIndex, now: SourceIndexEntry[]): MovedSources {\n const delta = sourceDelta(stamp, now);\n if (delta === null) {\n // A recorded index that came back null is the contradiction case — say\n // nothing rather than print an empty list under a staleness warning. With\n // no index at all, the count alone still beats silence: \"12 files then,\n // 14 now\" tells a reader to go looking for two new files.\n return {\n summary:\n stamp.entries !== null || stamp.count === undefined\n ? \"\"\n : ` (${stamp.count} file(s) then, ${now.length} now)`,\n };\n }\n const paths = [\n ...delta.added.map((path) => `added ${path}`),\n ...delta.changed.map((path) => `changed ${path}`),\n ...delta.removed.map((path) => `removed ${path}`),\n ].sort();\n return { paths, summary: ` (${paths.length} path(s) moved)` };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"stamp.js","sourceRoot":"","sources":["../../../src/core/provenance/stamp.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The stamp vocabulary: what `loam vouch` computes for a frontmatter stamp and\n * what a later check reads back out of one — `sources_digest` and the per-file\n * `sources_files` index beside it, and the local calendar day `last_verified`\n * carries. The document's own `content_digest` lived here too; it moved to\n * core/document/frontmatter.ts, beside the body cut it hashes, when the fleet\n * enumeration needed it (`contentDigest`'s docblock says why).\n */\nimport { createHash } from \"node:crypto\";\nimport { readFile } from \"node:fs/promises\";\nimport { collectSources, type SkippedSource } from \"./walk.js\";\n\n/**\n * The local calendar day, for the two commands that date a stamp. A vouch or a\n * verification record is a person saying \"today I read this\", so it is their\n * date, not UTC's — `toISOString` files an evening in the Americas under\n * tomorrow. It sits here because this module already owns the stamp vocabulary\n * the date is written into.\n */\nexport function today(now: Date): string {\n const pad = (n: number): string => String(n).padStart(2, \"0\");\n return `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}`;\n}\n\n/* ------------------------------------------------------------------ */\n/* The sources digest */\n/* ------------------------------------------------------------------ */\n\n/** How much of the sha256 is written into the document. */\nconst DIGEST_LENGTH = 16;\n\n/** How many source files the digest reads at once — see `sourcesDigest`. */\nconst DIGEST_READ_BATCH = 32;\n\nexport interface SourcesDigest extends SourcesExpansion {\n /** The stamp that goes in `sources_digest`. */\n digest: string;\n /**\n * Per file, its own short sha — the record `sources_files` carries so a later\n * `sources.stale` can name what moved instead of guessing. Same order as `files`.\n */\n index: SourceIndexEntry[];\n}\n\n/** One file's share of the stamp: what it is called, and what it said. */\nexport interface SourceIndexEntry {\n /** Repo-relative, `/`-separated. */\n path: string;\n /** sha256 of its bytes, truncated the way `sources_digest` is. */\n sha: string;\n}\n\n/** What a `sources` list covers, and what it does not. */\nexport interface SourcesExpansion {\n /** Repo-relative paths, sorted — exactly what a digest would hash. */\n files: string[];\n /** Everything the walk refused to follow. Empty in the ordinary case. */\n skipped: SkippedSource[];\n /**\n * Set only when `files` is empty: the one sentence that says the list covers\n * nothing. `loam vouch` refuses with it, `loam validate` grades it — see\n * emptySourcesMessage for why they must be the same words.\n */\n empty?: string;\n}\n\n/**\n * Digest the CONTENT of the files `sources` names. The recipe is spelled out\n * because the value is written into documents, and anything reading them has to\n * be able to reproduce it:\n *\n * 1. expand each entry to repo-relative file paths (`/` separators) — a file\n * is itself, a directory everything beneath it, minus what the repository\n * does not consider its own (see collectSources) — sorted and de-duplicated;\n * 2. per file, `sha256(bytes)`;\n * 3. feed `<path>\\0<hex>\\n` for each file, in that order, into an outer\n * sha256;\n * 4. keep the first 16 hex characters.\n *\n * Content, not mtime: git does not preserve modification times, so after a\n * fresh clone every file looks changed and the check would be false positives\n * end to end. Bytes survive the clone. Hashing the path alongside the content\n * means a rename registers, which is what a reader of the doc would want.\n *\n * 64 bits is a change detector, not a seal — it answers \"did this move?\", and\n * an adversary who wants a collision can have one.\n */\nexport async function sourcesDigest(repoDir: string, sources: string[]): Promise<SourcesDigest> {\n const { found, skipped } = await collectSources(repoDir, sources);\n const outer = createHash(\"sha256\");\n const index: SourceIndexEntry[] = [];\n // Read in fixed batches, hash in array order. `found` is already sorted and\n // every file's own sha depends on nothing but its own bytes, so the sequence\n // fed to `outer` — and therefore the digest — is unchanged by construction;\n // only the waiting overlaps. A `sources: [src/]` over a real service repo is\n // thousands of files, and one-await-per-file spent nearly all of its time\n // idle. The batch is bounded rather than a single `Promise.all` over the\n // whole list because an unbounded fan-out over a source tree runs the process\n // out of file descriptors, which would turn a stamp into an EMFILE.\n for (let start = 0; start < found.length; start += DIGEST_READ_BATCH) {\n const hashed = await Promise.all(\n found.slice(start, start + DIGEST_READ_BATCH).map(async (file) => ({\n rel: file.rel,\n content: createHash(\"sha256\").update(await readFile(file.abs)).digest(\"hex\"),\n })),\n );\n for (const { rel, content } of hashed) {\n outer.update(`${rel}\\0${content}\\n`);\n index.push({ path: rel, sha: content.slice(0, DIGEST_LENGTH) });\n }\n }\n // `empty` is deliberately not set here: it is a sentence about a named\n // document, and a digest has no name to put in it. Callers that need it say\n // emptySourcesMessage(label, sources) — the same words, from one definition.\n return { digest: outer.digest(\"hex\").slice(0, DIGEST_LENGTH), files: index.map((e) => e.path), index, skipped };\n}\n\n/**\n * What a `sources` list actually covers — the digest's file set without the\n * hashing.\n *\n * Exported because two commands must give the SAME answer to \"does this list\n * cover anything?\". `loam vouch` refuses to stamp an expansion that covers no\n * files (a digest over nothing never changes, so the stamp would read as\n * current forever), and until `loam validate` could say the same thing in the\n * same words, an author got a green validate followed by a vouch that refused —\n * two commands contradicting each other about one document, with nothing in the\n * green run hinting at it.\n */\nexport async function expandSourceFiles(\n repoDir: string,\n sources: string[],\n label: string,\n): Promise<SourcesExpansion> {\n const { found, skipped } = await collectSources(repoDir, sources);\n const files = found.map((f) => f.rel);\n return { files, skipped, ...(files.length === 0 ? { empty: emptySourcesMessage(label, sources) } : {}) };\n}\n\n/**\n * The one sentence for \"these paths exist and cover no file\". One definition,\n * because `vouch`'s refusal and `validate`'s finding are the same diagnosis\n * about the same document and must not drift into two descriptions of it — the\n * author fixes it once, in the sources list.\n */\nexport function emptySourcesMessage(label: string, sources: string[]): string {\n return `${label}: the sources listed match no files — ${sources.join(\", \")}. A digest over nothing would read as current forever.`;\n}\n\n/* ------------------------------------------------------------------ */\n/* The per-file index: what `sources.stale` names */\n/* ------------------------------------------------------------------ */\n\n/**\n * How many files `sources_files` will list one by one before it gives up and\n * records only how many there were.\n *\n * A readability budget, not a correctness one: the index lives in the header of\n * a document a person reads, and a `sources: [src/]` over a legacy service can\n * expand to thousands of files. Past the limit `sources.stale` falls back to\n * repeating the `sources` entries — worse advice, but a spec.md whose\n * frontmatter is longer than its requirements is worse still.\n */\nconst SOURCE_INDEX_LIMIT = 100;\n\n/**\n * The `sources_files` value `loam vouch` stamps beside `sources_digest`: one\n * `<sha> <path>` line per file (sha256sum's layout, and its column order, so\n * the path may contain spaces), or just the file count once the list would run\n * past SOURCE_INDEX_LIMIT.\n *\n * Its whole purpose is the next `sources.stale`. `sources_digest` can say THAT\n * the code moved and never which part of it; with the index a reader gets the\n * added, removed and changed paths, which is the difference between re-reading\n * one file and re-reading a directory. It is also a readable git diff in the\n * docs repo: the vouch commit shows exactly which source files the stamp now\n * stands over.\n */\nexport function encodeSourceIndex(index: SourceIndexEntry[]): string {\n if (index.length > SOURCE_INDEX_LIMIT) return String(index.length);\n return index.map((e) => `${e.sha} ${e.path}`).join(\"\\n\");\n}\n\n/** What a stamped `sources_files` says: the per-file shas, the count, or nothing. */\ninterface StampedIndex {\n /** path -> sha, or null when the stamp did not record them. */\n entries: Map<string, string> | null;\n /** How many files the stamp covered, when that is knowable. */\n count?: number;\n}\n\n/**\n * Read back what `encodeSourceIndex` wrote. Anything unrecognised reads as \"the\n * stamp said nothing\" rather than as an empty index: a header nobody can parse\n * must not turn into a `sources.stale` claiming every file was deleted.\n */\nexport function decodeSourceIndex(stamped: string | undefined): StampedIndex {\n if (stamped === undefined) return { entries: null };\n const text = stamped.trim();\n if (text.length === 0) return { entries: null };\n if (/^\\d+$/.test(text)) return { entries: null, count: Number(text) };\n\n const entries = new Map<string, string>();\n for (const line of text.split(\"\\n\")) {\n const match = /^([0-9a-f]{16})\\s+(.+)$/.exec(line.trim());\n if (match === null) return { entries: null };\n entries.set(match[2]!, match[1]!);\n }\n return { entries, count: entries.size };\n}\n\n/** The paths that moved between a stamp and now, with a phrase for the message. */\ninterface MovedSources {\n /** Annotated paths, or undefined when the stamp did not record enough to say. */\n paths?: string[];\n /** Appended to the `sources.stale` message; empty when there is nothing to add. */\n summary: string;\n}\n\n/**\n * The structured half of the answer: which paths were added, changed and\n * removed between a stamp and the current index — or null when the stamp\n * cannot say. Null covers two distinct silences on purpose, both of which mean\n * \"do not print a path list\": a stamp that recorded no per-file entries (the\n * count-only >100-file fallback, or a header nobody could parse), and an index\n * that AGREES with the current tree while the caller's digest comparison said\n * stale — a contradiction (a truncation collision, or a hand-edited header),\n * where an empty delta under a staleness warning would read as proof that\n * nothing moved. Callers that need the counts anyway read `stamp.count`.\n *\n * Extracted from `movedSources` (which now consumes it) when the re-vouch\n * reading pack became a second reader: the pack wants the three lists as\n * data, the stale finding wants them annotated in one column.\n */\nexport function sourceDelta(\n stamp: StampedIndex,\n now: SourceIndexEntry[],\n): { added: string[]; changed: string[]; removed: string[] } | null {\n if (stamp.entries === null) return null;\n const added: string[] = [];\n const changed: string[] = [];\n const current = new Map(now.map((e) => [e.path, e.sha]));\n for (const [path, sha] of current) {\n const before = stamp.entries.get(path);\n if (before === undefined) added.push(path);\n else if (before !== sha) changed.push(path);\n }\n const removed = [...stamp.entries.keys()].filter((path) => !current.has(path));\n if (added.length + changed.length + removed.length === 0) return null;\n added.sort();\n changed.sort();\n removed.sort();\n return { added, changed, removed };\n}\n\n/**\n * added / removed / changed, from the stamped index against the current one.\n * The annotation travels IN the detail line rather than in three separate\n * lists: a reader scanning a stale report wants the paths in one column, and\n * \"which of the three\" is one word wide.\n */\nexport function movedSources(stamp: StampedIndex, now: SourceIndexEntry[]): MovedSources {\n const delta = sourceDelta(stamp, now);\n if (delta === null) {\n // A recorded index that came back null is the contradiction case — say\n // nothing rather than print an empty list under a staleness warning. With\n // no index at all, the count alone still beats silence: \"12 files then,\n // 14 now\" tells a reader to go looking for two new files.\n return {\n summary:\n stamp.entries !== null || stamp.count === undefined\n ? \"\"\n : ` (${stamp.count} file(s) then, ${now.length} now)`,\n };\n }\n const paths = [\n ...delta.added.map((path) => `added ${path}`),\n ...delta.changed.map((path) => `changed ${path}`),\n ...delta.removed.map((path) => `removed ${path}`),\n ].sort();\n return { paths, summary: ` (${paths.length} path(s) moved)` };\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"entries.js","sourceRoot":"","sources":["../../../src/core/repo/entries.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * What the docs repo's two enumerations hand back, and how their ids compare.\n *\n * Split from the read model because these are the shapes every OTHER module\n * names — `ServiceEntry` is the type a dozen call sites annotate against — and\n * a shape should not drag the `readdir` that produces it into every importer.\n * `compareIds` lives here for the same reason: sorting ids is what a renderer\n * does, and no renderer wants the enumeration.\n */\nimport { serviceTreePath, type FeatureDir, type ServiceDir } from \"../kernel/ids/dirs.js\";\nimport { rawFeatureId, type RawFeatureId } from \"../kernel/ids/feature.js\";\nimport type { RawServiceId } from \"../kernel/ids/service.js\";\nimport type { Finding } from \"../vocabulary/report.js\";\n\n\nexport interface ServiceArtifacts {\n model: boolean;\n spec: boolean;\n openapi: boolean;\n /**\n * The async contract (AsyncAPI 3). Presence-tracked only, like `runbook` and\n * `health` — deliberately NOT part of the maturity ladder (core/vocabulary/maturity.ts),\n * whose `documented` rung is the model/spec/openapi triple. Adding a fourth\n * required artifact would demote every already-`documented` service in the\n * fleet on upgrade, without one byte of their files changing.\n */\n asyncapi: boolean;\n runbook: boolean;\n health: boolean;\n}\n\nexport interface ServiceEntry {\n /** Canonical service id — the LEAF directory name, at whatever depth the tree walk found it. */\n id: RawServiceId;\n /** Absolute path to the service's directory, as enumerated — the value every artifact path is resolved from. */\n dir: ServiceDir;\n /**\n * Names of the subsystem directories between `services/` and the service,\n * outermost first. Empty = unfiled — directly under `services/`, which is\n * permanent, normal and never a finding. Placement only: no identity, no\n * check and no join may branch on it.\n */\n subsystem: string[];\n has: ServiceArtifacts;\n /** Number of ADR files under adrs/. */\n adrs: number;\n /** `status` from the living spec's frontmatter; null when nobody has said. */\n status: string | null;\n /**\n * Provenance signals from the living spec's frontmatter: whether it declares\n * any `sources`, and whether a `sources_digest` stamp exists. Presence only —\n * whether the digest still matches the code is `validate`'s question, and it\n * can only be answered from inside the service's own repo.\n */\n sources: { declared: boolean; stamped: boolean };\n /**\n * \"sampled\" when EITHER spec axis carries a `vouch_scope` — a vouch a person\n * gave after reading a recorded sample of the document rather than all of\n * it — and null when neither does. Either axis, because the sample is per\n * file: a service whose arch.spec.md alone was sampled is a service part of\n * whose documentation nobody read.\n *\n * PRESENCE decides it, not decodability: a scope nobody can parse still\n * means somebody stamped a partial read, so it grades as sampled. Fail\n * closed, because the alternative is that mangling one field turns a partial\n * vouch into a full one on every dashboard reading this table.\n *\n * The RUNG never changes: `vouched` is a published contract and this is a\n * qualification of it, so the maturity ladder, the rollup and `--needs-work`\n * are all untouched. Only the renderings say \"vouched (sampled)\".\n */\n vouchScope: \"sampled\" | null;\n /**\n * Why no loam command can author this directory, or absent when the name is a\n * legal service id — `serviceIdProblem`'s own sentence, so the read model and\n * the refusal say the same thing.\n *\n * The enumeration still LISTS it: the directory is there, it holds somebody's\n * documents, and hiding it would repeat the symlink mistake one field up. But\n * every authoring command (`adopt`, `delta`, `rebase`, `new --touches`,\n * `init`) refuses this id with `invalid-option`, so grading it as an ordinary\n * service left the fleet gate demanding fixes no loam command can make. The\n * NAME is the finding — see `serviceIdFindings`.\n */\n idProblem?: string;\n}\n\nexport interface FeatureEntry {\n /** Feature id, derived from the directory name (FEAT-1-split -> FEAT-1). */\n id: RawFeatureId;\n /** The directory name as it is on disk, slug and all. */\n dirName: string;\n /** Absolute path to the feature directory. */\n dir: FeatureDir;\n archived: boolean;\n /** Services this feature carries a delta for, from specs/<svc>/. */\n services: RawServiceId[];\n has: { intent: boolean; delta: boolean };\n}\n\n/**\n * The directories in the `services/` tree that no loam command can address.\n *\n * A finding rather than a silent skip, and an ERROR rather than a warning,\n * because the state is not survivable in either direction: `loam adopt`,\n * `loam delta`, `loam rebase` and `loam new --touches` all refuse the id with\n * `invalid-option`, so the documents in there can never be updated by loam\n * again, while `validate --all` used to grade the directory as an ordinary\n * service and demand `model.likec4`, a spec and a landscape binding for it. The\n * fix is a rename, and it is a rename loam cannot do for you — the id is\n * spelled in the landscape binding, in the frontmatter and in every feature\n * that touched the service — so the message names all three.\n *\n * Emitted from the fleet target (`validate --all`), where the SET of service\n * directories is the thing being checked; a per-service run is already refused\n * by its own `--service` guard before it gets here. Lives beside the entry\n * shape rather than the enumeration because it is a projection of `idProblem`,\n * not a read — the entry docblock already points here.\n */\nexport function serviceIdFindings(services: ServiceEntry[]): Finding[] {\n return services\n .filter((s) => s.idProblem !== undefined)\n .map((s) => ({\n severity: \"error\" as const,\n code: \"service.id-invalid\",\n subject: s.id,\n message:\n // The path is spelled from the entry's own placement chain, not a\n // `services/<id>/` join: a FILED service with an illegal name lives at\n // services/<subsystem>/…/<id>/, and a finding naming a root directory\n // that does not exist sends the fix to the wrong place.\n `${serviceTreePath(s)}/ — ${s.idProblem} ` +\n `Every authoring command refuses this id (\\`loam adopt\\`, \\`loam delta\\`, \\`loam new --touches\\`), so nothing in that directory can be changed through loam. ` +\n `Rename the directory to a legal id, then update its \\`service:\\` frontmatter, its \\`metadata { service '${s.id}' }\\` binding in architecture/landscape.likec4, and any features/<FEAT>/specs/${s.id}/ that names it.`,\n }));\n}\n\n/**\n * Compare ids so digit runs sort numerically: FEAT-2 before FEAT-10.\n * Deterministic and locale-independent — ordering is part of the output\n * contract for `list`, and `--json` consumers diff it.\n */\nexport function compareIds(a: string, b: string): number {\n const ta = tokenize(a);\n const tb = tokenize(b);\n for (let i = 0; i < Math.max(ta.length, tb.length); i += 1) {\n const x = ta[i];\n const y = tb[i];\n if (x === undefined) return -1;\n if (y === undefined) return 1;\n if (typeof x === \"number\" && typeof y === \"number\") {\n if (x !== y) return x - y;\n } else if (String(x) !== String(y)) {\n return String(x) < String(y) ? -1 : 1;\n }\n }\n return 0;\n}\n\nfunction tokenize(s: string): (string | number)[] {\n return (s.match(/\\d+|\\D+/g) ?? []).map((t) => (/^\\d/.test(t) ? Number(t) : t));\n}\n\n/**\n * Known ids within a small edit distance of `id`, closest first, at most three.\n *\n * The did-you-mean hint for a name somebody typed: `loam new --touches` and\n * `loam delta <svc>` answer the same mistake, and each carried its own copy of\n * these twenty-four lines. It belongs here because the ids being scored are\n * this module's ids — service directory names, feature ids — and because\n * `compareIds` right above is the tiebreak that keeps the list deterministic\n * when two candidates score equally. Not in `core/kernel/ids/`: `compareIds` lives\n * here and repo.ts imports ids.js, so putting it there would close a cycle.\n *\n * `arch.ts`'s `closeIds` looks similar and is not: substring and prefix, capped\n * at five, answering \"which element could this be\" rather than \"did you\n * misspell a directory\". Two rules, two homes.\n */\nexport function nearestIds(id: string, known: string[]): string[] {\n const budget = Math.max(1, Math.floor(id.length / 4));\n return known\n .map((candidate) => ({ candidate, distance: editDistance(id.toLowerCase(), candidate.toLowerCase()) }))\n .filter((scored) => scored.distance <= budget)\n .sort((a, b) => a.distance - b.distance || compareIds(a.candidate, b.candidate))\n .slice(0, 3)\n .map((scored) => scored.candidate);\n}\n\n/** Plain Levenshtein — ids are short, and the row-at-a-time form keeps it obvious. */\nfunction editDistance(a: string, b: string): number {\n let previous = Array.from({ length: b.length + 1 }, (_, i) => i);\n for (let i = 1; i <= a.length; i += 1) {\n const current = [i];\n for (let j = 1; j <= b.length; j += 1) {\n const substitution = previous[j - 1]! + (a[i - 1] === b[j - 1] ? 0 : 1);\n current.push(Math.min(previous[j]! + 1, current[j - 1]! + 1, substitution));\n }\n previous = current;\n }\n return previous[b.length]!;\n}\n\n/**\n * Feature id from a directory name: everything up to and including the first\n * number run (`FEAT-101-payment-splitting` -> `FEAT-101`). A name with no\n * `<word>-<number>` head is its own id. Quirk: a dated slug keeps only its first\n * segment (`release-2024-01-x` -> `release-2024`) — ids are not meant to be dates.\n */\nexport function featureIdFromDirName(dirName: string): RawFeatureId {\n const m = /^(.*?-\\d+)(?:-|$)/.exec(dirName);\n // Constructed through the kernel helper, so this module holds no cast: the\n // derived id's provenance is the repository — a directory the enumeration\n // listed — which is exactly what `RawFeatureId` records.\n return rawFeatureId(m ? m[1]! : dirName);\n}\n"]}
|
|
1
|
+
{"version":3,"file":"entries.js","sourceRoot":"","sources":["../../../src/core/repo/entries.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * What the docs repo's two enumerations hand back, and how their ids compare.\n *\n * Split from the read model because these are the shapes every OTHER module\n * names — `ServiceEntry` is the type a dozen call sites annotate against — and\n * a shape should not drag the `readdir` that produces it into every importer.\n * `compareIds` lives here for the same reason: sorting ids is what a renderer\n * does, and no renderer wants the enumeration.\n */\nimport { serviceTreePath, type FeatureDir, type ServiceDir } from \"../kernel/ids/dirs.js\";\nimport { rawFeatureId, type RawFeatureId } from \"../kernel/ids/feature.js\";\nimport type { RawServiceId } from \"../kernel/ids/service.js\";\nimport type { Finding } from \"../vocabulary/report.js\";\n\n\nexport interface ServiceArtifacts {\n model: boolean;\n spec: boolean;\n openapi: boolean;\n /**\n * The async contract (AsyncAPI 3). Presence-tracked only, like `runbook` and\n * `health` — deliberately NOT part of the maturity ladder (core/vocabulary/maturity.ts),\n * whose `documented` rung is the model/spec/openapi triple. Adding a fourth\n * required artifact would demote every already-`documented` service in the\n * fleet on upgrade, without one byte of their files changing.\n */\n asyncapi: boolean;\n runbook: boolean;\n health: boolean;\n}\n\nexport interface ServiceEntry {\n /** Canonical service id — the LEAF directory name, at whatever depth the tree walk found it. */\n id: RawServiceId;\n /** Absolute path to the service's directory, as enumerated — the value every artifact path is resolved from. */\n dir: ServiceDir;\n /**\n * Names of the subsystem directories between `services/` and the service,\n * outermost first. Empty = unfiled — directly under `services/`, which is\n * permanent, normal and never a finding. Placement only: no identity, no\n * check and no join may branch on it.\n */\n subsystem: string[];\n has: ServiceArtifacts;\n /** Number of ADR files under adrs/. */\n adrs: number;\n /** `status` from the living spec's frontmatter; null when nobody has said. */\n status: string | null;\n /**\n * Provenance signals from the living spec's frontmatter: whether it declares\n * any `sources`, and whether a `sources_digest` stamp exists. Presence only —\n * whether the digest still matches the code is `validate`'s question, and it\n * can only be answered from inside the service's own repo.\n */\n sources: { declared: boolean; stamped: boolean };\n /**\n * \"sampled\" when EITHER spec axis carries a `vouch_scope` — a vouch a person\n * gave after reading a recorded sample of the document rather than all of\n * it — and null when neither does. Either axis, because the sample is per\n * file: a service whose arch.spec.md alone was sampled is a service part of\n * whose documentation nobody read.\n *\n * PRESENCE decides it, not decodability: a scope nobody can parse still\n * means somebody stamped a partial read, so it grades as sampled. Fail\n * closed, because the alternative is that mangling one field turns a partial\n * vouch into a full one on every dashboard reading this table.\n *\n * The RUNG never changes: `vouched` is a published contract and this is a\n * qualification of it, so the maturity ladder, the rollup and `--needs-work`\n * are all untouched. Only the renderings say \"vouched (sampled)\".\n */\n vouchScope: \"sampled\" | null;\n /**\n * The spec files (`SPEC_AXES`' own names, `spec.md` / `arch.spec.md`) whose\n * BODY moved after the vouch that stamped it — `content_digest` no longer\n * matches — and empty when none did. `content.stale`'s question, carried on\n * the entry because it needs nothing but the document: the docs repo can\n * answer it for every service, where `sources.stale` needs each service's\n * own repository.\n *\n * A qualification of the rung, exactly like `vouchScope` and for its reason:\n * a person DID stamp the document, so `vouched` stands, while the renderings\n * say \"vouched (stale)\" and the worklist owes the service a re-vouch. Without\n * it every fleet readout — `list`, `status`, the scorecard — called a service\n * vouched over words nobody had read since, while `validate` beside them\n * warned that nobody had.\n */\n contentStale: string[];\n /**\n * Why no loam command can author this directory, or absent when the name is a\n * legal service id — `serviceIdProblem`'s own sentence, so the read model and\n * the refusal say the same thing.\n *\n * The enumeration still LISTS it: the directory is there, it holds somebody's\n * documents, and hiding it would repeat the symlink mistake one field up. But\n * every authoring command (`adopt`, `delta`, `rebase`, `new --touches`,\n * `init`) refuses this id with `invalid-option`, so grading it as an ordinary\n * service left the fleet gate demanding fixes no loam command can make. The\n * NAME is the finding — see `serviceIdFindings`.\n */\n idProblem?: string;\n}\n\nexport interface FeatureEntry {\n /** Feature id, derived from the directory name (FEAT-1-split -> FEAT-1). */\n id: RawFeatureId;\n /** The directory name as it is on disk, slug and all. */\n dirName: string;\n /** Absolute path to the feature directory. */\n dir: FeatureDir;\n archived: boolean;\n /** Services this feature carries a delta for, from specs/<svc>/. */\n services: RawServiceId[];\n has: { intent: boolean; delta: boolean };\n}\n\n/**\n * The directories in the `services/` tree that no loam command can address.\n *\n * A finding rather than a silent skip, and an ERROR rather than a warning,\n * because the state is not survivable in either direction: `loam adopt`,\n * `loam delta`, `loam rebase` and `loam new --touches` all refuse the id with\n * `invalid-option`, so the documents in there can never be updated by loam\n * again, while `validate --all` used to grade the directory as an ordinary\n * service and demand `model.likec4`, a spec and a landscape binding for it. The\n * fix is a rename, and it is a rename loam cannot do for you — the id is\n * spelled in the landscape binding, in the frontmatter and in every feature\n * that touched the service — so the message names all three.\n *\n * Emitted from the fleet target (`validate --all`), where the SET of service\n * directories is the thing being checked; a per-service run is already refused\n * by its own `--service` guard before it gets here. Lives beside the entry\n * shape rather than the enumeration because it is a projection of `idProblem`,\n * not a read — the entry docblock already points here.\n */\nexport function serviceIdFindings(services: ServiceEntry[]): Finding[] {\n return services\n .filter((s) => s.idProblem !== undefined)\n .map((s) => ({\n severity: \"error\" as const,\n code: \"service.id-invalid\",\n subject: s.id,\n message:\n // The path is spelled from the entry's own placement chain, not a\n // `services/<id>/` join: a FILED service with an illegal name lives at\n // services/<subsystem>/…/<id>/, and a finding naming a root directory\n // that does not exist sends the fix to the wrong place.\n `${serviceTreePath(s)}/ — ${s.idProblem} ` +\n `Every authoring command refuses this id (\\`loam adopt\\`, \\`loam delta\\`, \\`loam new --touches\\`), so nothing in that directory can be changed through loam. ` +\n `Rename the directory to a legal id, then update its \\`service:\\` frontmatter, its \\`metadata { service '${s.id}' }\\` binding in architecture/landscape.likec4, and any features/<FEAT>/specs/${s.id}/ that names it.`,\n }));\n}\n\n/**\n * Compare ids so digit runs sort numerically: FEAT-2 before FEAT-10.\n * Deterministic and locale-independent — ordering is part of the output\n * contract for `list`, and `--json` consumers diff it.\n */\nexport function compareIds(a: string, b: string): number {\n const ta = tokenize(a);\n const tb = tokenize(b);\n for (let i = 0; i < Math.max(ta.length, tb.length); i += 1) {\n const x = ta[i];\n const y = tb[i];\n if (x === undefined) return -1;\n if (y === undefined) return 1;\n if (typeof x === \"number\" && typeof y === \"number\") {\n if (x !== y) return x - y;\n } else if (String(x) !== String(y)) {\n return String(x) < String(y) ? -1 : 1;\n }\n }\n return 0;\n}\n\nfunction tokenize(s: string): (string | number)[] {\n return (s.match(/\\d+|\\D+/g) ?? []).map((t) => (/^\\d/.test(t) ? Number(t) : t));\n}\n\n/**\n * Known ids within a small edit distance of `id`, closest first, at most three.\n *\n * The did-you-mean hint for a name somebody typed: `loam new --touches` and\n * `loam delta <svc>` answer the same mistake, and each carried its own copy of\n * these twenty-four lines. It belongs here because the ids being scored are\n * this module's ids — service directory names, feature ids — and because\n * `compareIds` right above is the tiebreak that keeps the list deterministic\n * when two candidates score equally. Not in `core/kernel/ids/`: `compareIds` lives\n * here and repo.ts imports ids.js, so putting it there would close a cycle.\n *\n * `arch.ts`'s `closeIds` looks similar and is not: substring and prefix, capped\n * at five, answering \"which element could this be\" rather than \"did you\n * misspell a directory\". Two rules, two homes.\n */\nexport function nearestIds(id: string, known: string[]): string[] {\n const budget = Math.max(1, Math.floor(id.length / 4));\n return known\n .map((candidate) => ({ candidate, distance: editDistance(id.toLowerCase(), candidate.toLowerCase()) }))\n .filter((scored) => scored.distance <= budget)\n .sort((a, b) => a.distance - b.distance || compareIds(a.candidate, b.candidate))\n .slice(0, 3)\n .map((scored) => scored.candidate);\n}\n\n/** Plain Levenshtein — ids are short, and the row-at-a-time form keeps it obvious. */\nfunction editDistance(a: string, b: string): number {\n let previous = Array.from({ length: b.length + 1 }, (_, i) => i);\n for (let i = 1; i <= a.length; i += 1) {\n const current = [i];\n for (let j = 1; j <= b.length; j += 1) {\n const substitution = previous[j - 1]! + (a[i - 1] === b[j - 1] ? 0 : 1);\n current.push(Math.min(previous[j]! + 1, current[j - 1]! + 1, substitution));\n }\n previous = current;\n }\n return previous[b.length]!;\n}\n\n/**\n * Feature id from a directory name: everything up to and including the first\n * number run (`FEAT-101-payment-splitting` -> `FEAT-101`). A name with no\n * `<word>-<number>` head is its own id. Quirk: a dated slug keeps only its first\n * segment (`release-2024-01-x` -> `release-2024`) — ids are not meant to be dates.\n */\nexport function featureIdFromDirName(dirName: string): RawFeatureId {\n const m = /^(.*?-\\d+)(?:-|$)/.exec(dirName);\n // Constructed through the kernel helper, so this module holds no cast: the\n // derived id's provenance is the repository — a directory the enumeration\n // listed — which is exactly what `RawFeatureId` records.\n return rawFeatureId(m ? m[1]! : dirName);\n}\n"]}
|
package/dist/core/repo/repo.js
CHANGED
|
@@ -7,14 +7,14 @@
|
|
|
7
7
|
* a repo to walk at all in `state.ts`.
|
|
8
8
|
*/
|
|
9
9
|
import { existsSync } from "node:fs";
|
|
10
|
-
import { readdir } from "node:fs/promises";
|
|
10
|
+
import { readdir, readFile } from "node:fs/promises";
|
|
11
11
|
import { join } from "node:path";
|
|
12
|
-
import { listField, readFrontmatter, stringField } from "../document/frontmatter.js";
|
|
12
|
+
import { contentStamp, listField, movedUnderVouch, readFrontmatter, stringField, } from "../document/frontmatter.js";
|
|
13
13
|
import { featureDirOf } from "../kernel/ids/dirs.js";
|
|
14
14
|
import { rawServiceId, serviceIdProblem } from "../kernel/ids/service.js";
|
|
15
15
|
import { readVouchScope } from "../provenance/sample/scope.js";
|
|
16
16
|
import { compareIds, featureIdFromDirName } from "./entries.js";
|
|
17
|
-
import { ARCHIVE_DIR, featurePaths, featuresDir, fleetAdrsDir, isServiceArtifactName, servicePathsAt } from "./paths.js";
|
|
17
|
+
import { ARCHIVE_DIR, featurePaths, featuresDir, fleetAdrsDir, isServiceArtifactName, servicePathsAt, SPEC_AXES, } from "./paths.js";
|
|
18
18
|
import { requireDocsRepo } from "./state.js";
|
|
19
19
|
import { countMarkdown, entryIs } from "./tree/fs.js";
|
|
20
20
|
import { walkServicesTree } from "./tree/walk.js";
|
|
@@ -93,6 +93,20 @@ async function archHeader(path) {
|
|
|
93
93
|
return { present: false, malformed: false, data: {}, body: "" };
|
|
94
94
|
}
|
|
95
95
|
}
|
|
96
|
+
/**
|
|
97
|
+
* The spec files whose body moved under their vouch — `content.stale`'s
|
|
98
|
+
* question, asked through the one predicate `loam validate` grades with
|
|
99
|
+
* (`movedUnderVouch`) so the fleet listing and the finding cannot disagree
|
|
100
|
+
* about one file. Only a header that carries a stamp (`contentStamp`) costs the
|
|
101
|
+
* second read: the frontmatter read already answered whether there is anything
|
|
102
|
+
* to compare, and on a fleet mid-adoption most headers do not. The body is
|
|
103
|
+
* read exactly as the finding reads it, as UTF-8 text, because the stamp is
|
|
104
|
+
* byte-exact.
|
|
105
|
+
*/
|
|
106
|
+
async function filesMovedUnderVouch(axes) {
|
|
107
|
+
const moved = await Promise.all(axes.map(async ({ file, path, header }) => contentStamp(header) !== undefined && movedUnderVouch(header, await readFile(path, "utf8")) ? [file] : []));
|
|
108
|
+
return moved.flat();
|
|
109
|
+
}
|
|
96
110
|
export async function serviceEntries(tree) {
|
|
97
111
|
const entries = await Promise.all(tree.services.map(async (svc) => {
|
|
98
112
|
const p = servicePathsAt(svc.dir);
|
|
@@ -103,6 +117,7 @@ export async function serviceEntries(tree) {
|
|
|
103
117
|
// sampled. `readFrontmatter` answers "absent" for a file that is not
|
|
104
118
|
// there, so a service without the second axis pays one existsSync.
|
|
105
119
|
const [fm, archFm] = await Promise.all([readFrontmatter(p.spec), archHeader(p.archSpec)]);
|
|
120
|
+
const headers = { spec: fm, archSpec: archFm };
|
|
106
121
|
const idProblem = serviceIdProblem(svc.id, "directory name");
|
|
107
122
|
return {
|
|
108
123
|
id: svc.id,
|
|
@@ -129,6 +144,10 @@ export async function serviceEntries(tree) {
|
|
|
129
144
|
// listing is the last place a mangled field should be able to buy
|
|
130
145
|
// itself a full-trust row.
|
|
131
146
|
vouchScope: [fm, archFm].some((header) => readVouchScope(header).kind !== "none") ? "sampled" : null,
|
|
147
|
+
// Both axes again, for the scope's reason: a stamp is per FILE, and an
|
|
148
|
+
// arch.spec.md edited under its vouch is a service part of whose
|
|
149
|
+
// documentation nobody has re-read.
|
|
150
|
+
contentStale: await filesMovedUnderVouch(SPEC_AXES.map((axis) => ({ file: axis.file, path: p[axis.key], header: headers[axis.key] }))),
|
|
132
151
|
};
|
|
133
152
|
}));
|
|
134
153
|
return entries.sort((a, b) => compareIds(a.id, b.id) || compareIds(a.dir, b.dir));
|
|
@@ -201,7 +220,8 @@ export async function listFeatures(docsDir, opts = {}, context) {
|
|
|
201
220
|
* directory over `FEAT-5-slug`, whose id is also `FEAT-5`), and under "include"
|
|
202
221
|
* an active feature wins over an archived one. Ties among equally-exact
|
|
203
222
|
* candidates go to the first by name — deterministic, unlike the raw readdir
|
|
204
|
-
* order this replaces; `
|
|
223
|
+
* order this replaces; `featureCandidates` returns every one of them, for a
|
|
224
|
+
* caller that must see the tie (`loam status` names the colliding directories).
|
|
205
225
|
*/
|
|
206
226
|
export async function resolveFeature(docsDir, arg, archived, context) {
|
|
207
227
|
return (await featureCandidates(docsDir, arg, archived, context))[0] ?? null;
|
|
@@ -220,16 +240,6 @@ export async function featureCandidates(docsDir, arg, archived, context) {
|
|
|
220
240
|
Number(a.archived) - Number(b.archived) ||
|
|
221
241
|
compareIds(a.dirName, b.dirName));
|
|
222
242
|
}
|
|
223
|
-
/**
|
|
224
|
-
* The message for an argument that names more than one feature — two directories
|
|
225
|
-
* carrying the same id (`FEAT-6-aaa` and `FEAT-6-zzz`). `resolveFeature` still
|
|
226
|
-
* picks one deterministically, but a command that is about to WRITE should say
|
|
227
|
-
* which directories collided and make the caller spell one out.
|
|
228
|
-
*/
|
|
229
|
-
export function ambiguousFeatureMessage(arg, candidates) {
|
|
230
|
-
return (`'${arg}' names ${candidates.length} features: ${candidates.map((c) => c.dirName).join(", ")}. ` +
|
|
231
|
-
"Pass the directory name you mean.");
|
|
232
|
-
}
|
|
233
243
|
/**
|
|
234
244
|
* The message for an exclude-mode miss. When the feature exists under
|
|
235
245
|
* `features/archive/`, "no feature" is a lie the caller would have to debug —
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"repo.js","sourceRoot":"","sources":["../../../src/core/repo/repo.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Read model of the docs repo — enumeration, and only by the filesystem.\n *\n * Files are the source of truth, so `services/` IS the list of services and\n * there is no manifest that can disagree with it. The shapes this hands back\n * live in `entries.ts`, the layout it walks in `paths.ts`, and whether there is\n * a repo to walk at all in `state.ts`.\n */\nimport { existsSync } from \"node:fs\";\nimport { readdir } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport { listField, readFrontmatter, stringField, type Frontmatter } from \"../document/frontmatter.js\";\nimport type { FleetContext } from \"../fleet-context.js\";\nimport { featureDirOf, type DocsDir, type FeatureDir } from \"../kernel/ids/dirs.js\";\nimport { rawServiceId, serviceIdProblem, type RawServiceId } from \"../kernel/ids/service.js\";\nimport { readVouchScope } from \"../provenance/sample/scope.js\";\nimport { compareIds, featureIdFromDirName, type FeatureEntry, type ServiceEntry } from \"./entries.js\";\nimport { ARCHIVE_DIR, featurePaths, featuresDir, fleetAdrsDir, isServiceArtifactName, servicePathsAt } from \"./paths.js\";\nimport { requireDocsRepo } from \"./state.js\";\nimport { countMarkdown, entryIs } from \"./tree/fs.js\";\nimport { walkServicesTree, type FleetTree } from \"./tree/walk.js\";\n\n/* ------------------------------------------------------------------ */\n/* Enumeration */\n/* ------------------------------------------------------------------ */\n\n/**\n * Sorted names of the real subdirectories of `dir` (dot-dirs and files skipped).\n * A missing `dir` is an empty list — the swallow is deliberate but narrow: it\n * only ever runs INSIDE a docs repo that `requireDocsRepo` has already\n * confirmed, where an absent `features/` or `specs/` means \"none yet\" and\n * nothing else.\n */\nasync function subdirs(dir: string): Promise<string[]> {\n if (!existsSync(dir)) return [];\n const entries = await readdir(dir, { withFileTypes: true });\n return entries\n .filter((e) => !e.name.startsWith(\".\") && entryIs(dir, e, \"dir\"))\n .map((e) => e.name)\n .sort(compareIds);\n}\n\n/** Services a feature carries a delta for, ordered — the subdirs of its specs/. */\nexport async function featureSpecServices(\n featureDir: FeatureDir,\n context?: FleetContext,\n): Promise<RawServiceId[]> {\n if (context !== undefined) return context.featureSpecServices(featureDir);\n return (await subdirs(featurePaths(featureDir).specsDir)).map(rawServiceId);\n}\n\n/**\n * The classified `services/` tree — services at any depth, the subsystems\n * grouping them, and the walk's own findings (`subsystem.*`). The walk itself\n * lives in `./tree/walk.ts` and never imports back into this package; this\n * function is where its two injected dependencies are built — the docs-repo\n * precondition and the artifact-name table `paths.ts` owns.\n */\nexport async function listFleetTree(docsDir: DocsDir, context?: FleetContext): Promise<FleetTree> {\n if (context !== undefined) return context.fleetTree(docsDir);\n requireDocsRepo(docsDir, [\"ok\"]);\n return walkServicesTree({ docsDir, isServiceArtifact: isServiceArtifactName });\n}\n\n/** Every service in the docs repo, at any depth of the tree, ordered by id. */\nexport async function listServices(docsDir: DocsDir, context?: FleetContext): Promise<ServiceEntry[]> {\n if (context !== undefined) return context.listServices(docsDir);\n return serviceEntries(await listFleetTree(docsDir));\n}\n\n/**\n * The full entries for a walked tree — frontmatter, artifact presence, ADR\n * count. Split from `listServices` so `FleetContext` can derive its services\n * memo from its tree memo: one walk per invocation answers both questions.\n * Ordering is `compareIds` over the id wherever the service sits — placement\n * is never part of any identity, and `--json` consumers diff this order — with\n * the directory as the tiebreak so a duplicated id (its own error finding)\n * still lists deterministically.\n */\n/**\n * The second spec axis's header, or an empty one when nobody could read it.\n *\n * `readFrontmatter` folds a decode failure into a flag but still THROWS when\n * the path exists and cannot be read as a file at all — a directory named\n * `arch.spec.md`, which test/gate-command.test.ts builds on purpose. This is\n * the fleet ENUMERATION, where one service's bad byte must cost one subject\n * and not the listing of the other 119, so the throw stops here.\n *\n * The distinction between \"no scope\" and \"nobody could look\" is not lost by\n * swallowing it: a path that exists and cannot be read is `service.unreadable`\n * — an ERROR that fails `loam validate` and `loam gate` — which is a louder\n * signal than any row of a listing, and it names the file this cannot.\n */\nasync function archHeader(path: string): Promise<Frontmatter> {\n try {\n return await readFrontmatter(path);\n } catch {\n return { present: false, malformed: false, data: {}, body: \"\" };\n }\n}\n\nexport async function serviceEntries(tree: FleetTree): Promise<ServiceEntry[]> {\n const entries = await Promise.all(\n tree.services.map(async (svc): Promise<ServiceEntry> => {\n const p = servicePathsAt(svc.dir);\n // Both spec axes, because a sample is per FILE: one `--sample 3` run can\n // read a short spec.md in full and stamp a scope on a long arch.spec.md\n // beside it, and a fleet row that reported only spec.md's header would\n // print that service as fully vouched while `loam validate` reported it\n // sampled. `readFrontmatter` answers \"absent\" for a file that is not\n // there, so a service without the second axis pays one existsSync.\n const [fm, archFm] = await Promise.all([readFrontmatter(p.spec), archHeader(p.archSpec)]);\n const idProblem = serviceIdProblem(svc.id, \"directory name\");\n return {\n id: svc.id,\n dir: svc.dir,\n subsystem: svc.subsystem,\n ...(idProblem === null ? {} : { idProblem }),\n has: {\n model: existsSync(p.model),\n spec: existsSync(p.spec),\n openapi: existsSync(p.openapi),\n asyncapi: existsSync(p.asyncapi),\n runbook: existsSync(p.runbook),\n health: existsSync(p.health),\n },\n adrs: await countMarkdown(p.adrsDir),\n status: stringField(fm, \"status\") ?? null,\n sources: {\n declared: listField(fm, \"sources\").length > 0,\n stamped: stringField(fm, \"sources_digest\") !== undefined,\n },\n // Either axis makes the SERVICE sampled, and presence is the test, not\n // decodability: an unreadable scope is still a stamped partial read\n // (core/provenance/sample/scope.ts's `readVouchScope`), and the fleet\n // listing is the last place a mangled field should be able to buy\n // itself a full-trust row.\n vouchScope: [fm, archFm].some((header) => readVouchScope(header).kind !== \"none\") ? \"sampled\" : null,\n };\n }),\n );\n return entries.sort((a, b) => compareIds(a.id, b.id) || compareIds(a.dir, b.dir));\n}\n\n/**\n * How many fleet-level ADRs `architecture/adrs/` holds — the same number, by the\n * same rule (`countMarkdown`), that `ServiceEntry.adrs` carries one altitude\n * down. One shape for one question: a second spelling of \"count the decisions\"\n * is how `list` and `show` once reported different ADR counts for one service.\n *\n * A count and never a finding. The directory is optional, an absent one is 0,\n * and nothing in loam grades either — see `fleetAdrsDir` for why that is the\n * whole design rather than an unfinished half of it. No `FleetContext` overload:\n * one caller asks this once per invocation, and a memo whose key is the docsDir\n * would be a cache nobody reads twice.\n */\nexport async function fleetAdrCount(docsDir: DocsDir): Promise<number> {\n return countMarkdown(fleetAdrsDir(docsDir));\n}\n\nasync function readFeature(dir: FeatureDir, dirName: string, archived: boolean): Promise<FeatureEntry> {\n const p = featurePaths(dir);\n return {\n id: featureIdFromDirName(dirName),\n dirName,\n dir,\n archived,\n services: (await subdirs(p.specsDir)).map(rawServiceId),\n has: { intent: existsSync(p.intent), delta: existsSync(p.delta) },\n };\n}\n\n/**\n * Features in the docs repo, ordered by id (active and archived interleaved when\n * both are asked for). `features/archive/` is the archive, never a feature.\n */\nexport async function listFeatures(\n docsDir: DocsDir,\n opts: { includeArchived?: boolean } = {},\n context?: FleetContext,\n): Promise<FeatureEntry[]> {\n if (context !== undefined) return context.listFeatures(docsDir, opts);\n // \"no-services\" is tolerated here on purpose: a docs repo whose services/ is\n // gone is broken, but it is `listServices` (and `loam doctor`) that says so.\n // Refusing here too would turn one diagnosis into two contradictory ones.\n requireDocsRepo(docsDir, [\"ok\", \"no-services\"]);\n const root = featuresDir(docsDir);\n const active = (await subdirs(root)).filter((n) => n !== ARCHIVE_DIR);\n // Branded at the join: the name came off this readdir, onto the features\n // root it was listed under — the provenance `FeatureDir` records.\n const out = await Promise.all(active.map((n) => readFeature(featureDirOf(join(root, n)), n, false)));\n\n if (opts.includeArchived) {\n const archiveRoot = join(root, ARCHIVE_DIR);\n const archived = await subdirs(archiveRoot);\n out.push(...(await Promise.all(archived.map((n) => readFeature(featureDirOf(join(archiveRoot, n)), n, true)))));\n }\n\n return out.sort((a, b) => compareIds(a.id, b.id) || compareIds(a.dirName, b.dirName));\n}\n\n/**\n * Which side of `features/archive/` a lookup may land on. The policy is an\n * argument, not a default, so every command's stance on shipped features is\n * visible at its call site: delta/validate/archive work on in-flight features\n * (\"exclude\"), show/verify/new read shipped ones too (\"include\"), unarchive\n * takes back nothing else (\"only\").\n */\nexport type ArchivedPolicy = \"exclude\" | \"include\" | \"only\";\n\n/**\n * Resolve a feature argument — a canonical id (`FEAT-5`) or an exact directory\n * name (`FEAT-5-slug`) — to its entry. The entry's `id` is the canonical\n * spelling, and it is the only name a caller may use from here on: tag filters,\n * coherence, and the cross-feature self-exclusion scans all compare against\n * `feature.id`, never against the raw argument.\n *\n * Matching is EXACT, on one of two spellings: the directory name as it is on\n * disk, or the canonical id derived from it. It used to also match any dirName\n * starting with `arg + \"-\"`, which made the argument a prefix query in\n * disguise: with FEAT-401 and FEAT-402 in flight, `loam archive FEAT` archived\n * whichever sorted first, and `loam delta billing` reached into\n * `billing-7-rewrite`. A destructive command must never pick a target the\n * caller did not name, so a prefix now resolves to nothing at all.\n *\n * An exact directory name still wins over the id spelling (`FEAT-5` the\n * directory over `FEAT-5-slug`, whose id is also `FEAT-5`), and under \"include\"\n * an active feature wins over an archived one. Ties among equally-exact\n * candidates go to the first by name — deterministic, unlike the raw readdir\n * order this replaces; `ambiguousFeatureMessage` lists them for the caller.\n */\nexport async function resolveFeature(\n docsDir: DocsDir,\n arg: string,\n archived: ArchivedPolicy,\n context?: FleetContext,\n): Promise<FeatureEntry | null> {\n return (await featureCandidates(docsDir, arg, archived, context))[0] ?? null;\n}\n\n/**\n * Every feature the argument names exactly, best first. Split out of\n * `resolveFeature` so a caller that wants to REFUSE an ambiguous argument can\n * see the tie instead of inheriting the winner.\n */\nexport async function featureCandidates(\n docsDir: DocsDir,\n arg: string,\n archived: ArchivedPolicy,\n context?: FleetContext,\n): Promise<FeatureEntry[]> {\n const all = await listFeatures(docsDir, { includeArchived: archived !== \"exclude\" }, context);\n return all\n .filter((f) => archived === \"include\" || f.archived === (archived === \"only\"))\n .filter((f) => f.dirName === arg || f.id === arg)\n .sort(\n (a, b) =>\n Number(b.dirName === arg) - Number(a.dirName === arg) ||\n Number(a.archived) - Number(b.archived) ||\n compareIds(a.dirName, b.dirName),\n );\n}\n\n/**\n * The message for an argument that names more than one feature — two directories\n * carrying the same id (`FEAT-6-aaa` and `FEAT-6-zzz`). `resolveFeature` still\n * picks one deterministically, but a command that is about to WRITE should say\n * which directories collided and make the caller spell one out.\n */\nexport function ambiguousFeatureMessage(arg: string, candidates: FeatureEntry[]): string {\n return (\n `'${arg}' names ${candidates.length} features: ${candidates.map((c) => c.dirName).join(\", \")}. ` +\n \"Pass the directory name you mean.\"\n );\n}\n\n/**\n * The message for an exclude-mode miss. When the feature exists under\n * `features/archive/`, \"no feature\" is a lie the caller would have to debug —\n * the honest answer is \"already archived\", plus how to look at it. The `--json`\n * code stays `unknown-target` either way: the target is unknown to the command\n * that asked, and the prose carries the diagnosis.\n */\nexport async function missingFeatureMessage(\n docsDir: DocsDir,\n arg: string,\n context?: FleetContext,\n): Promise<string> {\n const shipped = await resolveFeature(docsDir, arg, \"only\", context);\n if (shipped !== null) {\n return (\n `Feature '${shipped.id}' is already archived (features/archive/${shipped.dirName}) — ` +\n `\\`loam show ${shipped.id}\\` reads it, \\`loam unarchive ${shipped.id}\\` re-opens it.`\n );\n }\n return `No feature '${arg}' under ${featuresDir(docsDir)}.`;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"repo.js","sourceRoot":"","sources":["../../../src/core/repo/repo.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Read model of the docs repo — enumeration, and only by the filesystem.\n *\n * Files are the source of truth, so `services/` IS the list of services and\n * there is no manifest that can disagree with it. The shapes this hands back\n * live in `entries.ts`, the layout it walks in `paths.ts`, and whether there is\n * a repo to walk at all in `state.ts`.\n */\nimport { existsSync } from \"node:fs\";\nimport { readdir, readFile } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport {\n contentStamp,\n listField,\n movedUnderVouch,\n readFrontmatter,\n stringField,\n type Frontmatter,\n} from \"../document/frontmatter.js\";\nimport type { FleetContext } from \"../fleet-context.js\";\nimport { featureDirOf, type DocsDir, type FeatureDir } from \"../kernel/ids/dirs.js\";\nimport { rawServiceId, serviceIdProblem, type RawServiceId } from \"../kernel/ids/service.js\";\nimport { readVouchScope } from \"../provenance/sample/scope.js\";\nimport { compareIds, featureIdFromDirName, type FeatureEntry, type ServiceEntry } from \"./entries.js\";\nimport {\n ARCHIVE_DIR,\n featurePaths,\n featuresDir,\n fleetAdrsDir,\n isServiceArtifactName,\n servicePathsAt,\n SPEC_AXES,\n} from \"./paths.js\";\nimport { requireDocsRepo } from \"./state.js\";\nimport { countMarkdown, entryIs } from \"./tree/fs.js\";\nimport { walkServicesTree, type FleetTree } from \"./tree/walk.js\";\n\n/* ------------------------------------------------------------------ */\n/* Enumeration */\n/* ------------------------------------------------------------------ */\n\n/**\n * Sorted names of the real subdirectories of `dir` (dot-dirs and files skipped).\n * A missing `dir` is an empty list — the swallow is deliberate but narrow: it\n * only ever runs INSIDE a docs repo that `requireDocsRepo` has already\n * confirmed, where an absent `features/` or `specs/` means \"none yet\" and\n * nothing else.\n */\nasync function subdirs(dir: string): Promise<string[]> {\n if (!existsSync(dir)) return [];\n const entries = await readdir(dir, { withFileTypes: true });\n return entries\n .filter((e) => !e.name.startsWith(\".\") && entryIs(dir, e, \"dir\"))\n .map((e) => e.name)\n .sort(compareIds);\n}\n\n/** Services a feature carries a delta for, ordered — the subdirs of its specs/. */\nexport async function featureSpecServices(\n featureDir: FeatureDir,\n context?: FleetContext,\n): Promise<RawServiceId[]> {\n if (context !== undefined) return context.featureSpecServices(featureDir);\n return (await subdirs(featurePaths(featureDir).specsDir)).map(rawServiceId);\n}\n\n/**\n * The classified `services/` tree — services at any depth, the subsystems\n * grouping them, and the walk's own findings (`subsystem.*`). The walk itself\n * lives in `./tree/walk.ts` and never imports back into this package; this\n * function is where its two injected dependencies are built — the docs-repo\n * precondition and the artifact-name table `paths.ts` owns.\n */\nexport async function listFleetTree(docsDir: DocsDir, context?: FleetContext): Promise<FleetTree> {\n if (context !== undefined) return context.fleetTree(docsDir);\n requireDocsRepo(docsDir, [\"ok\"]);\n return walkServicesTree({ docsDir, isServiceArtifact: isServiceArtifactName });\n}\n\n/** Every service in the docs repo, at any depth of the tree, ordered by id. */\nexport async function listServices(docsDir: DocsDir, context?: FleetContext): Promise<ServiceEntry[]> {\n if (context !== undefined) return context.listServices(docsDir);\n return serviceEntries(await listFleetTree(docsDir));\n}\n\n/**\n * The full entries for a walked tree — frontmatter, artifact presence, ADR\n * count. Split from `listServices` so `FleetContext` can derive its services\n * memo from its tree memo: one walk per invocation answers both questions.\n * Ordering is `compareIds` over the id wherever the service sits — placement\n * is never part of any identity, and `--json` consumers diff this order — with\n * the directory as the tiebreak so a duplicated id (its own error finding)\n * still lists deterministically.\n */\n/**\n * The second spec axis's header, or an empty one when nobody could read it.\n *\n * `readFrontmatter` folds a decode failure into a flag but still THROWS when\n * the path exists and cannot be read as a file at all — a directory named\n * `arch.spec.md`, which test/gate-command.test.ts builds on purpose. This is\n * the fleet ENUMERATION, where one service's bad byte must cost one subject\n * and not the listing of the other 119, so the throw stops here.\n *\n * The distinction between \"no scope\" and \"nobody could look\" is not lost by\n * swallowing it: a path that exists and cannot be read is `service.unreadable`\n * — an ERROR that fails `loam validate` and `loam gate` — which is a louder\n * signal than any row of a listing, and it names the file this cannot.\n */\nasync function archHeader(path: string): Promise<Frontmatter> {\n try {\n return await readFrontmatter(path);\n } catch {\n return { present: false, malformed: false, data: {}, body: \"\" };\n }\n}\n\n/**\n * The spec files whose body moved under their vouch — `content.stale`'s\n * question, asked through the one predicate `loam validate` grades with\n * (`movedUnderVouch`) so the fleet listing and the finding cannot disagree\n * about one file. Only a header that carries a stamp (`contentStamp`) costs the\n * second read: the frontmatter read already answered whether there is anything\n * to compare, and on a fleet mid-adoption most headers do not. The body is\n * read exactly as the finding reads it, as UTF-8 text, because the stamp is\n * byte-exact.\n */\nasync function filesMovedUnderVouch(\n axes: readonly { file: string; path: string; header: Frontmatter }[],\n): Promise<string[]> {\n const moved = await Promise.all(\n axes.map(async ({ file, path, header }) =>\n contentStamp(header) !== undefined && movedUnderVouch(header, await readFile(path, \"utf8\")) ? [file] : [],\n ),\n );\n return moved.flat();\n}\n\nexport async function serviceEntries(tree: FleetTree): Promise<ServiceEntry[]> {\n const entries = await Promise.all(\n tree.services.map(async (svc): Promise<ServiceEntry> => {\n const p = servicePathsAt(svc.dir);\n // Both spec axes, because a sample is per FILE: one `--sample 3` run can\n // read a short spec.md in full and stamp a scope on a long arch.spec.md\n // beside it, and a fleet row that reported only spec.md's header would\n // print that service as fully vouched while `loam validate` reported it\n // sampled. `readFrontmatter` answers \"absent\" for a file that is not\n // there, so a service without the second axis pays one existsSync.\n const [fm, archFm] = await Promise.all([readFrontmatter(p.spec), archHeader(p.archSpec)]);\n const headers = { spec: fm, archSpec: archFm };\n const idProblem = serviceIdProblem(svc.id, \"directory name\");\n return {\n id: svc.id,\n dir: svc.dir,\n subsystem: svc.subsystem,\n ...(idProblem === null ? {} : { idProblem }),\n has: {\n model: existsSync(p.model),\n spec: existsSync(p.spec),\n openapi: existsSync(p.openapi),\n asyncapi: existsSync(p.asyncapi),\n runbook: existsSync(p.runbook),\n health: existsSync(p.health),\n },\n adrs: await countMarkdown(p.adrsDir),\n status: stringField(fm, \"status\") ?? null,\n sources: {\n declared: listField(fm, \"sources\").length > 0,\n stamped: stringField(fm, \"sources_digest\") !== undefined,\n },\n // Either axis makes the SERVICE sampled, and presence is the test, not\n // decodability: an unreadable scope is still a stamped partial read\n // (core/provenance/sample/scope.ts's `readVouchScope`), and the fleet\n // listing is the last place a mangled field should be able to buy\n // itself a full-trust row.\n vouchScope: [fm, archFm].some((header) => readVouchScope(header).kind !== \"none\") ? \"sampled\" : null,\n // Both axes again, for the scope's reason: a stamp is per FILE, and an\n // arch.spec.md edited under its vouch is a service part of whose\n // documentation nobody has re-read.\n contentStale: await filesMovedUnderVouch(\n SPEC_AXES.map((axis) => ({ file: axis.file, path: p[axis.key], header: headers[axis.key] })),\n ),\n };\n }),\n );\n return entries.sort((a, b) => compareIds(a.id, b.id) || compareIds(a.dir, b.dir));\n}\n\n/**\n * How many fleet-level ADRs `architecture/adrs/` holds — the same number, by the\n * same rule (`countMarkdown`), that `ServiceEntry.adrs` carries one altitude\n * down. One shape for one question: a second spelling of \"count the decisions\"\n * is how `list` and `show` once reported different ADR counts for one service.\n *\n * A count and never a finding. The directory is optional, an absent one is 0,\n * and nothing in loam grades either — see `fleetAdrsDir` for why that is the\n * whole design rather than an unfinished half of it. No `FleetContext` overload:\n * one caller asks this once per invocation, and a memo whose key is the docsDir\n * would be a cache nobody reads twice.\n */\nexport async function fleetAdrCount(docsDir: DocsDir): Promise<number> {\n return countMarkdown(fleetAdrsDir(docsDir));\n}\n\nasync function readFeature(dir: FeatureDir, dirName: string, archived: boolean): Promise<FeatureEntry> {\n const p = featurePaths(dir);\n return {\n id: featureIdFromDirName(dirName),\n dirName,\n dir,\n archived,\n services: (await subdirs(p.specsDir)).map(rawServiceId),\n has: { intent: existsSync(p.intent), delta: existsSync(p.delta) },\n };\n}\n\n/**\n * Features in the docs repo, ordered by id (active and archived interleaved when\n * both are asked for). `features/archive/` is the archive, never a feature.\n */\nexport async function listFeatures(\n docsDir: DocsDir,\n opts: { includeArchived?: boolean } = {},\n context?: FleetContext,\n): Promise<FeatureEntry[]> {\n if (context !== undefined) return context.listFeatures(docsDir, opts);\n // \"no-services\" is tolerated here on purpose: a docs repo whose services/ is\n // gone is broken, but it is `listServices` (and `loam doctor`) that says so.\n // Refusing here too would turn one diagnosis into two contradictory ones.\n requireDocsRepo(docsDir, [\"ok\", \"no-services\"]);\n const root = featuresDir(docsDir);\n const active = (await subdirs(root)).filter((n) => n !== ARCHIVE_DIR);\n // Branded at the join: the name came off this readdir, onto the features\n // root it was listed under — the provenance `FeatureDir` records.\n const out = await Promise.all(active.map((n) => readFeature(featureDirOf(join(root, n)), n, false)));\n\n if (opts.includeArchived) {\n const archiveRoot = join(root, ARCHIVE_DIR);\n const archived = await subdirs(archiveRoot);\n out.push(...(await Promise.all(archived.map((n) => readFeature(featureDirOf(join(archiveRoot, n)), n, true)))));\n }\n\n return out.sort((a, b) => compareIds(a.id, b.id) || compareIds(a.dirName, b.dirName));\n}\n\n/**\n * Which side of `features/archive/` a lookup may land on. The policy is an\n * argument, not a default, so every command's stance on shipped features is\n * visible at its call site: delta/validate/archive work on in-flight features\n * (\"exclude\"), show/verify/new read shipped ones too (\"include\"), unarchive\n * takes back nothing else (\"only\").\n */\nexport type ArchivedPolicy = \"exclude\" | \"include\" | \"only\";\n\n/**\n * Resolve a feature argument — a canonical id (`FEAT-5`) or an exact directory\n * name (`FEAT-5-slug`) — to its entry. The entry's `id` is the canonical\n * spelling, and it is the only name a caller may use from here on: tag filters,\n * coherence, and the cross-feature self-exclusion scans all compare against\n * `feature.id`, never against the raw argument.\n *\n * Matching is EXACT, on one of two spellings: the directory name as it is on\n * disk, or the canonical id derived from it. It used to also match any dirName\n * starting with `arg + \"-\"`, which made the argument a prefix query in\n * disguise: with FEAT-401 and FEAT-402 in flight, `loam archive FEAT` archived\n * whichever sorted first, and `loam delta billing` reached into\n * `billing-7-rewrite`. A destructive command must never pick a target the\n * caller did not name, so a prefix now resolves to nothing at all.\n *\n * An exact directory name still wins over the id spelling (`FEAT-5` the\n * directory over `FEAT-5-slug`, whose id is also `FEAT-5`), and under \"include\"\n * an active feature wins over an archived one. Ties among equally-exact\n * candidates go to the first by name — deterministic, unlike the raw readdir\n * order this replaces; `featureCandidates` returns every one of them, for a\n * caller that must see the tie (`loam status` names the colliding directories).\n */\nexport async function resolveFeature(\n docsDir: DocsDir,\n arg: string,\n archived: ArchivedPolicy,\n context?: FleetContext,\n): Promise<FeatureEntry | null> {\n return (await featureCandidates(docsDir, arg, archived, context))[0] ?? null;\n}\n\n/**\n * Every feature the argument names exactly, best first. Split out of\n * `resolveFeature` so a caller that wants to REFUSE an ambiguous argument can\n * see the tie instead of inheriting the winner.\n */\nexport async function featureCandidates(\n docsDir: DocsDir,\n arg: string,\n archived: ArchivedPolicy,\n context?: FleetContext,\n): Promise<FeatureEntry[]> {\n const all = await listFeatures(docsDir, { includeArchived: archived !== \"exclude\" }, context);\n return all\n .filter((f) => archived === \"include\" || f.archived === (archived === \"only\"))\n .filter((f) => f.dirName === arg || f.id === arg)\n .sort(\n (a, b) =>\n Number(b.dirName === arg) - Number(a.dirName === arg) ||\n Number(a.archived) - Number(b.archived) ||\n compareIds(a.dirName, b.dirName),\n );\n}\n\n/**\n * The message for an exclude-mode miss. When the feature exists under\n * `features/archive/`, \"no feature\" is a lie the caller would have to debug —\n * the honest answer is \"already archived\", plus how to look at it. The `--json`\n * code stays `unknown-target` either way: the target is unknown to the command\n * that asked, and the prose carries the diagnosis.\n */\nexport async function missingFeatureMessage(\n docsDir: DocsDir,\n arg: string,\n context?: FleetContext,\n): Promise<string> {\n const shipped = await resolveFeature(docsDir, arg, \"only\", context);\n if (shipped !== null) {\n return (\n `Feature '${shipped.id}' is already archived (features/archive/${shipped.dirName}) — ` +\n `\\`loam show ${shipped.id}\\` reads it, \\`loam unarchive ${shipped.id}\\` re-opens it.`\n );\n }\n return `No feature '${arg}' under ${featuresDir(docsDir)}.`;\n}\n"]}
|
|
@@ -26,9 +26,18 @@ export function findInTree(tree, name) {
|
|
|
26
26
|
export function treeNames(tree) {
|
|
27
27
|
return [...tree.services.map((s) => s.id), ...tree.subsystems.map((s) => s.name)];
|
|
28
28
|
}
|
|
29
|
-
/**
|
|
30
|
-
|
|
31
|
-
|
|
29
|
+
/**
|
|
30
|
+
* The near-miss tail a refusal appends to its "no such name" sentence, over
|
|
31
|
+
* whichever pool the verb resolves against: the close names when there are
|
|
32
|
+
* any, else `otherwise` — the verb's own pointer at what exists, or "" for
|
|
33
|
+
* none. Eight refusals across `subsystem`, `adopt --subsystem` and
|
|
34
|
+
* `list --subsystem` end this way, and each spelled the conditional itself;
|
|
35
|
+
* the lead sentence and the fallback stay theirs, because those are what
|
|
36
|
+
* differ.
|
|
37
|
+
*/
|
|
38
|
+
export function closeNamesHint(name, pool, otherwise) {
|
|
39
|
+
const close = closeIds(name, pool);
|
|
40
|
+
return close.length > 0 ? ` Close names: ${close.join(", ")}.` : otherwise;
|
|
32
41
|
}
|
|
33
42
|
/**
|
|
34
43
|
* Does the subsystem at `subPath` contain `path` — itself, or anywhere beneath
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"find.js","sourceRoot":"","sources":["../../../../src/core/repo/tree/find.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAS5C;;;;;GAKG;AACH,MAAM,UAAU,UAAU,CAAC,IAAe,EAAE,IAAY;IACtD,MAAM,OAAO,GAAG,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,IAAI,CAAC,CAAC;IACzD,IAAI,OAAO,KAAK,SAAS;QAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,CAAC;IAC/D,MAAM,SAAS,GAAG,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,IAAI,CAAC,CAAC;IAC/D,IAAI,SAAS,KAAK,SAAS;QAAE,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,SAAS,EAAE,CAAC;IACrE,OAAO,IAAI,CAAC;AACd,CAAC;AAED,sFAAsF;AACtF,MAAM,UAAU,SAAS,CAAC,IAAe;IACvC,OAAO,CAAC,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,GAAG,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC;AACpF,CAAC;AAED
|
|
1
|
+
{"version":3,"file":"find.js","sourceRoot":"","sources":["../../../../src/core/repo/tree/find.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAS5C;;;;;GAKG;AACH,MAAM,UAAU,UAAU,CAAC,IAAe,EAAE,IAAY;IACtD,MAAM,OAAO,GAAG,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,IAAI,CAAC,CAAC;IACzD,IAAI,OAAO,KAAK,SAAS;QAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,CAAC;IAC/D,MAAM,SAAS,GAAG,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,IAAI,CAAC,CAAC;IAC/D,IAAI,SAAS,KAAK,SAAS;QAAE,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,SAAS,EAAE,CAAC;IACrE,OAAO,IAAI,CAAC;AACd,CAAC;AAED,sFAAsF;AACtF,MAAM,UAAU,SAAS,CAAC,IAAe;IACvC,OAAO,CAAC,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,GAAG,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC;AACpF,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,cAAc,CAAC,IAAY,EAAE,IAAc,EAAE,SAAiB;IAC5E,MAAM,KAAK,GAAG,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;IACnC,OAAO,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,iBAAiB,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,SAAS,CAAC;AAC7E,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,eAAe,CAAC,OAAiB,EAAE,IAAc;IAC/D,OAAO,OAAO,CAAC,MAAM,IAAI,IAAI,CAAC,MAAM,IAAI,OAAO,CAAC,KAAK,CAAC,CAAC,IAAI,EAAE,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC;AACvF,CAAC;AAED,oGAAoG;AACpG,MAAM,UAAU,aAAa,CAAC,IAAe,EAAE,GAAmB;IAChE,OAAO,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,IAAI,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,CAAC;AAC3F,CAAC;AAED,4EAA4E;AAC5E,MAAM,UAAU,eAAe,CAAC,IAAe,EAAE,GAAmB;IAClE,OAAO,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,GAAG,IAAI,eAAe,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC;AACvF,CAAC","sourcesContent":["/**\n * Name resolution over a walked tree — the questions every `loam subsystem`\n * verb asks before it acts: what does this name denote, what sits beneath a\n * subsystem, and which names come close to a miss. One module so `move`,\n * `rm`, `new` and `history` cannot grow four private spellings of \"the flat\n * namespace\" — the walk already enforces one namespace, and resolution must\n * read it the same way.\n */\nimport { closeIds } from \"../../c4/arch.js\";\nimport type { FleetTree, SubsystemEntry, WalkedService } from \"./walk.js\";\n\n/** What a name denotes in the tree — a service, a subsystem, or nothing. */\nexport type TreeHit =\n | { kind: \"service\"; service: WalkedService }\n | { kind: \"subsystem\"; subsystem: SubsystemEntry }\n | null;\n\n/**\n * Resolve one name against the flat namespace. Services win a tie on\n * purpose: a tie IS `subsystem.name-collision`, already an error finding,\n * and while it stands the service reading keeps every id-addressed command\n * coherent.\n */\nexport function findInTree(tree: FleetTree, name: string): TreeHit {\n const service = tree.services.find((s) => s.id === name);\n if (service !== undefined) return { kind: \"service\", service };\n const subsystem = tree.subsystems.find((s) => s.name === name);\n if (subsystem !== undefined) return { kind: \"subsystem\", subsystem };\n return null;\n}\n\n/** Every name the flat namespace holds — service ids and subsystem names together. */\nexport function treeNames(tree: FleetTree): string[] {\n return [...tree.services.map((s) => s.id), ...tree.subsystems.map((s) => s.name)];\n}\n\n/**\n * The near-miss tail a refusal appends to its \"no such name\" sentence, over\n * whichever pool the verb resolves against: the close names when there are\n * any, else `otherwise` — the verb's own pointer at what exists, or \"\" for\n * none. Eight refusals across `subsystem`, `adopt --subsystem` and\n * `list --subsystem` end this way, and each spelled the conditional itself;\n * the lead sentence and the fallback stay theirs, because those are what\n * differ.\n */\nexport function closeNamesHint(name: string, pool: string[], otherwise: string): string {\n const close = closeIds(name, pool);\n return close.length > 0 ? ` Close names: ${close.join(\", \")}.` : otherwise;\n}\n\n/**\n * Does the subsystem at `subPath` contain `path` — itself, or anywhere beneath\n * it? The move-into-own-subtree refusal. The parameter is the name chain, not\n * a `SubsystemEntry`, because a chain is all this comparison reads — the one\n * caller outside this module (`move`) has a chain it just composed and no\n * entry to hand, and a structural parameter it never reads would force a\n * placeholder there.\n */\nexport function withinSubsystem(subPath: string[], path: string[]): boolean {\n return subPath.length <= path.length && subPath.every((name, i) => path[i] === name);\n}\n\n/** Services anywhere beneath a subsystem — the transitive membership the views and counts share. */\nexport function servicesUnder(tree: FleetTree, sub: SubsystemEntry): WalkedService[] {\n return tree.services.filter((s) => sub.path.every((name, i) => s.subsystem[i] === name));\n}\n\n/** Child subsystems anywhere beneath one — `rm`'s other half of \"empty\". */\nexport function subsystemsUnder(tree: FleetTree, sub: SubsystemEntry): SubsystemEntry[] {\n return tree.subsystems.filter((c) => c !== sub && withinSubsystem(sub.path, c.path));\n}\n"]}
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* package direction is `repo → repo/tree` (never back — `walk.ts` says why),
|
|
7
7
|
* so the shared spelling lives on this side of the edge.
|
|
8
8
|
*/
|
|
9
|
-
import { existsSync, statSync } from "node:fs";
|
|
9
|
+
import { existsSync, realpathSync, statSync } from "node:fs";
|
|
10
10
|
import { readdir } from "node:fs/promises";
|
|
11
11
|
import { join } from "node:path";
|
|
12
12
|
/**
|
|
@@ -61,4 +61,30 @@ export async function markdownFiles(dir) {
|
|
|
61
61
|
export async function countMarkdown(dir) {
|
|
62
62
|
return (await markdownFiles(dir)).length;
|
|
63
63
|
}
|
|
64
|
+
/**
|
|
65
|
+
* Mark a directory as descended-into; false when it (or another name for it)
|
|
66
|
+
* already was. The symlink-cycle guard every walk that follows links through
|
|
67
|
+
* `entryIs` carries — the service tree (`./walk.ts`), the capability tree, the
|
|
68
|
+
* glossary, the generated-gherkin scan — and for one reason: a followed cycle
|
|
69
|
+
* without a visited set recurses until the stack ends. A path realpath cannot
|
|
70
|
+
* resolve is one whose children cannot be read either, so it is never entered.
|
|
71
|
+
*/
|
|
72
|
+
export function markDescended(dir, visited) {
|
|
73
|
+
let real;
|
|
74
|
+
try {
|
|
75
|
+
real = realpathSync(dir);
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
if (visited.has(real))
|
|
81
|
+
return false;
|
|
82
|
+
visited.add(real);
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
/** Sorted non-dot subdirectory names, links followed — a tree walk's deterministic child order. */
|
|
86
|
+
export async function subdirNames(dir) {
|
|
87
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
88
|
+
return entries.filter((e) => !e.name.startsWith(".") && entryIs(dir, e, "dir")).map((e) => e.name).sort();
|
|
89
|
+
}
|
|
64
90
|
//# sourceMappingURL=fs.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fs.js","sourceRoot":"","sources":["../../../../src/core/repo/tree/fs.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EAAE,UAAU,EAAE,QAAQ,EAAe,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"fs.js","sourceRoot":"","sources":["../../../../src/core/repo/tree/fs.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,QAAQ,EAAe,MAAM,SAAS,CAAC;AAC1E,OAAO,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAC3C,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEjC;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,OAAO,CAAC,GAAW,EAAE,CAAS,EAAE,IAAoB;IAClE,IAAI,CAAC,CAAC,CAAC,cAAc,EAAE;QAAE,OAAO,IAAI,KAAK,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC;IAC9E,IAAI,CAAC;QACH,MAAM,CAAC,GAAG,QAAQ,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC;QACtC,OAAO,IAAI,KAAK,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC;IACvD,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,GAAW;IAC7C,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC;QAAE,OAAO,EAAE,CAAC;IAChC,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,GAAG,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,CAAC;IAC5D,OAAO,OAAO;SACX,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,OAAO,CAAC,GAAG,EAAE,CAAC,EAAE,MAAM,CAAC,CAAC;SAChE,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC;SAC7B,IAAI,EAAE,CAAC;AACZ,CAAC;AAED,4FAA4F;AAC5F,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,GAAW;IAC7C,OAAO,CAAC,MAAM,aAAa,CAAC,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC;AAC3C,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,aAAa,CAAC,GAAW,EAAE,OAAoB;IAC7D,IAAI,IAAY,CAAC;IACjB,IAAI,CAAC;QACH,IAAI,GAAG,YAAY,CAAC,GAAG,CAAC,CAAC;IAC3B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;IACD,IAAI,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC;QAAE,OAAO,KAAK,CAAC;IACpC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAClB,OAAO,IAAI,CAAC;AACd,CAAC;AAED,mGAAmG;AACnG,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,GAAW;IAC3C,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,GAAG,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,CAAC;IAC5D,OAAO,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,OAAO,CAAC,GAAG,EAAE,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,CAAC;AAC5G,CAAC","sourcesContent":["/**\n * Directory-entry questions the enumerations share, symlinks followed.\n *\n * Moved here from `repo/repo.ts` when the tree walk arrived: the walk and the\n * feature enumeration must answer \"what IS this entry\" identically, and the\n * package direction is `repo → repo/tree` (never back — `walk.ts` says why),\n * so the shared spelling lives on this side of the edge.\n */\nimport { existsSync, realpathSync, statSync, type Dirent } from \"node:fs\";\nimport { readdir } from \"node:fs/promises\";\nimport { join } from \"node:path\";\n\n/**\n * What a directory entry IS, following a symlink to find out.\n *\n * `readdir(withFileTypes)` does not follow symlinks: for a symlink `Dirent`\n * every `isDirectory()`/`isFile()` is false, whatever it points at. That made a\n * symlinked `services/<id>/` or `features/<id>/` vanish from every enumeration\n * at once — `list`, `validate --all`, the fleet gate — with no diagnostic\n * anywhere, which is the one outcome a shared docs repo cannot survive: the\n * fleet gate reported a service that is not there as fine because it never saw\n * it. Composing a docs repo out of symlinks (a worktree, a submodule, one\n * service's directory shared between two checkouts) is legitimate, so they are\n * FOLLOWED rather than refused. A dangling link stats as nothing and is skipped\n * exactly as an absent directory is — it is not a service, and saying so would\n * mean inventing an entry to hang the complaint on.\n */\nexport function entryIs(dir: string, e: Dirent, want: \"dir\" | \"file\"): boolean {\n if (!e.isSymbolicLink()) return want === \"dir\" ? e.isDirectory() : e.isFile();\n try {\n const s = statSync(join(dir, e.name));\n return want === \"dir\" ? s.isDirectory() : s.isFile();\n } catch {\n return false;\n }\n}\n\n/**\n * Markdown files sitting directly in `dir`, sorted, as full paths — the ADR\n * corpus, and the only place that rule is spelled. Symlinks are resolved\n * through `entryIs`, which is the whole reason this is shared: `show` once kept\n * a second copy that tested `Dirent.isFile()` itself — always false for a\n * symlink, whatever it points at — so a docs repo composed of symlinks (the\n * layout `entryIs` above exists to support) had `loam list` and `loam show`\n * reporting different ADR counts for the same service. One rule, one answer.\n *\n * The list, not the count, is the primitive: the link check reads these same\n * files, and a corpus that disagreed with the count would grade documents the\n * fleet listing says are not there.\n */\nexport async function markdownFiles(dir: string): Promise<string[]> {\n if (!existsSync(dir)) return [];\n const entries = await readdir(dir, { withFileTypes: true });\n return entries\n .filter((e) => e.name.endsWith(\".md\") && entryIs(dir, e, \"file\"))\n .map((e) => join(dir, e.name))\n .sort();\n}\n\n/** How many markdown files sit directly in `dir` — the ADR count, over the corpus above. */\nexport async function countMarkdown(dir: string): Promise<number> {\n return (await markdownFiles(dir)).length;\n}\n\n/**\n * Mark a directory as descended-into; false when it (or another name for it)\n * already was. The symlink-cycle guard every walk that follows links through\n * `entryIs` carries — the service tree (`./walk.ts`), the capability tree, the\n * glossary, the generated-gherkin scan — and for one reason: a followed cycle\n * without a visited set recurses until the stack ends. A path realpath cannot\n * resolve is one whose children cannot be read either, so it is never entered.\n */\nexport function markDescended(dir: string, visited: Set<string>): boolean {\n let real: string;\n try {\n real = realpathSync(dir);\n } catch {\n return false;\n }\n if (visited.has(real)) return false;\n visited.add(real);\n return true;\n}\n\n/** Sorted non-dot subdirectory names, links followed — a tree walk's deterministic child order. */\nexport async function subdirNames(dir: string): Promise<string[]> {\n const entries = await readdir(dir, { withFileTypes: true });\n return entries.filter((e) => !e.name.startsWith(\".\") && entryIs(dir, e, \"dir\")).map((e) => e.name).sort();\n}\n"]}
|
|
@@ -43,13 +43,12 @@
|
|
|
43
43
|
* for the symlink-cycle guard — `entryIs` follows symlinks on purpose, and a
|
|
44
44
|
* followed cycle without the visited-set is an unbounded recursion.
|
|
45
45
|
*/
|
|
46
|
-
import { realpathSync } from "node:fs";
|
|
47
46
|
import { readdir } from "node:fs/promises";
|
|
48
47
|
import { join } from "node:path";
|
|
49
48
|
import { serviceDirOf, serviceTreePath } from "../../kernel/ids/dirs.js";
|
|
50
49
|
import { rawServiceId } from "../../kernel/ids/service.js";
|
|
51
50
|
import { subsystemNameProblem } from "../../kernel/ids/subsystem.js";
|
|
52
|
-
import { entryIs } from "./fs.js";
|
|
51
|
+
import { entryIs, markDescended, subdirNames } from "./fs.js";
|
|
53
52
|
import { readSubsystemMarker, SUBSYSTEM_MARKER } from "./marker.js";
|
|
54
53
|
/**
|
|
55
54
|
* The subdirectory names SCHEMA's service layout reserves for a service's own
|
|
@@ -82,39 +81,13 @@ export async function walkServicesTree(request) {
|
|
|
82
81
|
names: new Map(),
|
|
83
82
|
};
|
|
84
83
|
const root = join(request.docsDir, "services");
|
|
85
|
-
|
|
84
|
+
markDescended(root, ctx.visited);
|
|
86
85
|
for (const name of await subdirNames(root)) {
|
|
87
86
|
await visitDir({ dir: join(root, name), name, chain: [] }, ctx);
|
|
88
87
|
}
|
|
89
88
|
namespaceFindings(ctx);
|
|
90
89
|
return ctx.tree;
|
|
91
90
|
}
|
|
92
|
-
/**
|
|
93
|
-
* Mark a directory as descended-into; false when it (or another name for it)
|
|
94
|
-
* already was. Guards SUBTREES, not entries: a directory reachable under two
|
|
95
|
-
* names is classified twice — both names exist and hiding the second would be
|
|
96
|
-
* the silent shrink this module refuses — but its children are enumerated
|
|
97
|
-
* once, so a symlink cycle terminates instead of recursing forever. A path
|
|
98
|
-
* realpath cannot resolve is one whose children cannot be read either.
|
|
99
|
-
*/
|
|
100
|
-
function guardDescent(dir, ctx) {
|
|
101
|
-
let real;
|
|
102
|
-
try {
|
|
103
|
-
real = realpathSync(dir);
|
|
104
|
-
}
|
|
105
|
-
catch {
|
|
106
|
-
return false;
|
|
107
|
-
}
|
|
108
|
-
if (ctx.visited.has(real))
|
|
109
|
-
return false;
|
|
110
|
-
ctx.visited.add(real);
|
|
111
|
-
return true;
|
|
112
|
-
}
|
|
113
|
-
/** Sorted non-dot subdirectory names — the walk's deterministic child order. */
|
|
114
|
-
async function subdirNames(dir) {
|
|
115
|
-
const entries = await readdir(dir, { withFileTypes: true });
|
|
116
|
-
return entries.filter((e) => !e.name.startsWith(".") && entryIs(dir, e, "dir")).map((e) => e.name).sort();
|
|
117
|
-
}
|
|
118
91
|
/** Classify one directory and, for the group kinds, walk what is inside it. */
|
|
119
92
|
async function visitDir(spot, ctx) {
|
|
120
93
|
const rel = ["services", ...spot.chain, spot.name].join("/");
|
|
@@ -219,7 +192,12 @@ async function visitSubsystem(spot, at, ctx) {
|
|
|
219
192
|
*/
|
|
220
193
|
async function descendInto(spot, dirs, ctx) {
|
|
221
194
|
const before = ctx.tree.services.length;
|
|
222
|
-
|
|
195
|
+
// The guard (`./fs.ts`'s `markDescended`) protects SUBTREES, not entries: a
|
|
196
|
+
// directory reachable under two names is classified twice — both names
|
|
197
|
+
// exist and hiding the second would be the silent shrink this module
|
|
198
|
+
// refuses — but its children are enumerated once, so a symlink cycle
|
|
199
|
+
// terminates instead of recursing forever.
|
|
200
|
+
if (markDescended(spot.dir, ctx.visited)) {
|
|
223
201
|
for (const child of dirs) {
|
|
224
202
|
await visitDir({ dir: join(spot.dir, child), name: child, chain: [...spot.chain, spot.name] }, ctx);
|
|
225
203
|
}
|