@ybotok/loam 0.2.0-alpha.5 → 0.2.0-alpha.7
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 +1063 -0
- package/MIGRATING-from-OpenSpec.md +7 -4
- package/QUICKSTART.md +8 -3
- package/README.md +13 -7
- package/ROADMAP.md +285 -11
- package/SCHEMA.md +566 -45
- package/WORKFLOW.md +23 -10
- package/dist/commands/adopt/render.js +47 -8
- package/dist/commands/adopt/render.js.map +1 -1
- package/dist/commands/archive/plan/landscape.js +45 -1
- package/dist/commands/archive/plan/landscape.js.map +1 -1
- package/dist/commands/archive/plan/model/extending.js +85 -0
- package/dist/commands/archive/plan/model/extending.js.map +1 -0
- package/dist/commands/archive/run.js +53 -23
- package/dist/commands/archive/run.js.map +1 -1
- package/dist/commands/context/print.js +11 -1
- package/dist/commands/context/print.js.map +1 -1
- package/dist/commands/doctor.js +46 -0
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/explore.js +12 -2
- package/dist/commands/explore.js.map +1 -1
- package/dist/commands/list/views.js +5 -1
- package/dist/commands/list/views.js.map +1 -1
- package/dist/commands/seed/commit.js +34 -1
- package/dist/commands/seed/commit.js.map +1 -1
- package/dist/commands/seed/plan.js +158 -13
- package/dist/commands/seed/plan.js.map +1 -1
- package/dist/commands/seed/seed.js +7 -2
- package/dist/commands/seed/seed.js.map +1 -1
- package/dist/commands/show/feature.js +2 -1
- package/dist/commands/show/feature.js.map +1 -1
- package/dist/commands/show/marks.js +3 -5
- package/dist/commands/show/marks.js.map +1 -1
- package/dist/commands/show/service.js +39 -18
- package/dist/commands/show/service.js.map +1 -1
- package/dist/commands/subsystem/edit.js +4 -2
- package/dist/commands/subsystem/edit.js.map +1 -1
- package/dist/commands/subsystem/move.js +55 -17
- package/dist/commands/subsystem/move.js.map +1 -1
- package/dist/commands/subsystem/sync.js +238 -87
- package/dist/commands/subsystem/sync.js.map +1 -1
- package/dist/commands/subsystem/txn/exclude.js +132 -0
- package/dist/commands/subsystem/txn/exclude.js.map +1 -0
- package/dist/commands/subsystem/txn/report.js +174 -0
- package/dist/commands/subsystem/txn/report.js.map +1 -0
- package/dist/commands/subsystem/txn/txn.js +126 -22
- package/dist/commands/subsystem/txn/txn.js.map +1 -1
- package/dist/commands/subsystem/txn/views.js +159 -19
- package/dist/commands/subsystem/txn/views.js.map +1 -1
- package/dist/commands/validate/arch-coverage.js +16 -9
- package/dist/commands/validate/arch-coverage.js.map +1 -1
- package/dist/commands/validate/checks/fleet-shape.js +30 -77
- package/dist/commands/validate/checks/fleet-shape.js.map +1 -1
- package/dist/commands/validate/checks/health-deps.js +9 -6
- package/dist/commands/validate/checks/health-deps.js.map +1 -1
- package/dist/commands/validate/checks/vocabulary.js +4 -19
- package/dist/commands/validate/checks/vocabulary.js.map +1 -1
- package/dist/commands/validate/feature.js +1 -1
- package/dist/commands/validate/feature.js.map +1 -1
- package/dist/commands/validate/fleet/census.js +37 -13
- package/dist/commands/validate/fleet/census.js.map +1 -1
- package/dist/commands/validate/fleet/landscape.js +131 -120
- package/dist/commands/validate/fleet/landscape.js.map +1 -1
- package/dist/commands/validate/fleet/load.js +66 -0
- package/dist/commands/validate/fleet/load.js.map +1 -1
- package/dist/commands/validate/fleet/map/attest.js +102 -0
- package/dist/commands/validate/fleet/map/attest.js.map +1 -0
- package/dist/commands/validate/fleet/map/bindings.js +109 -0
- package/dist/commands/validate/fleet/map/bindings.js.map +1 -0
- package/dist/commands/validate/fleet/map/consumers.js +219 -0
- package/dist/commands/validate/fleet/map/consumers.js.map +1 -0
- package/dist/commands/validate/fleet/map/isolation.js +116 -0
- package/dist/commands/validate/fleet/map/isolation.js.map +1 -0
- package/dist/commands/validate/fleet/map/usecases.js +56 -0
- package/dist/commands/validate/fleet/map/usecases.js.map +1 -0
- package/dist/commands/validate/fleet/scorecard/scorecard.js.map +1 -1
- package/dist/commands/validate/fleet/usecases/steps.js +49 -17
- package/dist/commands/validate/fleet/usecases/steps.js.map +1 -1
- package/dist/commands/validate/fleet/views/fleet-project.js +217 -0
- package/dist/commands/validate/fleet/views/fleet-project.js.map +1 -0
- package/dist/commands/validate/fleet/views/ids.js +45 -15
- package/dist/commands/validate/fleet/views/ids.js.map +1 -1
- package/dist/commands/validate/fleet/views/projects.js +258 -70
- package/dist/commands/validate/fleet/views/projects.js.map +1 -1
- package/dist/commands/validate/fleet/views/root-documents.js +99 -0
- package/dist/commands/validate/fleet/views/root-documents.js.map +1 -0
- package/dist/commands/validate/fleet/views/stale.js +32 -15
- package/dist/commands/validate/fleet/views/stale.js.map +1 -1
- package/dist/commands/validate/service/model/diverged.js +226 -0
- package/dist/commands/validate/service/model/diverged.js.map +1 -0
- package/dist/commands/validate/service/model/grade.js +163 -0
- package/dist/commands/validate/service/model/grade.js.map +1 -0
- package/dist/commands/validate/service/service.js +110 -34
- package/dist/commands/validate/service/service.js.map +1 -1
- package/dist/commands/validate/service/specs.js +39 -5
- package/dist/commands/validate/service/specs.js.map +1 -1
- package/dist/commands/validate/service/spine.js +41 -3
- package/dist/commands/validate/service/spine.js.map +1 -1
- package/dist/commands/validate/service/usecases/tags.js +113 -0
- package/dist/commands/validate/service/usecases/tags.js.map +1 -0
- package/dist/commands/validate/service/usecases/usecases.js +209 -0
- package/dist/commands/validate/service/usecases/usecases.js.map +1 -0
- package/dist/commands/validate/validate.js +10 -24
- package/dist/commands/validate/validate.js.map +1 -1
- package/dist/core/agent/agents-md/artifacts.js +1 -0
- package/dist/core/agent/agents-md/artifacts.js.map +1 -1
- package/dist/core/agent/agents-md/command-map.js +19 -5
- package/dist/core/agent/agents-md/command-map.js.map +1 -1
- package/dist/core/agent/agents-md/cycle.js +24 -9
- package/dist/core/agent/agents-md/cycle.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/subsystem.js +70 -17
- package/dist/core/agent/agents-md/map/subsystem.js.map +1 -1
- package/dist/core/agent/agents-md/map/usecases.js +45 -3
- package/dist/core/agent/agents-md/map/usecases.js.map +1 -1
- package/dist/core/agent/agents-md/refusals.js +5 -3
- package/dist/core/agent/agents-md/refusals.js.map +1 -1
- package/dist/core/agent/agents-md.js +26 -7
- package/dist/core/agent/agents-md.js.map +1 -1
- package/dist/core/agent/support/report.js +39 -5
- package/dist/core/agent/support/report.js.map +1 -1
- package/dist/core/agent/workflows/adopt.js +123 -14
- package/dist/core/agent/workflows/adopt.js.map +1 -1
- package/dist/core/agent/workflows/check.js +34 -20
- package/dist/core/agent/workflows/check.js.map +1 -1
- package/dist/core/agent/workflows/closing.js +3 -1
- package/dist/core/agent/workflows/closing.js.map +1 -1
- package/dist/core/agent/workflows/reference/spine.js +12 -6
- package/dist/core/agent/workflows/reference/spine.js.map +1 -1
- package/dist/core/brief/brief.js +88 -19
- package/dist/core/brief/brief.js.map +1 -1
- package/dist/core/brief/checks.js +133 -7
- package/dist/core/brief/checks.js.map +1 -1
- package/dist/core/brief/landscape.js +212 -129
- package/dist/core/brief/landscape.js.map +1 -1
- package/dist/core/brief/map/extend.js +81 -0
- package/dist/core/brief/map/extend.js.map +1 -0
- package/dist/core/brief/map/owed.js +284 -0
- package/dist/core/brief/map/owed.js.map +1 -0
- package/dist/core/brief/targets.js +94 -29
- package/dist/core/brief/targets.js.map +1 -1
- package/dist/core/brief/unchecked.js +13 -3
- package/dist/core/brief/unchecked.js.map +1 -1
- package/dist/core/c4/likec4.js +23 -1
- package/dist/core/c4/likec4.js.map +1 -1
- package/dist/core/c4/parsed/deployment.js +5 -3
- package/dist/core/c4/parsed/deployment.js.map +1 -1
- package/dist/core/c4/parsed/view-ids.js +39 -1
- package/dist/core/c4/parsed/view-ids.js.map +1 -1
- package/dist/core/c4/prefetch/fleet.js +142 -0
- package/dist/core/c4/prefetch/fleet.js.map +1 -0
- package/dist/core/c4/project/architecture.js +77 -7
- package/dist/core/c4/project/architecture.js.map +1 -1
- package/dist/core/c4/project/batch.js +140 -0
- package/dist/core/c4/project/batch.js.map +1 -0
- package/dist/core/c4/project/documents.js +12 -6
- package/dist/core/c4/project/documents.js.map +1 -1
- package/dist/core/c4/project/load.js +85 -42
- package/dist/core/c4/project/load.js.map +1 -1
- package/dist/core/c4/project/staged.js +53 -15
- package/dist/core/c4/project/staged.js.map +1 -1
- package/dist/core/c4/resolve/attested.js +0 -0
- package/dist/core/c4/resolve/attested.js.map +1 -0
- package/dist/core/c4/resolve/service.js +8 -17
- package/dist/core/c4/resolve/service.js.map +1 -1
- package/dist/core/c4/resolve/steps.js +18 -0
- package/dist/core/c4/resolve/steps.js.map +1 -1
- package/dist/core/c4/root-project/exclude.js +352 -0
- package/dist/core/c4/root-project/exclude.js.map +1 -0
- package/dist/core/c4/service-model/documents.js +47 -0
- package/dist/core/c4/service-model/documents.js.map +1 -0
- package/dist/core/c4/service-model/fleet/extending.js +82 -0
- package/dist/core/c4/service-model/fleet/extending.js.map +1 -0
- package/dist/core/c4/service-model/load.js +165 -0
- package/dist/core/c4/service-model/load.js.map +1 -0
- package/dist/core/c4/service-model/renderer.js +128 -0
- package/dist/core/c4/service-model/renderer.js.map +1 -0
- package/dist/core/c4/service-model/shape.js +126 -0
- package/dist/core/c4/service-model/shape.js.map +1 -0
- package/dist/core/c4/service-model/slice.js +188 -0
- package/dist/core/c4/service-model/slice.js.map +1 -0
- package/dist/core/c4/source-scan.js +26 -0
- package/dist/core/c4/source-scan.js.map +1 -1
- package/dist/core/c4/splice/authored-source.js +60 -13
- package/dist/core/c4/splice/authored-source.js.map +1 -1
- package/dist/core/c4/splice/contract.js.map +1 -1
- package/dist/core/c4/splice/identity/declared.js +87 -0
- package/dist/core/c4/splice/identity/declared.js.map +1 -0
- package/dist/core/c4/splice/identity/edges.js +17 -0
- package/dist/core/c4/splice/identity/edges.js.map +1 -1
- package/dist/core/c4/splice/identity/existing.js +80 -0
- package/dist/core/c4/splice/identity/existing.js.map +1 -0
- package/dist/core/c4/splice/identity/interior.js +80 -0
- package/dist/core/c4/splice/identity/interior.js.map +1 -0
- package/dist/core/c4/splice/identity/owner.js +50 -0
- package/dist/core/c4/splice/identity/owner.js.map +1 -0
- package/dist/core/c4/splice/landscape-merge.js +154 -100
- package/dist/core/c4/splice/landscape-merge.js.map +1 -1
- package/dist/core/c4/splice/model/merge.js +140 -0
- package/dist/core/c4/splice/model/merge.js.map +1 -0
- package/dist/core/c4/splice/placement.js +14 -4
- package/dist/core/c4/splice/placement.js.map +1 -1
- package/dist/core/c4/workspace.js +2 -1
- package/dist/core/c4/workspace.js.map +1 -1
- package/dist/core/coherence/coherence.js +7 -0
- package/dist/core/coherence/coherence.js.map +1 -1
- package/dist/core/delta/delta.js +9 -0
- package/dist/core/delta/delta.js.map +1 -1
- package/dist/core/deployment/delta.js +16 -10
- package/dist/core/deployment/delta.js.map +1 -1
- package/dist/core/diff/base-state.js +21 -5
- package/dist/core/diff/base-state.js.map +1 -1
- package/dist/core/diff/victims.js +10 -1
- package/dist/core/diff/victims.js.map +1 -1
- package/dist/core/docs.js +29 -24
- package/dist/core/docs.js.map +1 -1
- package/dist/core/doctor/config.js +7 -3
- package/dist/core/doctor/config.js.map +1 -1
- package/dist/core/doctor/doctor.js +36 -8
- package/dist/core/doctor/doctor.js.map +1 -1
- package/dist/core/doctor/report.js.map +1 -1
- package/dist/core/doctor/reports/scan.js +242 -0
- package/dist/core/doctor/reports/scan.js.map +1 -0
- package/dist/core/explain/refusals.js +1 -1
- package/dist/core/explain/refusals.js.map +1 -1
- package/dist/core/explore/explore.js +44 -9
- package/dist/core/explore/explore.js.map +1 -1
- package/dist/core/fleet-context.js +47 -0
- package/dist/core/fleet-context.js.map +1 -1
- package/dist/core/gate/checks.js +5 -1
- package/dist/core/gate/checks.js.map +1 -1
- package/dist/core/gate/partners.js +65 -26
- package/dist/core/gate/partners.js.map +1 -1
- package/dist/core/kernel/ids/fqn/ancestors.js +65 -0
- package/dist/core/kernel/ids/fqn/ancestors.js.map +1 -0
- package/dist/core/pack/living.js +47 -13
- package/dist/core/pack/living.js.map +1 -1
- package/dist/core/pack/pack.js +9 -6
- package/dist/core/pack/pack.js.map +1 -1
- package/dist/core/projection/arch-slice.js +2 -2
- package/dist/core/projection/arch-slice.js.map +1 -1
- package/dist/core/repo/paths.js +45 -38
- package/dist/core/repo/paths.js.map +1 -1
- package/dist/core/repo/tree/render/body.js +45 -6
- package/dist/core/repo/tree/render/body.js.map +1 -1
- package/dist/core/repo/tree/render/projects.js +93 -35
- package/dist/core/repo/tree/render/projects.js.map +1 -1
- package/dist/core/repo/tree/render/service/header.js +125 -0
- package/dist/core/repo/tree/render/service/header.js.map +1 -0
- package/dist/core/repo/tree/render/service/peers.js +28 -0
- package/dist/core/repo/tree/render/service/peers.js.map +1 -0
- package/dist/core/repo/tree/render/service/view-id.js +67 -0
- package/dist/core/repo/tree/render/service/view-id.js.map +1 -0
- package/dist/core/repo/tree/render/service/views.js +78 -0
- package/dist/core/repo/tree/render/service/views.js.map +1 -0
- package/dist/core/repo/tree/render/stale.js +20 -4
- package/dist/core/repo/tree/render/stale.js.map +1 -1
- package/dist/core/repo/tree/render/views.js +149 -32
- package/dist/core/repo/tree/render/views.js.map +1 -1
- package/dist/core/repo/tree/walk.js +45 -2
- package/dist/core/repo/tree/walk.js.map +1 -1
- package/dist/core/scaffold/landscape.js +219 -9
- package/dist/core/scaffold/landscape.js.map +1 -1
- package/dist/core/staging/writes.js.map +1 -1
- package/dist/core/usecases/delta/claims.js +1 -0
- package/dist/core/usecases/delta/claims.js.map +1 -1
- package/dist/core/usecases/delta/overlay.js +4 -0
- package/dist/core/usecases/delta/overlay.js.map +1 -1
- package/dist/core/usecases/fleet.js +18 -6
- package/dist/core/usecases/fleet.js.map +1 -1
- package/dist/core/usecases/operations.js +7 -0
- package/dist/core/usecases/operations.js.map +1 -1
- package/dist/core/usecases/service/exclude.js +119 -0
- package/dist/core/usecases/service/exclude.js.map +1 -0
- package/dist/core/usecases/service/flows.js +161 -0
- package/dist/core/usecases/service/flows.js.map +1 -0
- package/dist/core/verify/checklist.js +42 -10
- package/dist/core/verify/checklist.js.map +1 -1
- package/dist/core/vocabulary/issue.js.map +1 -1
- package/dist/core/vocabulary/maturity.js +51 -1
- package/dist/core/vocabulary/maturity.js.map +1 -1
- package/examples/README.md +30 -8
- package/examples/docs/architecture/landscape.likec4 +47 -0
- package/examples/docs/architecture/services.likec4 +70 -0
- package/examples/docs/architecture/subsystems.likec4 +3 -0
- package/examples/docs/architecture/usecases/order-notification.likec4 +5 -2
- package/examples/docs/likec4.config.json +0 -1
- package/examples/docs/services/checkout-web/model.likec4 +39 -56
- package/examples/docs/services/order-service/arch.spec.md +13 -6
- package/examples/docs/services/order-service/model.likec4 +58 -70
- package/examples/docs/services/payment-service/arch.spec.md +7 -4
- package/examples/docs/services/payment-service/model.likec4 +38 -39
- package/examples/docs/services/platform/identity-service/arch.spec.md +7 -4
- package/examples/docs/services/platform/identity-service/model.likec4 +35 -37
- package/examples/docs/services/platform/notification-service/health.yaml +8 -3
- package/examples/docs/services/platform/notification-service/model.likec4 +43 -64
- package/package.json +1 -1
- package/examples/docs/services/checkout-web/likec4.config.json +0 -4
- package/examples/docs/services/order-service/likec4.config.json +0 -4
- package/examples/docs/services/payment-service/likec4.config.json +0 -4
- package/examples/docs/services/platform/identity-service/likec4.config.json +0 -4
- package/examples/docs/services/platform/notification-service/likec4.config.json +0 -4
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
import { access, readFile } from "node:fs/promises";
|
|
16
16
|
import { dirname } from "node:path";
|
|
17
17
|
import { configPath, parseConfig } from "../envelope/config.js";
|
|
18
|
-
import { loadFile } from "../c4/likec4.js";
|
|
18
|
+
import { errorText, loadFile } from "../c4/likec4.js";
|
|
19
19
|
import { conflictMarkerLines } from "../conflict-markers.js";
|
|
20
20
|
import {} from "./report.js";
|
|
21
21
|
/**
|
|
@@ -113,8 +113,12 @@ export async function inspectLandscape(path, findings) {
|
|
|
113
113
|
findings.push({
|
|
114
114
|
severity: "blocker",
|
|
115
115
|
code: "doctor.landscape-invalid",
|
|
116
|
-
|
|
117
|
-
|
|
116
|
+
// Through `errorText`, the one spelling of a diagnostic loam prints. This
|
|
117
|
+
// site hand-rolled its own and passed the LSP range's line through raw, so
|
|
118
|
+
// `doctor` named the line ABOVE the fault and disagreed with `validate
|
|
119
|
+
// --all` about the same error on the same tree — the last formatter the
|
|
120
|
+
// 1-based consolidation missed (verification 2026-09-04).
|
|
121
|
+
message: `architecture/landscape.likec4 does not parse: ${errorText(first)}` +
|
|
118
122
|
(doc.errors.length > 1 ? ` — and ${doc.errors.length - 1} more` : ""),
|
|
119
123
|
fix: `Fix ${path}; every fleet-wide check is blind until it parses.`,
|
|
120
124
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.js","sourceRoot":"","sources":["../../../src/core/doctor/config.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AACpD,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,UAAU,EAAE,WAAW,EAAmB,MAAM,uBAAuB,CAAC;AACjF,OAAO,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../../../src/core/doctor/config.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The config and the fleet map, read the way every other command reads them.\n *\n * `doctor` used to carry its own config validator. It agreed with `loadConfig`\n * on the fields both happened to check and disagreed everywhere else — it\n * accepted a `gherkinDir` of `\"../shared\"` that `loadConfig` refused outright,\n * so `doctor` reported a healthy repo in which no command could run. Two\n * validators are two opinions about one file, and the one the user reads is\n * never the one the commands obey.\n *\n * The landscape is READ, not stat'ed, for the same reason: `doctor` once\n * reported `landscape: yes` for a file full of conflict markers, which answers\n * \"is there a file\" — not the question anyone runs doctor to ask.\n */\nimport { access, readFile } from \"node:fs/promises\";\nimport { dirname } from \"node:path\";\nimport { configPath, parseConfig, type LoamConfig } from \"../envelope/config.js\";\nimport { errorText, loadFile } from \"../c4/likec4.js\";\nimport { conflictMarkerLines } from \"../conflict-markers.js\";\nimport { type DoctorFinding, type DoctorReport } from \"./report.js\";\n\nexport interface ConfigInspection {\n status: DoctorReport[\"config\"][\"status\"];\n config: LoamConfig | null;\n error: string | null;\n}\n\n/**\n * Read and validate the config the way every other command does — through\n * `parseConfig`, never through a second implementation.\n *\n * doctor used to carry its own validator. It agreed with `loadConfig` on the\n * fields both happened to check and disagreed everywhere else: doctor accepted\n * a `gherkinDir` of `\"../shared\"` that `loadConfig` refused outright, so\n * `doctor` reported a healthy repo in which no command could run. Two\n * validators are two opinions about the same file, and the one the user reads\n * is never the one the commands obey.\n */\nexport async function inspectConfig(cwd: string): Promise<ConfigInspection> {\n const path = configPath(cwd);\n let raw: string;\n try {\n raw = await readFile(path, \"utf8\");\n } catch (error) {\n const code = (error as NodeJS.ErrnoException).code;\n if (code === \"ENOENT\") return { status: \"missing\", config: null, error: null };\n return {\n status: \"invalid\",\n config: null,\n error: error instanceof Error ? error.message : String(error),\n };\n }\n\n try {\n return { status: \"valid\", error: null, config: parseConfig(raw, dirname(path)) };\n } catch (error) {\n return {\n status: \"invalid\",\n config: null,\n error: error instanceof Error ? error.message : String(error),\n };\n }\n}\n\nexport async function canAccess(path: string, mode: number): Promise<boolean> {\n try {\n await access(path, mode);\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * Read the landscape, don't just stat it. `doctor` reported `landscape: yes`\n * for a file full of conflict markers — the check answered \"is there a file\",\n * which is not the question anyone runs doctor to ask.\n */\nexport async function inspectLandscape(path: string, findings: DoctorFinding[]): Promise<void> {\n let source: string;\n try {\n source = await readFile(path, \"utf8\");\n } catch (error) {\n findings.push({\n severity: \"blocker\",\n code: \"doctor.landscape-unreadable\",\n message: `${path} exists but could not be read: ${error instanceof Error ? error.message : String(error)}`,\n fix: `Check permissions on ${path}.`,\n });\n return;\n }\n\n // The three-way merge left its markers in the file. Checked BEFORE the\n // parser, because a conflicted landscape does parse sometimes — both sides of\n // a conflict can be syntactically valid LikeC4 — and \"your map contains two\n // halves of two different maps\" is a more useful sentence than any parser\n // error. This is the failure mode of onboarding a fleet: ten people adopt ten\n // services into one landscape.likec4 in the same week.\n //\n // The scan itself is conflict-markers's, which is where the same rule already\n // grades every other document loam reads. doctor kept a second copy of it,\n // and a second copy of a rule is a second chance to spell it differently;\n // what doctor owns is what a conflicted landscape COSTS, which is the finding\n // below and not the search.\n const conflicted = conflictMarkerLines(source);\n if (conflicted.length > 0) {\n findings.push({\n severity: \"blocker\",\n code: \"doctor.landscape-merge-conflict\",\n message:\n `architecture/landscape.likec4 still contains merge conflict markers ` +\n `(line${conflicted.length === 1 ? \"\" : \"s\"} ${conflicted.join(\", \")}).`,\n fix: `Resolve the conflict in ${path} — keep BOTH services' elements and edges, then re-run \\`loam doctor\\`.`,\n });\n return;\n }\n\n const doc = await loadFile(path);\n if (doc.errors.length > 0) {\n const first = doc.errors[0]!;\n findings.push({\n severity: \"blocker\",\n code: \"doctor.landscape-invalid\",\n // Through `errorText`, the one spelling of a diagnostic loam prints. This\n // site hand-rolled its own and passed the LSP range's line through raw, so\n // `doctor` named the line ABOVE the fault and disagreed with `validate\n // --all` about the same error on the same tree — the last formatter the\n // 1-based consolidation missed (verification 2026-09-04).\n message:\n `architecture/landscape.likec4 does not parse: ${errorText(first)}` +\n (doc.errors.length > 1 ? ` — and ${doc.errors.length - 1} more` : \"\"),\n fix: `Fix ${path}; every fleet-wide check is blind until it parses.`,\n });\n }\n}\n\n/**\n * The tool ids loam.json records `init` as having written files for, or null\n * when it records none we can plan for.\n *\n * `parseConfig` checks `agentTools`' SHAPE and deliberately not its contents —\n * a config written by a newer binary may name a tool this one has never heard\n * of, and refusing to load it would break every command, not just this check.\n * So the registry filter belongs here: `plannedCommandFiles` throws on an id it\n * does not know, and a config from the future must not turn the read-only\n * preflight into a crash. A tool we cannot plan files for is a tool we have\n * nothing to say about.\n */\n"]}
|
|
@@ -23,6 +23,7 @@ import {} from "./report.js";
|
|
|
23
23
|
import { canAccess, inspectConfig, inspectLandscape } from "./config.js";
|
|
24
24
|
import { inspectAgentSurface } from "./agents.js";
|
|
25
25
|
import { gradeWritePathResidue } from "./residue.js";
|
|
26
|
+
import { scanReports } from "./reports/scan.js";
|
|
26
27
|
export async function diagnose(cwd = process.cwd()) {
|
|
27
28
|
const inspected = await inspectConfig(cwd);
|
|
28
29
|
const path = configPath(cwd);
|
|
@@ -139,15 +140,16 @@ export async function diagnose(cwd = process.cwd()) {
|
|
|
139
140
|
severity: "warning",
|
|
140
141
|
code: "doctor.likec4-config-missing",
|
|
141
142
|
message: `${docsDir}/${LIKEC4_PROJECT_FILENAME} is missing, so this tree is not a loadable LikeC4 workspace — `
|
|
142
|
-
+ "
|
|
143
|
-
+ "
|
|
143
|
+
+ "a feature delta, and any service model that declares its own `specification` block, has to be "
|
|
144
|
+
+ "excluded from the root project, and pointing a renderer at the repo root with nothing excluded "
|
|
145
|
+
+ "merges them into one model and reports every declaration as a duplicate.",
|
|
144
146
|
fix: `Write ${docsDir}/${LIKEC4_PROJECT_FILENAME} with:\n${LIKEC4_PROJECT_CONFIG.trimEnd()}\n`
|
|
145
|
-
+ "That
|
|
146
|
-
+ "the
|
|
147
|
-
+ "
|
|
148
|
-
+ "
|
|
149
|
-
+ "
|
|
150
|
-
+ "being readable alone is what
|
|
147
|
+
+ "That makes the docs root one LikeC4 project holding the fleet map and every service model that "
|
|
148
|
+
+ "extends it, so `npx likec4 start` in the docs repo renders both. Then run `loam subsystem sync`: "
|
|
149
|
+
+ "it maintains the `exclude` list above — one `services/<tree>/**` entry per model that stands "
|
|
150
|
+
+ "alone — and writes one create-only likec4.config.json beside each of those, so a standalone model "
|
|
151
|
+
+ "renders from the same root as a project of its own. Only a feature delta still renders from its "
|
|
152
|
+
+ "own directory (`npx likec4 start features/<FEAT>`) — being readable alone is what that file is for.",
|
|
151
153
|
});
|
|
152
154
|
}
|
|
153
155
|
// What a killed writer left behind. Read from `scanWritePathResidue`, whose
|
|
@@ -257,6 +259,31 @@ export async function diagnose(cwd = process.cwd()) {
|
|
|
257
259
|
// to the repo `init` ran in — the directory holding loam.json, which is not
|
|
258
260
|
// the cwd now that config discovery walks upward.
|
|
259
261
|
const agents = await inspectAgentSurface(dirname(path), inspected.config, docsDir, findings);
|
|
262
|
+
// The problem reports, read beside the config that RESOLVED — `config.root`,
|
|
263
|
+
// the directory the loam.json was found in — because the `loam-report`
|
|
264
|
+
// protocol says "at the current repository root", and config discovery walks
|
|
265
|
+
// upward, so the cwd is not that root.
|
|
266
|
+
//
|
|
267
|
+
// Where nothing PARSED, the directory of the file loam FOUND, which is what
|
|
268
|
+
// `dirname(path)` is: `configPath` returns the discovered loam.json, or the
|
|
269
|
+
// cwd's own spelling of it when there is none, so this is the found root when
|
|
270
|
+
// there is one and the cwd when there is not — the same root the agent surface
|
|
271
|
+
// above is inspected against. Falling back to the raw cwd instead meant that
|
|
272
|
+
// from a subdirectory of a repo whose loam.json is unparseable, `doctor`
|
|
273
|
+
// reported `total 0, next 001` while five reports sat beside the config it had
|
|
274
|
+
// just named in the same envelope, and handed out an ordinal already taken
|
|
275
|
+
// (verification 2026-09-04, second pass). A report corpus in a repo whose
|
|
276
|
+
// loam.json is broken is still worth counting — this is precisely the preflight
|
|
277
|
+
// that runs when the config does not resolve — and the blocker above already
|
|
278
|
+
// names the real problem.
|
|
279
|
+
//
|
|
280
|
+
// No findings come back, and that is the design: this is the one directory
|
|
281
|
+
// loam reads that loam never writes, its contents are the team's, and an open
|
|
282
|
+
// report is not a defect in the repository holding it. `doctor` reports it as
|
|
283
|
+
// state — like `docs`, `counts` and the write path — so the corpus is
|
|
284
|
+
// countable by the protocol that asks for it (`reports.next` is the ordinal
|
|
285
|
+
// the next report takes) without ever becoming a complaint.
|
|
286
|
+
const reports = await scanReports(inspected.config?.root ?? dirname(path));
|
|
260
287
|
return {
|
|
261
288
|
healthy: !findings.some((finding) => finding.severity === "blocker"),
|
|
262
289
|
runtime: {
|
|
@@ -278,6 +305,7 @@ export async function diagnose(cwd = process.cwd()) {
|
|
|
278
305
|
counts: { services: services.length, activeFeatures: activeFeatures.length },
|
|
279
306
|
currentService,
|
|
280
307
|
agents,
|
|
308
|
+
reports,
|
|
281
309
|
writePath,
|
|
282
310
|
serviceWritePath,
|
|
283
311
|
findings,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"doctor.js","sourceRoot":"","sources":["../../../src/core/doctor/doctor.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The preflight: can loam run here at all, and can its answers be trusted?\n *\n * The walk only. Each question it asks lives in its own module — `./config.ts`\n * for the config and the fleet map, `./agents.ts` for the agent surface,\n * `./residue.ts` for what a killed writer left — and every one of them returns\n * findings rather than printing. That is the whole contract this module keeps:\n * `doctor` is the command that must be able to describe a broken repository\n * without becoming the next thing that breaks in it.\n */\nimport { constants, existsSync } from \"node:fs\";\nimport { dirname, isAbsolute, join, resolve } from \"node:path\";\nimport { CONFIG_FILENAME, configPath } from \"../envelope/config.js\";\nimport { LOAM_VERSION } from \"../envelope/version.js\";\nimport { landscapePath, LIKEC4_PROJECT_FILENAME } from \"../repo/paths.js\";\nimport { docsRepoState } from \"../repo/state.js\";\nimport { listFeatures, listServices } from \"../repo/repo.js\";\nimport { LIKEC4_PROJECT_CONFIG } from \"../docs.js\";\nimport { gherkinRoot } from \"../gherkin/stamp.js\";\nimport { UnsafePathError } from \"../kernel/path-safety.js\";\nimport { scanWritePathResidue } from \"../staging/recovery/residue.js\";\nimport { type DoctorFinding, type DoctorReport } from \"./report.js\";\nimport { canAccess, inspectConfig, inspectLandscape } from \"./config.js\";\nimport { inspectAgentSurface } from \"./agents.js\";\nimport { gradeWritePathResidue } from \"./residue.js\";\n\nexport async function diagnose(cwd = process.cwd()): Promise<DoctorReport> {\n const inspected = await inspectConfig(cwd);\n const path = configPath(cwd);\n const findings: DoctorFinding[] = [];\n if (inspected.status === \"missing\") {\n findings.push({\n severity: \"blocker\",\n code: \"doctor.config-missing\",\n message: `No ${CONFIG_FILENAME} found at or above ${cwd}.`,\n fix: \"Run `loam init --docs <path-to-docs-repo>` here (add `--create` to make a new docs repo).\",\n });\n } else if (inspected.status === \"invalid\") {\n findings.push({\n severity: \"blocker\",\n code: \"doctor.config-invalid\",\n message: `Invalid ${CONFIG_FILENAME}: ${inspected.error ?? \"unknown error\"}`,\n fix: `Repair ${path} — or delete it and re-run \\`loam init\\`.`,\n });\n }\n\n const docsDir = inspected.config?.docsDir ?? null;\n const state = docsDir === null ? null : docsRepoState(docsDir);\n const exists = state !== null && state.kind !== \"missing\";\n const readable = exists && docsDir !== null ? await canAccess(docsDir, constants.R_OK) : false;\n const writable = exists && docsDir !== null ? await canAccess(docsDir, constants.W_OK) : false;\n const servicesDir = state?.kind === \"ok\";\n const landscapeFile = docsDir === null ? null : landscapePath(docsDir);\n const landscape = landscapeFile === null ? false : await canAccess(landscapeFile, constants.F_OK);\n\n // The docs-repo verdict comes from `docsRepoState`, the same function\n // `listServices` refuses on, so doctor cannot call a repo healthy that the\n // enumeration will not read.\n if (state?.kind === \"missing\") {\n findings.push({\n severity: \"blocker\",\n code: \"doctor.docs-missing\",\n message: `Configured docsDir is missing or is not a directory: ${docsDir}`,\n fix: `Fix \"docsDir\" in ${path}, clone the docs repo to ${docsDir}, or run \\`loam init --docs <dir> --create\\`.`,\n });\n } else if (state?.kind === \"no-services\") {\n findings.push({\n severity: \"blocker\",\n code: \"doctor.services-missing\",\n message: `Required services/ directory is missing under ${docsDir} — that path is not a docs repo.`,\n fix: `Point \"docsDir\" in ${path} at the shared docs repo, or run \\`loam init --docs ${docsDir} --create\\`.`,\n });\n }\n\n // An absolute docsDir in a committed config names a directory that exists on\n // exactly one machine. It is a warning and not a blocker because it works\n // perfectly — for the person who ran `loam init`, and for nobody who clones\n // the repo afterwards.\n const asWritten = inspected.config?.docsDirAsWritten;\n const configRoot = inspected.config?.root;\n if (\n asWritten !== undefined\n && configRoot !== undefined\n && isAbsolute(asWritten)\n && resolve(asWritten) !== resolve(configRoot)\n ) {\n findings.push({\n severity: \"warning\",\n code: \"doctor.docs-absolute\",\n message:\n `\"docsDir\" is stored as an absolute path (${asWritten}); ` +\n `${CONFIG_FILENAME} is committed, so it will not resolve on anyone else's machine.`,\n fix: `Rewrite \"docsDir\" in ${path} as a path relative to that file (e.g. \"../docs\").`,\n });\n }\n\n if (exists && !readable) {\n findings.push({\n severity: \"warning\",\n code: \"doctor.docs-unreadable\",\n message: `docsDir is not readable by this process: ${docsDir}`,\n fix: `Grant read access to ${docsDir} (check ownership and mode).`,\n });\n }\n if (exists && !writable) {\n findings.push({\n severity: \"warning\",\n code: \"doctor.docs-readonly\",\n message: `docsDir is not writable; read-only commands work, archive/init do not: ${docsDir}`,\n fix: `Grant write access to ${docsDir} if you need \\`loam new\\`, \\`loam adopt\\` or \\`loam archive\\`.`,\n });\n }\n if (exists && !landscape) {\n findings.push({\n severity: \"warning\",\n code: \"doctor.landscape-missing\",\n message: `${landscapeFile} is missing — nothing cross-service can be checked without it.`,\n fix: \"Create architecture/landscape.likec4 with one element per service and an edge per call (`loam init --create` writes the empty map).\",\n });\n } else if (exists && landscapeFile !== null && readable) {\n await inspectLandscape(landscapeFile, findings);\n }\n\n // The renderer's view of the repo, which is not loam's. loam parses every\n // `.likec4` file ALONE, so each one declares its own `specification` block and\n // re-declares whatever elements it names; LikeC4's own workspace loader merges\n // the whole tree into one model, and those declarations then collide. The\n // result was two tools disagreeing in the most confusing possible direction:\n // `loam validate --all` reported zero errors on a tree where `npx likec4\n // start` — the command loam's own brief recommends — refused to load at all.\n // `loam init --create` writes the project file that scopes the root project to\n // the landscape; a docs repo created before it exists has to be told, because\n // nothing else will ever mention it. The root file is also the gate on the\n // per-service ones: `loam subsystem sync` writes a create-only\n // likec4.config.json beside every service model — the LikeC4 project that\n // makes each model render from the same root, beside the fleet map — and it\n // writes none of them until this file exists, so a tree missing it renders\n // nothing from the docs root, not only the map. (Only a feature delta still renders from\n // its own directory; an archived feature would otherwise keep a dead project\n // forever.) The fix below says both, because this finding is the one place a\n // repo that predates either file is ever told. A warning, not a blocker: no\n // loam check reads this file, and a repo without it is only unrenderable.\n if (docsDir !== null && exists && readable && !existsSync(join(docsDir, LIKEC4_PROJECT_FILENAME))) {\n findings.push({\n severity: \"warning\",\n code: \"doctor.likec4-config-missing\",\n message:\n `${docsDir}/${LIKEC4_PROJECT_FILENAME} is missing, so this tree is not a loadable LikeC4 workspace — `\n + \"every service model and feature delta declares its own `specification` block, and pointing a \"\n + \"renderer at the repo root merges them into one model and reports every declaration as a duplicate.\",\n fix:\n `Write ${docsDir}/${LIKEC4_PROJECT_FILENAME} with:\\n${LIKEC4_PROJECT_CONFIG.trimEnd()}\\n`\n + \"That scopes the root project to architecture/, so `npx likec4 start` in the docs repo renders \"\n + \"the fleet map. Then run `loam subsystem sync`: it writes one create-only likec4.config.json \"\n + \"beside every service model, so each model renders from that same root as a project of its own \"\n + \"(the root project excludes services/**, and a model with no project file renders nowhere from the docs root). \"\n + \"Only a feature delta still renders from its own directory (`npx likec4 start features/<FEAT>`) — \"\n + \"being readable alone is what these files are for.\",\n });\n }\n\n // What a killed writer left behind. Read from `scanWritePathResidue`, whose\n // spellings of `.loam-lock` / `.loam-commit` / the temp-file pattern are\n // staging's own — doctor grades, it does not re-spell. This is the surface\n // that used to report `healthy: true` over half-merged docs.\n const writePath = docsDir !== null && exists && readable\n ? await scanWritePathResidue(docsDir)\n : null;\n if (docsDir !== null && writePath !== null) gradeWritePathResidue(docsDir, writePath, findings);\n\n // The one writer whose journal is NOT in the docs repo: `loam gherkin`\n // commits into the service repo's owned `<gherkinDir>/loam/` root, with the\n // same lock and journal files. When this repo IS a service repo, that root\n // gets the same scan and the same grades — the codes are the contract, the\n // prose says where — because \"doctor reports an interrupted commit until\n // recovery succeeds\" has to hold for every writer, not only the docs-repo\n // ones. Scanning it is cheap: the root is loam's own flat directory.\n let serviceWritePath = null;\n if (inspected.config?.service !== undefined && inspected.config.root !== undefined) {\n try {\n const emissionRoot = gherkinRoot(inspected.config.root, inspected.config.gherkinDir);\n if (existsSync(emissionRoot)) {\n serviceWritePath = await scanWritePathResidue(emissionRoot);\n gradeWritePathResidue(emissionRoot, serviceWritePath, findings);\n }\n } catch (err) {\n // An unsafe gherkinDir is `loam gherkin`'s refusal to make, with its own\n // sentence; doctor only declines to scan what it cannot trust.\n if (!(err instanceof UnsafePathError)) throw err;\n }\n }\n\n let services: Awaited<ReturnType<typeof listServices>> = [];\n let activeFeatures: Awaited<ReturnType<typeof listFeatures>> = [];\n // Whether the fleet was actually READ, which is a narrower question than\n // whether reading it was attempted — hence set inside the try and not from\n // the guard, so an enumeration that threw leaves this false alongside its own\n // `doctor.inventory-unreadable`.\n let inventoried = false;\n if (docsDir !== null && readable && servicesDir) {\n try {\n [services, activeFeatures] = await Promise.all([\n listServices(docsDir),\n listFeatures(docsDir),\n ]);\n inventoried = true;\n } catch (error) {\n findings.push({\n severity: \"warning\",\n code: \"doctor.inventory-unreadable\",\n message: `Could not inventory the docs repo: ${error instanceof Error ? error.message : String(error)}`,\n fix: `Check that ${docsDir}/services and ${docsDir}/features are readable directories.`,\n });\n }\n }\n\n const configuredService = inspected.config?.service ?? null;\n const currentService: DoctorReport[\"currentService\"] = configuredService === null\n ? { configured: null, status: \"unbound\" }\n : services.some((service) => service.id === configuredService)\n ? { configured: configuredService, status: \"matched\" }\n : { configured: configuredService, status: \"unknown\" };\n // Standing INSIDE the docs repo, an absent service binding is the correct\n // state, not a gap: the docs repo is the fleet, it is not any one service, and\n // the fix this finding prints — `loam init --service <id>` — would bind the\n // shared repo to one of the services it holds, which is not a thing loam has\n // a meaning for. doctor already knew where it was standing (it prints the\n // fleet count two lines up) and reported the warning anyway, so the first\n // preflight anyone ran in a freshly created docs repo came back yellow with\n // advice that made it worse. The docs repo's own loam.json is the one that\n // resolves `docsDir` to the directory holding it — that identity, not a\n // filename, is the test.\n const inDocsRepo = configRoot !== undefined && docsDir !== null\n && resolve(configRoot) === resolve(docsDir);\n if (currentService.status === \"unbound\" && !inDocsRepo) {\n findings.push({\n severity: \"warning\",\n code: \"doctor.service-unbound\",\n message: \"loam.json has no service binding; service-repo checks need `service`.\",\n fix: `Run \\`loam init --docs ${inspected.config?.docsDirAsWritten ?? \"<dir>\"} --service <id>\\` here.`,\n });\n } else if (currentService.status === \"unknown\" && inventoried) {\n // Only when there was an inventory to be absent from. This finding is a\n // claim about a fleet that was read; with no docsDir, an unreadable one, or\n // an enumeration that threw, `services` is empty for a reason that has\n // nothing to do with `service`, and doctor pointed the reader at the one\n // config field that was fine while the blocker it had already filed named\n // the real one. `currentService.status` still reports `unknown` — the\n // envelope describes what loam could and could not match, and an unread\n // fleet matches nothing.\n findings.push({\n severity: \"warning\",\n code: \"doctor.service-unknown\",\n message: `Configured service '${configuredService}' is not present under services/.`,\n // `--service <id>`, not a positional: `loam adopt` takes no argument, so\n // the spelling this finding used to print was refused by commander with\n // \"too many arguments\" — as the FIRST instruction a freshly bound service\n // repo ever receives. test/agent-commands-runnable.test.ts parses every\n // `loam …` loam prints against the real program so it cannot recur.\n fix: `Run \\`loam adopt --service ${configuredService}\\` to onboard it, or fix \"service\" in ${path}.`,\n });\n }\n\n // The slash commands (and everything else `init` lays down for a tool) belong\n // to the repo `init` ran in — the directory holding loam.json, which is not\n // the cwd now that config discovery walks upward.\n const agents = await inspectAgentSurface(dirname(path), inspected.config, docsDir, findings);\n\n return {\n healthy: !findings.some((finding) => finding.severity === \"blocker\"),\n runtime: {\n package: \"@ybotok/loam\",\n version: LOAM_VERSION,\n node: process.version,\n platform: process.platform,\n arch: process.arch,\n },\n config: { path, status: inspected.status, error: inspected.error },\n docs: {\n path: docsDir,\n exists,\n readable,\n writable,\n servicesDir,\n landscape,\n },\n counts: { services: services.length, activeFeatures: activeFeatures.length },\n currentService,\n agents,\n writePath,\n serviceWritePath,\n findings,\n };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"doctor.js","sourceRoot":"","sources":["../../../src/core/doctor/doctor.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The preflight: can loam run here at all, and can its answers be trusted?\n *\n * The walk only. Each question it asks lives in its own module — `./config.ts`\n * for the config and the fleet map, `./agents.ts` for the agent surface,\n * `./residue.ts` for what a killed writer left — and every one of them returns\n * findings rather than printing. That is the whole contract this module keeps:\n * `doctor` is the command that must be able to describe a broken repository\n * without becoming the next thing that breaks in it.\n */\nimport { constants, existsSync } from \"node:fs\";\nimport { dirname, isAbsolute, join, resolve } from \"node:path\";\nimport { CONFIG_FILENAME, configPath } from \"../envelope/config.js\";\nimport { LOAM_VERSION } from \"../envelope/version.js\";\nimport { landscapePath, LIKEC4_PROJECT_FILENAME } from \"../repo/paths.js\";\nimport { docsRepoState } from \"../repo/state.js\";\nimport { listFeatures, listServices } from \"../repo/repo.js\";\nimport { LIKEC4_PROJECT_CONFIG } from \"../docs.js\";\nimport { gherkinRoot } from \"../gherkin/stamp.js\";\nimport { UnsafePathError } from \"../kernel/path-safety.js\";\nimport { scanWritePathResidue } from \"../staging/recovery/residue.js\";\nimport { type DoctorFinding, type DoctorReport } from \"./report.js\";\nimport { canAccess, inspectConfig, inspectLandscape } from \"./config.js\";\nimport { inspectAgentSurface } from \"./agents.js\";\nimport { gradeWritePathResidue } from \"./residue.js\";\nimport { scanReports } from \"./reports/scan.js\";\n\nexport async function diagnose(cwd = process.cwd()): Promise<DoctorReport> {\n const inspected = await inspectConfig(cwd);\n const path = configPath(cwd);\n const findings: DoctorFinding[] = [];\n if (inspected.status === \"missing\") {\n findings.push({\n severity: \"blocker\",\n code: \"doctor.config-missing\",\n message: `No ${CONFIG_FILENAME} found at or above ${cwd}.`,\n fix: \"Run `loam init --docs <path-to-docs-repo>` here (add `--create` to make a new docs repo).\",\n });\n } else if (inspected.status === \"invalid\") {\n findings.push({\n severity: \"blocker\",\n code: \"doctor.config-invalid\",\n message: `Invalid ${CONFIG_FILENAME}: ${inspected.error ?? \"unknown error\"}`,\n fix: `Repair ${path} — or delete it and re-run \\`loam init\\`.`,\n });\n }\n\n const docsDir = inspected.config?.docsDir ?? null;\n const state = docsDir === null ? null : docsRepoState(docsDir);\n const exists = state !== null && state.kind !== \"missing\";\n const readable = exists && docsDir !== null ? await canAccess(docsDir, constants.R_OK) : false;\n const writable = exists && docsDir !== null ? await canAccess(docsDir, constants.W_OK) : false;\n const servicesDir = state?.kind === \"ok\";\n const landscapeFile = docsDir === null ? null : landscapePath(docsDir);\n const landscape = landscapeFile === null ? false : await canAccess(landscapeFile, constants.F_OK);\n\n // The docs-repo verdict comes from `docsRepoState`, the same function\n // `listServices` refuses on, so doctor cannot call a repo healthy that the\n // enumeration will not read.\n if (state?.kind === \"missing\") {\n findings.push({\n severity: \"blocker\",\n code: \"doctor.docs-missing\",\n message: `Configured docsDir is missing or is not a directory: ${docsDir}`,\n fix: `Fix \"docsDir\" in ${path}, clone the docs repo to ${docsDir}, or run \\`loam init --docs <dir> --create\\`.`,\n });\n } else if (state?.kind === \"no-services\") {\n findings.push({\n severity: \"blocker\",\n code: \"doctor.services-missing\",\n message: `Required services/ directory is missing under ${docsDir} — that path is not a docs repo.`,\n fix: `Point \"docsDir\" in ${path} at the shared docs repo, or run \\`loam init --docs ${docsDir} --create\\`.`,\n });\n }\n\n // An absolute docsDir in a committed config names a directory that exists on\n // exactly one machine. It is a warning and not a blocker because it works\n // perfectly — for the person who ran `loam init`, and for nobody who clones\n // the repo afterwards.\n const asWritten = inspected.config?.docsDirAsWritten;\n const configRoot = inspected.config?.root;\n if (\n asWritten !== undefined\n && configRoot !== undefined\n && isAbsolute(asWritten)\n && resolve(asWritten) !== resolve(configRoot)\n ) {\n findings.push({\n severity: \"warning\",\n code: \"doctor.docs-absolute\",\n message:\n `\"docsDir\" is stored as an absolute path (${asWritten}); ` +\n `${CONFIG_FILENAME} is committed, so it will not resolve on anyone else's machine.`,\n fix: `Rewrite \"docsDir\" in ${path} as a path relative to that file (e.g. \"../docs\").`,\n });\n }\n\n if (exists && !readable) {\n findings.push({\n severity: \"warning\",\n code: \"doctor.docs-unreadable\",\n message: `docsDir is not readable by this process: ${docsDir}`,\n fix: `Grant read access to ${docsDir} (check ownership and mode).`,\n });\n }\n if (exists && !writable) {\n findings.push({\n severity: \"warning\",\n code: \"doctor.docs-readonly\",\n message: `docsDir is not writable; read-only commands work, archive/init do not: ${docsDir}`,\n fix: `Grant write access to ${docsDir} if you need \\`loam new\\`, \\`loam adopt\\` or \\`loam archive\\`.`,\n });\n }\n if (exists && !landscape) {\n findings.push({\n severity: \"warning\",\n code: \"doctor.landscape-missing\",\n message: `${landscapeFile} is missing — nothing cross-service can be checked without it.`,\n fix: \"Create architecture/landscape.likec4 with one element per service and an edge per call (`loam init --create` writes the empty map).\",\n });\n } else if (exists && landscapeFile !== null && readable) {\n await inspectLandscape(landscapeFile, findings);\n }\n\n // The renderer's view of the repo, which is not loam's. loam parses every\n // `.likec4` file ALONE, so each one declares its own `specification` block and\n // re-declares whatever elements it names; LikeC4's own workspace loader merges\n // the whole tree into one model, and those declarations then collide. The\n // result was two tools disagreeing in the most confusing possible direction:\n // `loam validate --all` reported zero errors on a tree where `npx likec4\n // start` — the command loam's own brief recommends — refused to load at all.\n // `loam init --create` writes the project file that scopes the root project to\n // the landscape; a docs repo created before it exists has to be told, because\n // nothing else will ever mention it. The root file is also the gate on the\n // per-service ones: `loam subsystem sync` writes a create-only\n // likec4.config.json beside every service model — the LikeC4 project that\n // makes each model render from the same root, beside the fleet map — and it\n // writes none of them until this file exists, so a tree missing it renders\n // nothing from the docs root, not only the map. (Only a feature delta still renders from\n // its own directory; an archived feature would otherwise keep a dead project\n // forever.) The fix below says both, because this finding is the one place a\n // repo that predates either file is ever told. A warning, not a blocker: no\n // loam check reads this file, and a repo without it is only unrenderable.\n if (docsDir !== null && exists && readable && !existsSync(join(docsDir, LIKEC4_PROJECT_FILENAME))) {\n findings.push({\n severity: \"warning\",\n code: \"doctor.likec4-config-missing\",\n message:\n `${docsDir}/${LIKEC4_PROJECT_FILENAME} is missing, so this tree is not a loadable LikeC4 workspace — `\n + \"a feature delta, and any service model that declares its own `specification` block, has to be \"\n + \"excluded from the root project, and pointing a renderer at the repo root with nothing excluded \"\n + \"merges them into one model and reports every declaration as a duplicate.\",\n fix:\n `Write ${docsDir}/${LIKEC4_PROJECT_FILENAME} with:\\n${LIKEC4_PROJECT_CONFIG.trimEnd()}\\n`\n + \"That makes the docs root one LikeC4 project holding the fleet map and every service model that \"\n + \"extends it, so `npx likec4 start` in the docs repo renders both. Then run `loam subsystem sync`: \"\n + \"it maintains the `exclude` list above — one `services/<tree>/**` entry per model that stands \"\n + \"alone — and writes one create-only likec4.config.json beside each of those, so a standalone model \"\n + \"renders from the same root as a project of its own. Only a feature delta still renders from its \"\n + \"own directory (`npx likec4 start features/<FEAT>`) — being readable alone is what that file is for.\",\n });\n }\n\n // What a killed writer left behind. Read from `scanWritePathResidue`, whose\n // spellings of `.loam-lock` / `.loam-commit` / the temp-file pattern are\n // staging's own — doctor grades, it does not re-spell. This is the surface\n // that used to report `healthy: true` over half-merged docs.\n const writePath = docsDir !== null && exists && readable\n ? await scanWritePathResidue(docsDir)\n : null;\n if (docsDir !== null && writePath !== null) gradeWritePathResidue(docsDir, writePath, findings);\n\n // The one writer whose journal is NOT in the docs repo: `loam gherkin`\n // commits into the service repo's owned `<gherkinDir>/loam/` root, with the\n // same lock and journal files. When this repo IS a service repo, that root\n // gets the same scan and the same grades — the codes are the contract, the\n // prose says where — because \"doctor reports an interrupted commit until\n // recovery succeeds\" has to hold for every writer, not only the docs-repo\n // ones. Scanning it is cheap: the root is loam's own flat directory.\n let serviceWritePath = null;\n if (inspected.config?.service !== undefined && inspected.config.root !== undefined) {\n try {\n const emissionRoot = gherkinRoot(inspected.config.root, inspected.config.gherkinDir);\n if (existsSync(emissionRoot)) {\n serviceWritePath = await scanWritePathResidue(emissionRoot);\n gradeWritePathResidue(emissionRoot, serviceWritePath, findings);\n }\n } catch (err) {\n // An unsafe gherkinDir is `loam gherkin`'s refusal to make, with its own\n // sentence; doctor only declines to scan what it cannot trust.\n if (!(err instanceof UnsafePathError)) throw err;\n }\n }\n\n let services: Awaited<ReturnType<typeof listServices>> = [];\n let activeFeatures: Awaited<ReturnType<typeof listFeatures>> = [];\n // Whether the fleet was actually READ, which is a narrower question than\n // whether reading it was attempted — hence set inside the try and not from\n // the guard, so an enumeration that threw leaves this false alongside its own\n // `doctor.inventory-unreadable`.\n let inventoried = false;\n if (docsDir !== null && readable && servicesDir) {\n try {\n [services, activeFeatures] = await Promise.all([\n listServices(docsDir),\n listFeatures(docsDir),\n ]);\n inventoried = true;\n } catch (error) {\n findings.push({\n severity: \"warning\",\n code: \"doctor.inventory-unreadable\",\n message: `Could not inventory the docs repo: ${error instanceof Error ? error.message : String(error)}`,\n fix: `Check that ${docsDir}/services and ${docsDir}/features are readable directories.`,\n });\n }\n }\n\n const configuredService = inspected.config?.service ?? null;\n const currentService: DoctorReport[\"currentService\"] = configuredService === null\n ? { configured: null, status: \"unbound\" }\n : services.some((service) => service.id === configuredService)\n ? { configured: configuredService, status: \"matched\" }\n : { configured: configuredService, status: \"unknown\" };\n // Standing INSIDE the docs repo, an absent service binding is the correct\n // state, not a gap: the docs repo is the fleet, it is not any one service, and\n // the fix this finding prints — `loam init --service <id>` — would bind the\n // shared repo to one of the services it holds, which is not a thing loam has\n // a meaning for. doctor already knew where it was standing (it prints the\n // fleet count two lines up) and reported the warning anyway, so the first\n // preflight anyone ran in a freshly created docs repo came back yellow with\n // advice that made it worse. The docs repo's own loam.json is the one that\n // resolves `docsDir` to the directory holding it — that identity, not a\n // filename, is the test.\n const inDocsRepo = configRoot !== undefined && docsDir !== null\n && resolve(configRoot) === resolve(docsDir);\n if (currentService.status === \"unbound\" && !inDocsRepo) {\n findings.push({\n severity: \"warning\",\n code: \"doctor.service-unbound\",\n message: \"loam.json has no service binding; service-repo checks need `service`.\",\n fix: `Run \\`loam init --docs ${inspected.config?.docsDirAsWritten ?? \"<dir>\"} --service <id>\\` here.`,\n });\n } else if (currentService.status === \"unknown\" && inventoried) {\n // Only when there was an inventory to be absent from. This finding is a\n // claim about a fleet that was read; with no docsDir, an unreadable one, or\n // an enumeration that threw, `services` is empty for a reason that has\n // nothing to do with `service`, and doctor pointed the reader at the one\n // config field that was fine while the blocker it had already filed named\n // the real one. `currentService.status` still reports `unknown` — the\n // envelope describes what loam could and could not match, and an unread\n // fleet matches nothing.\n findings.push({\n severity: \"warning\",\n code: \"doctor.service-unknown\",\n message: `Configured service '${configuredService}' is not present under services/.`,\n // `--service <id>`, not a positional: `loam adopt` takes no argument, so\n // the spelling this finding used to print was refused by commander with\n // \"too many arguments\" — as the FIRST instruction a freshly bound service\n // repo ever receives. test/agent-commands-runnable.test.ts parses every\n // `loam …` loam prints against the real program so it cannot recur.\n fix: `Run \\`loam adopt --service ${configuredService}\\` to onboard it, or fix \"service\" in ${path}.`,\n });\n }\n\n // The slash commands (and everything else `init` lays down for a tool) belong\n // to the repo `init` ran in — the directory holding loam.json, which is not\n // the cwd now that config discovery walks upward.\n const agents = await inspectAgentSurface(dirname(path), inspected.config, docsDir, findings);\n\n // The problem reports, read beside the config that RESOLVED — `config.root`,\n // the directory the loam.json was found in — because the `loam-report`\n // protocol says \"at the current repository root\", and config discovery walks\n // upward, so the cwd is not that root.\n //\n // Where nothing PARSED, the directory of the file loam FOUND, which is what\n // `dirname(path)` is: `configPath` returns the discovered loam.json, or the\n // cwd's own spelling of it when there is none, so this is the found root when\n // there is one and the cwd when there is not — the same root the agent surface\n // above is inspected against. Falling back to the raw cwd instead meant that\n // from a subdirectory of a repo whose loam.json is unparseable, `doctor`\n // reported `total 0, next 001` while five reports sat beside the config it had\n // just named in the same envelope, and handed out an ordinal already taken\n // (verification 2026-09-04, second pass). A report corpus in a repo whose\n // loam.json is broken is still worth counting — this is precisely the preflight\n // that runs when the config does not resolve — and the blocker above already\n // names the real problem.\n //\n // No findings come back, and that is the design: this is the one directory\n // loam reads that loam never writes, its contents are the team's, and an open\n // report is not a defect in the repository holding it. `doctor` reports it as\n // state — like `docs`, `counts` and the write path — so the corpus is\n // countable by the protocol that asks for it (`reports.next` is the ordinal\n // the next report takes) without ever becoming a complaint.\n const reports = await scanReports(inspected.config?.root ?? dirname(path));\n\n return {\n healthy: !findings.some((finding) => finding.severity === \"blocker\"),\n runtime: {\n package: \"@ybotok/loam\",\n version: LOAM_VERSION,\n node: process.version,\n platform: process.platform,\n arch: process.arch,\n },\n config: { path, status: inspected.status, error: inspected.error },\n docs: {\n path: docsDir,\n exists,\n readable,\n writable,\n servicesDir,\n landscape,\n },\n counts: { services: services.length, activeFeatures: activeFeatures.length },\n currentService,\n agents,\n reports,\n writePath,\n serviceWritePath,\n findings,\n };\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"report.js","sourceRoot":"","sources":["../../../src/core/doctor/report.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,EAAyB,MAAM,gCAAgC,CAAC","sourcesContent":["/**\n * What a diagnosis IS: the finding shape, the report shape, and the agent\n * surface inside it.\n *\n * A module of its own because five checks fill this in and the command prints\n * it, so every one of them agrees about what a blocker means here — something\n * that stops loam running at all, as against a warning, which is something a\n * person should know before trusting an answer. That distinction is the whole\n * value of `doctor`, and it lives in the type rather than in five checks'\n * judgement.\n */\nimport { type WritePathResidue } from \"../staging/recovery/residue.js\";\n\nexport type DoctorSeverity = \"blocker\" | \"warning\";\n\nexport interface DoctorFinding {\n severity: DoctorSeverity;\n code: string;\n message: string;\n /**\n * The next command or edit that clears this finding. Mandatory, not optional:\n * a diagnostic that names a problem without naming its fix is a diagnostic\n * the reader has to research, and `doctor` is what someone runs precisely\n * because they do not yet know what loam wants from them.\n */\n fix: string;\n}\n\nexport interface DoctorReport {\n healthy: boolean;\n runtime: {\n package: \"@ybotok/loam\";\n version: string;\n node: string;\n platform: NodeJS.Platform;\n arch: string;\n };\n config: {\n path: string;\n status: \"missing\" | \"invalid\" | \"valid\";\n error: string | null;\n };\n docs: {\n path: string | null;\n exists: boolean;\n readable: boolean;\n writable: boolean;\n servicesDir: boolean;\n landscape: boolean;\n };\n counts: { services: number; activeFeatures: number };\n currentService: {\n configured: string | null;\n status: \"unbound\" | \"matched\" | \"unknown\";\n };\n /** The agent surface of this repo — see `inspectAgentSurface`. */\n agents: AgentSurface;\n /**\n * What a write that did not finish left in the docs repo, or null when\n * `docsDir` never resolved. Reported as state, like `docs` and `counts`: a\n * lock somebody is legitimately holding right now is a fact, not a complaint.\n */\n writePath: WritePathResidue | null;\n /**\n * The same scan over the service repo's owned `<gherkinDir>/loam/` root —\n * the one writer whose journal is not in the docs repo. Null when this repo\n * is not a service repo, or the root does not exist. Additive: a consumer\n * of the original envelope never sees a key it did not ask about removed.\n */\n serviceWritePath: WritePathResidue | null;\n findings: DoctorFinding[];\n}\n\nexport interface AgentSurface {\n /** Tool ids this repo has an agent surface for. Empty is legal (`init --no-commands`). */\n tools: string[];\n /** Workflow subset managed for those tools. */\n profile: \"full\" | \"service\" | \"docs\";\n /** Whether `tools` came from loam.json's own record or from what is on disk. */\n toolsSource: \"config\" | \"disk\";\n /** How many files the running binary would lay down for `tools`. */\n plannedFiles: number;\n /** Repo-relative paths of those files that are not there. */\n missingFiles: string[];\n /**\n * Repo-relative paths of the ones that ARE there carrying no version stamp,\n * or one older than this binary — see `staleAgentFiles`.\n */\n staleFiles: string[];\n stamp: {\n /** Where the docs repo's AGENTS.md is, or null when `docsDir` did not resolve. */\n path: string | null;\n /** Whether it could be read at all — absent and unstamped are different facts. */\n present: boolean;\n /** The version the stamp names, null when there is none (or an unparseable one). */\n version: string | null;\n /** Whether `agents.stale` fired: the stamp is missing or trails this binary. */\n stale: boolean;\n };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"report.js","sourceRoot":"","sources":["../../../src/core/doctor/report.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,EAAyB,MAAM,gCAAgC,CAAC","sourcesContent":["/**\n * What a diagnosis IS: the finding shape, the report shape, and the agent\n * surface inside it.\n *\n * A module of its own because five checks fill this in and the command prints\n * it, so every one of them agrees about what a blocker means here — something\n * that stops loam running at all, as against a warning, which is something a\n * person should know before trusting an answer. That distinction is the whole\n * value of `doctor`, and it lives in the type rather than in five checks'\n * judgement.\n */\nimport { type WritePathResidue } from \"../staging/recovery/residue.js\";\n\nexport type DoctorSeverity = \"blocker\" | \"warning\";\n\nexport interface DoctorFinding {\n severity: DoctorSeverity;\n code: string;\n message: string;\n /**\n * The next command or edit that clears this finding. Mandatory, not optional:\n * a diagnostic that names a problem without naming its fix is a diagnostic\n * the reader has to research, and `doctor` is what someone runs precisely\n * because they do not yet know what loam wants from them.\n */\n fix: string;\n}\n\nexport interface DoctorReport {\n healthy: boolean;\n runtime: {\n package: \"@ybotok/loam\";\n version: string;\n node: string;\n platform: NodeJS.Platform;\n arch: string;\n };\n config: {\n path: string;\n status: \"missing\" | \"invalid\" | \"valid\";\n error: string | null;\n };\n docs: {\n path: string | null;\n exists: boolean;\n readable: boolean;\n writable: boolean;\n servicesDir: boolean;\n landscape: boolean;\n };\n counts: { services: number; activeFeatures: number };\n currentService: {\n configured: string | null;\n status: \"unbound\" | \"matched\" | \"unknown\";\n };\n /** The agent surface of this repo — see `inspectAgentSurface`. */\n agents: AgentSurface;\n /**\n * The problem reports this repo has collected — see `scanReports`. State,\n * like `docs` and `counts`, and never a finding: a repository with eleven\n * open reports is not a broken repository, it is one that has been paying\n * attention. `healthy` does not move for anything in here.\n */\n reports: ProblemReports;\n /**\n * What a write that did not finish left in the docs repo, or null when\n * `docsDir` never resolved. Reported as state, like `docs` and `counts`: a\n * lock somebody is legitimately holding right now is a fact, not a complaint.\n */\n writePath: WritePathResidue | null;\n /**\n * The same scan over the service repo's owned `<gherkinDir>/loam/` root —\n * the one writer whose journal is not in the docs repo. Null when this repo\n * is not a service repo, or the root does not exist. Additive: a consumer\n * of the original envelope never sees a key it did not ask about removed.\n */\n serviceWritePath: WritePathResidue | null;\n findings: DoctorFinding[];\n}\n\n/**\n * Where one problem report got to, as its own `Status:` line says.\n *\n * `fixed` and `superseded` are the FIRST WORD of a longer line (`fixed in\n * 0.2.0-alpha.5`, `superseded by 010`), because the rest of that line is prose\n * an author writes for a reader and loam has no business parsing. `unstated`\n * covers both a report written before the field existed and one whose value is\n * a word this vocabulary does not know — loam counts what it can read and says\n * so, rather than inventing a sixth bucket for every synonym somebody types.\n */\nexport type ProblemReportStatus = \"open\" | \"sent\" | \"fixed\" | \"superseded\" | \"unstated\";\n\nexport interface ProblemReportEntry {\n /** The file's own name inside the reports directory — `012-2026-09-03-slug.md`. */\n file: string;\n /**\n * The leading zero-padded ordinal the protocol now asks for, or null for a\n * name written before it did (`2026-09-03-slug.md`). A report with no ordinal\n * is still a report: this directory predates the numbering.\n */\n ordinal: string | null;\n status: ProblemReportStatus;\n}\n\n/** The report corpus a repository has collected — see `scanReports`. */\nexport interface ProblemReports {\n /**\n * The ABSOLUTE path of the directory — `loam-reports/` beside the `loam.json`\n * that resolved, else beside the directory `doctor` ran in. Absolute because\n * the protocol tells an agent to write there, and a repo-relative spelling\n * left it to guess which repository \"relative\" meant: `doctor` is the\n * preflight that runs precisely when the config may not resolve, and in a\n * service repository the config's root is that repository, not the docs one.\n */\n dir: string;\n /** Whether that directory exists at all. A repo with no reports is normal. */\n present: boolean;\n total: number;\n /**\n * The ordinal the next report takes: one more than the highest present,\n * padded to at least three digits, `001` over an empty or absent directory —\n * it WIDENS past three rather than reusing a number once a corpus gets\n * there, which is what every page saying \"at least three digits\" describes.\n * This is the field the `loam-report` protocol tells an agent to read.\n */\n next: string;\n entries: ProblemReportEntry[];\n}\n\nexport interface AgentSurface {\n /** Tool ids this repo has an agent surface for. Empty is legal (`init --no-commands`). */\n tools: string[];\n /** Workflow subset managed for those tools. */\n profile: \"full\" | \"service\" | \"docs\";\n /** Whether `tools` came from loam.json's own record or from what is on disk. */\n toolsSource: \"config\" | \"disk\";\n /** How many files the running binary would lay down for `tools`. */\n plannedFiles: number;\n /** Repo-relative paths of those files that are not there. */\n missingFiles: string[];\n /**\n * Repo-relative paths of the ones that ARE there carrying no version stamp,\n * or one older than this binary — see `staleAgentFiles`.\n */\n staleFiles: string[];\n stamp: {\n /** Where the docs repo's AGENTS.md is, or null when `docsDir` did not resolve. */\n path: string | null;\n /** Whether it could be read at all — absent and unstamped are different facts. */\n present: boolean;\n /** The version the stamp names, null when there is none (or an unparseable one). */\n version: string | null;\n /** Whether `agents.stale` fired: the stamp is missing or trails this binary. */\n stale: boolean;\n };\n}\n"]}
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The problem reports a repository has collected: how many there are, what
|
|
3
|
+
* state each one is in, and which ordinal the next one takes.
|
|
4
|
+
*
|
|
5
|
+
* A sub-package because `doctor/` is at its five-file limit, and a scan of its
|
|
6
|
+
* own because this is the one directory loam reads that loam never writes. The
|
|
7
|
+
* `loam-report` protocol has always asked a repository to accumulate a corpus
|
|
8
|
+
* under `loam-reports/` and then never looked at it — eleven reports landed in
|
|
9
|
+
* this repository in two days, three of them already closed, and every bit of
|
|
10
|
+
* that state lived outside the files, in whoever remembered. The directory is
|
|
11
|
+
* loam's own convention and its contents are loam's own format, so counting
|
|
12
|
+
* them is a fact loam can read as cheaply as it reads `agentFiles`. Reading is
|
|
13
|
+
* the whole of it: nothing here transmits anything, which is the constraint the
|
|
14
|
+
* protocol is built around.
|
|
15
|
+
*
|
|
16
|
+
* Everything below is best-effort on purpose. A report is a hand-written
|
|
17
|
+
* Markdown file, so an absent directory, an unreadable one, a name written
|
|
18
|
+
* before the numbering existed and a file with no `Status:` line are all
|
|
19
|
+
* ordinary states of a healthy repository. None of them is a finding, and
|
|
20
|
+
* `doctor`'s `healthy` does not move for anything in here — a repo with eleven
|
|
21
|
+
* open reports is not broken, it is one that has been paying attention.
|
|
22
|
+
*/
|
|
23
|
+
import { readdir, readFile } from "node:fs/promises";
|
|
24
|
+
import { join } from "node:path";
|
|
25
|
+
import {} from "../report.js";
|
|
26
|
+
/**
|
|
27
|
+
* The conventional NAME of the directory the `loam-report` protocol asks a
|
|
28
|
+
* repository to accumulate, spelled once — the segment `scanReports` joins onto
|
|
29
|
+
* the resolved root to reach it.
|
|
30
|
+
*
|
|
31
|
+
* A name rather than a path, because two readers want different things out of
|
|
32
|
+
* it and both are right: `ProblemReports.dir` carries the joined ABSOLUTE
|
|
33
|
+
* location, because the protocol tells an agent to write there and a relative
|
|
34
|
+
* spelling leaves it guessing which repository "relative" meant (`../report.ts`
|
|
35
|
+
* records that), while `commands/doctor.ts`'s human row prints this constant, so
|
|
36
|
+
* a person standing in the repository reads `loam-reports/` — what the protocol,
|
|
37
|
+
* the reports themselves and every page that mentions them say.
|
|
38
|
+
*/
|
|
39
|
+
export const REPORTS_DIR = "loam-reports";
|
|
40
|
+
/**
|
|
41
|
+
* The leading ordinal of `012-2026-09-03-slug.md`. Three digits or more, so the
|
|
42
|
+
* thousandth report widens the field instead of losing its number.
|
|
43
|
+
*/
|
|
44
|
+
const ORDINAL = /^(\d{3,})-/;
|
|
45
|
+
/**
|
|
46
|
+
* A name written before the numbering existed: `2026-09-03-slug.md`, the shape
|
|
47
|
+
* eleven reports in this repository already carry.
|
|
48
|
+
*
|
|
49
|
+
* Checked FIRST, and this is the whole reason it exists as its own pattern: a
|
|
50
|
+
* four-digit year satisfies `ORDINAL` perfectly, so a legacy name read as an
|
|
51
|
+
* ordinal is report number 2026 — and the next report loam hands out is then
|
|
52
|
+
* 2027, in a directory whose highest real ordinal is 3. A legacy name is still
|
|
53
|
+
* a report; it just has no number, and contributes none.
|
|
54
|
+
*/
|
|
55
|
+
const LEGACY_DATE_NAME = /^\d{4}-\d{2}-\d{2}-/;
|
|
56
|
+
/** The ordinal a report's file name claims, or null when the name predates numbering. */
|
|
57
|
+
function ordinalOf(file) {
|
|
58
|
+
if (LEGACY_DATE_NAME.test(file))
|
|
59
|
+
return null;
|
|
60
|
+
return ORDINAL.exec(file)?.[1] ?? null;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* The template's `- Status:` line, matched in the header FIELD BLOCK alone (see
|
|
64
|
+
* `headerFields`). Only the first word is captured: the vocabulary's two
|
|
65
|
+
* composite values carry prose after it (`fixed in 0.2.0-alpha.5`,
|
|
66
|
+
* `superseded by 010`) that an author writes for a reader, and parsing further
|
|
67
|
+
* would make loam an authority on a sentence it does not own.
|
|
68
|
+
*/
|
|
69
|
+
const STATUS_LINE = /^[ \t]*-[ \t]*Status:[ \t]*(\S+)/m;
|
|
70
|
+
/** A fenced block's delimiter line, either spelling. */
|
|
71
|
+
const FENCE = /^[ \t]*(`{3,}|~{3,})/;
|
|
72
|
+
/**
|
|
73
|
+
* An indented code block's line — the OTHER way markdown quotes code, and the
|
|
74
|
+
* way `loam instructions loam-report` prints its own template (the four-space
|
|
75
|
+
* block in ../../agent/support/report.ts). A fence-only reader counted such a
|
|
76
|
+
* report open off the template's `- Status: open`.
|
|
77
|
+
*/
|
|
78
|
+
const INDENTED = /^(?: {4}|\t)/;
|
|
79
|
+
/** A section heading — the line that ends the header field block. */
|
|
80
|
+
const SECTION = /^##[ \t]/;
|
|
81
|
+
/**
|
|
82
|
+
* The report's header field block: every line above the first `## ` heading,
|
|
83
|
+
* with quoted code — fenced or indented — dropped.
|
|
84
|
+
*
|
|
85
|
+
* Matched anywhere in the file, `- Status:` convicted the wrong reports twice
|
|
86
|
+
* over. A report that QUOTES the protocol template — which several do, because
|
|
87
|
+
* an agent reporting that the protocol is wrong pastes the protocol — carries a
|
|
88
|
+
* `- Status: open` inside a fence and was counted open however its own header
|
|
89
|
+
* read; and a report whose body discusses a status line later on was read the
|
|
90
|
+
* same way. The status a report claims is a header FIELD, written beside
|
|
91
|
+
* `- Classification:` above the first section, so that is the only place it is
|
|
92
|
+
* read from. An unterminated fence swallows the rest of the file rather than
|
|
93
|
+
* flipping back — the safe direction, since the alternative is reading code as
|
|
94
|
+
* a field.
|
|
95
|
+
*
|
|
96
|
+
* Two shapes defeated the first version of that, and both are the same quoting
|
|
97
|
+
* case: a NESTED fence, where the outer ```` opens and the inner ``` was taken
|
|
98
|
+
* for its close, leaving the quoted block read as fields; and an INDENTED
|
|
99
|
+
* block, which the protocol itself uses and whose indented `## Summary` never
|
|
100
|
+
* ends the header either. So a fence is closed only by a run of the same
|
|
101
|
+
* character at least as long as the one that opened it, and an indented block
|
|
102
|
+
* (four spaces after a blank line, through the blank lines inside it) is
|
|
103
|
+
* dropped whole.
|
|
104
|
+
*/
|
|
105
|
+
function headerFields(text) {
|
|
106
|
+
const kept = [];
|
|
107
|
+
let fence = null;
|
|
108
|
+
let indented = false;
|
|
109
|
+
let afterBlank = true;
|
|
110
|
+
for (const line of text.split(/\r?\n/)) {
|
|
111
|
+
if (fence !== null) {
|
|
112
|
+
const close = FENCE.exec(line)?.[1];
|
|
113
|
+
if (close !== undefined && close[0] === fence[0] && close.length >= fence.length)
|
|
114
|
+
fence = null;
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
const blank = line.trim() === "";
|
|
118
|
+
if (indented) {
|
|
119
|
+
if (blank || INDENTED.test(line))
|
|
120
|
+
continue;
|
|
121
|
+
indented = false;
|
|
122
|
+
}
|
|
123
|
+
const open = FENCE.exec(line)?.[1];
|
|
124
|
+
if (open !== undefined) {
|
|
125
|
+
fence = open;
|
|
126
|
+
afterBlank = false;
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
if (afterBlank && INDENTED.test(line)) {
|
|
130
|
+
indented = true;
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
if (SECTION.test(line))
|
|
134
|
+
break;
|
|
135
|
+
kept.push(line);
|
|
136
|
+
afterBlank = blank;
|
|
137
|
+
}
|
|
138
|
+
return kept.join("\n");
|
|
139
|
+
}
|
|
140
|
+
/** The words a `Status:` line may say that loam can count. */
|
|
141
|
+
const STATED = ["open", "sent", "fixed", "superseded"];
|
|
142
|
+
/**
|
|
143
|
+
* The ordinal the next report takes: one past the highest present, never one
|
|
144
|
+
* past the count.
|
|
145
|
+
*
|
|
146
|
+
* Those two differ exactly when the directory has a gap or a legacy name in it
|
|
147
|
+
* — which this directory does, since the numbering arrived after eleven
|
|
148
|
+
* reports had already been written — and counting instead of maximising is how
|
|
149
|
+
* a next ordinal starts handing out numbers that are already taken.
|
|
150
|
+
*/
|
|
151
|
+
function nextOrdinal(entries) {
|
|
152
|
+
let highest = 0;
|
|
153
|
+
for (const entry of entries) {
|
|
154
|
+
if (entry.ordinal === null)
|
|
155
|
+
continue;
|
|
156
|
+
const value = Number.parseInt(entry.ordinal, 10);
|
|
157
|
+
if (value > highest)
|
|
158
|
+
highest = value;
|
|
159
|
+
}
|
|
160
|
+
return String(highest + 1).padStart(3, "0");
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* What one report's own `Status:` line says, or `unstated`.
|
|
164
|
+
*
|
|
165
|
+
* Unreadable reads as unstated rather than throwing: a report saved as UTF-16,
|
|
166
|
+
* a file the process cannot open, or a directory somebody named `notes.md` are
|
|
167
|
+
* all things a diagnostic must survive. `doctor` is the command that has to be
|
|
168
|
+
* able to describe a broken repository without becoming the next thing that
|
|
169
|
+
* breaks in it.
|
|
170
|
+
*/
|
|
171
|
+
async function statusOf(path) {
|
|
172
|
+
let text;
|
|
173
|
+
try {
|
|
174
|
+
text = await readFile(path, "utf8");
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
// Unreadable is not a state the file claims — it is a state of the read.
|
|
178
|
+
return "unstated";
|
|
179
|
+
}
|
|
180
|
+
const word = STATUS_LINE.exec(headerFields(text))?.[1]?.toLowerCase();
|
|
181
|
+
return STATED.find((known) => known === word) ?? "unstated";
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Read `<root>/loam-reports/`, where `root` is the directory holding the
|
|
185
|
+
* `loam.json` that resolved — the repository the reports were written at the
|
|
186
|
+
* root of — falling back, when nothing parsed, to the directory of the
|
|
187
|
+
* `loam.json` loam FOUND (`doctor.ts` says why that and not the cwd), and to the
|
|
188
|
+
* run directory only when there is no config file anywhere above it.
|
|
189
|
+
*
|
|
190
|
+
* Every `*.md` in the directory is an entry, including a hand-written index
|
|
191
|
+
* page: which files in there are "really" reports is a second naming
|
|
192
|
+
* convention, and inventing one would make loam disagree with a directory
|
|
193
|
+
* listing about what the directory holds. An unnumbered entry contributes
|
|
194
|
+
* nothing to `next`, so an index costs the corpus nothing either.
|
|
195
|
+
*/
|
|
196
|
+
export async function scanReports(root) {
|
|
197
|
+
const dir = join(root, REPORTS_DIR);
|
|
198
|
+
let names;
|
|
199
|
+
try {
|
|
200
|
+
names = (await readdir(dir)).filter((name) => name.toLowerCase().endsWith(".md"));
|
|
201
|
+
}
|
|
202
|
+
catch (error) {
|
|
203
|
+
// Absent is the state of every repository that has never had to write a
|
|
204
|
+
// report, and it is the common one — `present: false`, nothing to say. Any
|
|
205
|
+
// other errno means the directory IS there and this process could not list
|
|
206
|
+
// it, which is a different sentence: the corpus exists and loam cannot see
|
|
207
|
+
// into it, so `present` stays true and the counts stay honestly empty.
|
|
208
|
+
const code = error.code;
|
|
209
|
+
const present = code !== "ENOENT" && code !== "ENOTDIR";
|
|
210
|
+
return { dir, present, total: 0, next: "001", entries: [] };
|
|
211
|
+
}
|
|
212
|
+
// Sorted here rather than left in readdir order, which is the filesystem's and
|
|
213
|
+
// differs between one machine and another — and sorted by the ORDINAL the name
|
|
214
|
+
// claims, numerically, because report order is what a reader scanning the list
|
|
215
|
+
// expects and byte order stops being it at exactly the point the widening rule
|
|
216
|
+
// was written for: `1000-…` sorts before `998-…`, so a directory that has
|
|
217
|
+
// passed its thousandth report reads 1000, 1001, 998, 999 (verification
|
|
218
|
+
// 2026-09-04, second pass). Unnumbered names — the shape reports carried before
|
|
219
|
+
// the numbering existed — contribute no ordinal, so they sort last among
|
|
220
|
+
// themselves by name, where a reader looking for "the next one" is not looking.
|
|
221
|
+
const rank = (file) => {
|
|
222
|
+
const ordinal = ordinalOf(file);
|
|
223
|
+
return ordinal === null ? Number.POSITIVE_INFINITY : Number.parseInt(ordinal, 10);
|
|
224
|
+
};
|
|
225
|
+
names.sort((a, b) => rank(a) - rank(b) || (a < b ? -1 : a > b ? 1 : 0));
|
|
226
|
+
// Read together, like `staleAgentFiles` does: a report corpus is a flat
|
|
227
|
+
// directory of small hand-written files, so there is nothing here worth a
|
|
228
|
+
// concurrency cap, and `Promise.all` preserves the order just established.
|
|
229
|
+
const entries = await Promise.all(names.map(async (file) => ({
|
|
230
|
+
file,
|
|
231
|
+
ordinal: ordinalOf(file),
|
|
232
|
+
status: await statusOf(join(dir, file)),
|
|
233
|
+
})));
|
|
234
|
+
return {
|
|
235
|
+
dir,
|
|
236
|
+
present: true,
|
|
237
|
+
total: entries.length,
|
|
238
|
+
next: nextOrdinal(entries),
|
|
239
|
+
entries,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
//# sourceMappingURL=scan.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scan.js","sourceRoot":"","sources":["../../../../src/core/doctor/reports/scan.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AACrD,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAIN,MAAM,cAAc,CAAC;AAEtB;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG,cAAc,CAAC;AAE1C;;;GAGG;AACH,MAAM,OAAO,GAAG,YAAY,CAAC;AAE7B;;;;;;;;;GASG;AACH,MAAM,gBAAgB,GAAG,qBAAqB,CAAC;AAE/C,yFAAyF;AACzF,SAAS,SAAS,CAAC,IAAY;IAC7B,IAAI,gBAAgB,CAAC,IAAI,CAAC,IAAI,CAAC;QAAE,OAAO,IAAI,CAAC;IAC7C,OAAO,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC;AACzC,CAAC;AAED;;;;;;GAMG;AACH,MAAM,WAAW,GAAG,mCAAmC,CAAC;AAExD,wDAAwD;AACxD,MAAM,KAAK,GAAG,sBAAsB,CAAC;AAErC;;;;;GAKG;AACH,MAAM,QAAQ,GAAG,cAAc,CAAC;AAEhC,qEAAqE;AACrE,MAAM,OAAO,GAAG,UAAU,CAAC;AAE3B;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,SAAS,YAAY,CAAC,IAAY;IAChC,MAAM,IAAI,GAAa,EAAE,CAAC;IAC1B,IAAI,KAAK,GAAkB,IAAI,CAAC;IAChC,IAAI,QAAQ,GAAG,KAAK,CAAC;IACrB,IAAI,UAAU,GAAG,IAAI,CAAC;IACtB,KAAK,MAAM,IAAI,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,EAAE,CAAC;QACvC,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;YACnB,MAAM,KAAK,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;YACpC,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,CAAC,CAAC,CAAC,KAAK,KAAK,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,MAAM,IAAI,KAAK,CAAC,MAAM;gBAAE,KAAK,GAAG,IAAI,CAAC;YAC/F,SAAS;QACX,CAAC;QACD,MAAM,KAAK,GAAG,IAAI,CAAC,IAAI,EAAE,KAAK,EAAE,CAAC;QACjC,IAAI,QAAQ,EAAE,CAAC;YACb,IAAI,KAAK,IAAI,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC;gBAAE,SAAS;YAC3C,QAAQ,GAAG,KAAK,CAAC;QACnB,CAAC;QACD,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;QACnC,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;YACvB,KAAK,GAAG,IAAI,CAAC;YACb,UAAU,GAAG,KAAK,CAAC;YACnB,SAAS;QACX,CAAC;QACD,IAAI,UAAU,IAAI,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;YACtC,QAAQ,GAAG,IAAI,CAAC;YAChB,SAAS;QACX,CAAC;QACD,IAAI,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC;YAAE,MAAM;QAC9B,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAChB,UAAU,GAAG,KAAK,CAAC;IACrB,CAAC;IACD,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACzB,CAAC;AAED,8DAA8D;AAC9D,MAAM,MAAM,GAAmC,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,YAAY,CAAC,CAAC;AAEvF;;;;;;;;GAQG;AACH,SAAS,WAAW,CAAC,OAAsC;IACzD,IAAI,OAAO,GAAG,CAAC,CAAC;IAChB,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;QAC5B,IAAI,KAAK,CAAC,OAAO,KAAK,IAAI;YAAE,SAAS;QACrC,MAAM,KAAK,GAAG,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;QACjD,IAAI,KAAK,GAAG,OAAO;YAAE,OAAO,GAAG,KAAK,CAAC;IACvC,CAAC;IACD,OAAO,MAAM,CAAC,OAAO,GAAG,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;AAC9C,CAAC;AAED;;;;;;;;GAQG;AACH,KAAK,UAAU,QAAQ,CAAC,IAAY;IAClC,IAAI,IAAY,CAAC;IACjB,IAAI,CAAC;QACH,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IACtC,CAAC;IAAC,MAAM,CAAC;QACP,yEAAyE;QACzE,OAAO,UAAU,CAAC;IACpB,CAAC;IACD,MAAM,IAAI,GAAG,WAAW,CAAC,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,CAAC;IACtE,OAAO,MAAM,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,KAAK,IAAI,CAAC,IAAI,UAAU,CAAC;AAC9D,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,IAAY;IAC5C,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,EAAE,WAAW,CAAC,CAAC;IACpC,IAAI,KAAe,CAAC;IACpB,IAAI,CAAC;QACH,KAAK,GAAG,CAAC,MAAM,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC;IACpF,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,wEAAwE;QACxE,2EAA2E;QAC3E,2EAA2E;QAC3E,2EAA2E;QAC3E,uEAAuE;QACvE,MAAM,IAAI,GAAI,KAA+B,CAAC,IAAI,CAAC;QACnD,MAAM,OAAO,GAAG,IAAI,KAAK,QAAQ,IAAI,IAAI,KAAK,SAAS,CAAC;QACxD,OAAO,EAAE,GAAG,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC;IAC9D,CAAC;IAED,+EAA+E;IAC/E,+EAA+E;IAC/E,+EAA+E;IAC/E,+EAA+E;IAC/E,0EAA0E;IAC1E,wEAAwE;IACxE,gFAAgF;IAChF,yEAAyE;IACzE,gFAAgF;IAChF,MAAM,IAAI,GAAG,CAAC,IAAY,EAAU,EAAE;QACpC,MAAM,OAAO,GAAG,SAAS,CAAC,IAAI,CAAC,CAAC;QAChC,OAAO,OAAO,KAAK,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,iBAAiB,CAAC,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;IACpF,CAAC,CAAC;IACF,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IACxE,wEAAwE;IACxE,0EAA0E;IAC1E,2EAA2E;IAC3E,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,KAAK,EAAE,IAAI,EAA+B,EAAE,CAAC,CAAC;QACxF,IAAI;QACJ,OAAO,EAAE,SAAS,CAAC,IAAI,CAAC;QACxB,MAAM,EAAE,MAAM,QAAQ,CAAC,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;KACxC,CAAC,CAAC,CAAC,CAAC;IAEL,OAAO;QACL,GAAG;QACH,OAAO,EAAE,IAAI;QACb,KAAK,EAAE,OAAO,CAAC,MAAM;QACrB,IAAI,EAAE,WAAW,CAAC,OAAO,CAAC;QAC1B,OAAO;KACR,CAAC;AACJ,CAAC","sourcesContent":["/**\n * The problem reports a repository has collected: how many there are, what\n * state each one is in, and which ordinal the next one takes.\n *\n * A sub-package because `doctor/` is at its five-file limit, and a scan of its\n * own because this is the one directory loam reads that loam never writes. The\n * `loam-report` protocol has always asked a repository to accumulate a corpus\n * under `loam-reports/` and then never looked at it — eleven reports landed in\n * this repository in two days, three of them already closed, and every bit of\n * that state lived outside the files, in whoever remembered. The directory is\n * loam's own convention and its contents are loam's own format, so counting\n * them is a fact loam can read as cheaply as it reads `agentFiles`. Reading is\n * the whole of it: nothing here transmits anything, which is the constraint the\n * protocol is built around.\n *\n * Everything below is best-effort on purpose. A report is a hand-written\n * Markdown file, so an absent directory, an unreadable one, a name written\n * before the numbering existed and a file with no `Status:` line are all\n * ordinary states of a healthy repository. None of them is a finding, and\n * `doctor`'s `healthy` does not move for anything in here — a repo with eleven\n * open reports is not broken, it is one that has been paying attention.\n */\nimport { readdir, readFile } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport {\n type ProblemReportEntry,\n type ProblemReports,\n type ProblemReportStatus,\n} from \"../report.js\";\n\n/**\n * The conventional NAME of the directory the `loam-report` protocol asks a\n * repository to accumulate, spelled once — the segment `scanReports` joins onto\n * the resolved root to reach it.\n *\n * A name rather than a path, because two readers want different things out of\n * it and both are right: `ProblemReports.dir` carries the joined ABSOLUTE\n * location, because the protocol tells an agent to write there and a relative\n * spelling leaves it guessing which repository \"relative\" meant (`../report.ts`\n * records that), while `commands/doctor.ts`'s human row prints this constant, so\n * a person standing in the repository reads `loam-reports/` — what the protocol,\n * the reports themselves and every page that mentions them say.\n */\nexport const REPORTS_DIR = \"loam-reports\";\n\n/**\n * The leading ordinal of `012-2026-09-03-slug.md`. Three digits or more, so the\n * thousandth report widens the field instead of losing its number.\n */\nconst ORDINAL = /^(\\d{3,})-/;\n\n/**\n * A name written before the numbering existed: `2026-09-03-slug.md`, the shape\n * eleven reports in this repository already carry.\n *\n * Checked FIRST, and this is the whole reason it exists as its own pattern: a\n * four-digit year satisfies `ORDINAL` perfectly, so a legacy name read as an\n * ordinal is report number 2026 — and the next report loam hands out is then\n * 2027, in a directory whose highest real ordinal is 3. A legacy name is still\n * a report; it just has no number, and contributes none.\n */\nconst LEGACY_DATE_NAME = /^\\d{4}-\\d{2}-\\d{2}-/;\n\n/** The ordinal a report's file name claims, or null when the name predates numbering. */\nfunction ordinalOf(file: string): string | null {\n if (LEGACY_DATE_NAME.test(file)) return null;\n return ORDINAL.exec(file)?.[1] ?? null;\n}\n\n/**\n * The template's `- Status:` line, matched in the header FIELD BLOCK alone (see\n * `headerFields`). Only the first word is captured: the vocabulary's two\n * composite values carry prose after it (`fixed in 0.2.0-alpha.5`,\n * `superseded by 010`) that an author writes for a reader, and parsing further\n * would make loam an authority on a sentence it does not own.\n */\nconst STATUS_LINE = /^[ \\t]*-[ \\t]*Status:[ \\t]*(\\S+)/m;\n\n/** A fenced block's delimiter line, either spelling. */\nconst FENCE = /^[ \\t]*(`{3,}|~{3,})/;\n\n/**\n * An indented code block's line — the OTHER way markdown quotes code, and the\n * way `loam instructions loam-report` prints its own template (the four-space\n * block in ../../agent/support/report.ts). A fence-only reader counted such a\n * report open off the template's `- Status: open`.\n */\nconst INDENTED = /^(?: {4}|\\t)/;\n\n/** A section heading — the line that ends the header field block. */\nconst SECTION = /^##[ \\t]/;\n\n/**\n * The report's header field block: every line above the first `## ` heading,\n * with quoted code — fenced or indented — dropped.\n *\n * Matched anywhere in the file, `- Status:` convicted the wrong reports twice\n * over. A report that QUOTES the protocol template — which several do, because\n * an agent reporting that the protocol is wrong pastes the protocol — carries a\n * `- Status: open` inside a fence and was counted open however its own header\n * read; and a report whose body discusses a status line later on was read the\n * same way. The status a report claims is a header FIELD, written beside\n * `- Classification:` above the first section, so that is the only place it is\n * read from. An unterminated fence swallows the rest of the file rather than\n * flipping back — the safe direction, since the alternative is reading code as\n * a field.\n *\n * Two shapes defeated the first version of that, and both are the same quoting\n * case: a NESTED fence, where the outer ```` opens and the inner ``` was taken\n * for its close, leaving the quoted block read as fields; and an INDENTED\n * block, which the protocol itself uses and whose indented `## Summary` never\n * ends the header either. So a fence is closed only by a run of the same\n * character at least as long as the one that opened it, and an indented block\n * (four spaces after a blank line, through the blank lines inside it) is\n * dropped whole.\n */\nfunction headerFields(text: string): string {\n const kept: string[] = [];\n let fence: string | null = null;\n let indented = false;\n let afterBlank = true;\n for (const line of text.split(/\\r?\\n/)) {\n if (fence !== null) {\n const close = FENCE.exec(line)?.[1];\n if (close !== undefined && close[0] === fence[0] && close.length >= fence.length) fence = null;\n continue;\n }\n const blank = line.trim() === \"\";\n if (indented) {\n if (blank || INDENTED.test(line)) continue;\n indented = false;\n }\n const open = FENCE.exec(line)?.[1];\n if (open !== undefined) {\n fence = open;\n afterBlank = false;\n continue;\n }\n if (afterBlank && INDENTED.test(line)) {\n indented = true;\n continue;\n }\n if (SECTION.test(line)) break;\n kept.push(line);\n afterBlank = blank;\n }\n return kept.join(\"\\n\");\n}\n\n/** The words a `Status:` line may say that loam can count. */\nconst STATED: readonly ProblemReportStatus[] = [\"open\", \"sent\", \"fixed\", \"superseded\"];\n\n/**\n * The ordinal the next report takes: one past the highest present, never one\n * past the count.\n *\n * Those two differ exactly when the directory has a gap or a legacy name in it\n * — which this directory does, since the numbering arrived after eleven\n * reports had already been written — and counting instead of maximising is how\n * a next ordinal starts handing out numbers that are already taken.\n */\nfunction nextOrdinal(entries: readonly ProblemReportEntry[]): string {\n let highest = 0;\n for (const entry of entries) {\n if (entry.ordinal === null) continue;\n const value = Number.parseInt(entry.ordinal, 10);\n if (value > highest) highest = value;\n }\n return String(highest + 1).padStart(3, \"0\");\n}\n\n/**\n * What one report's own `Status:` line says, or `unstated`.\n *\n * Unreadable reads as unstated rather than throwing: a report saved as UTF-16,\n * a file the process cannot open, or a directory somebody named `notes.md` are\n * all things a diagnostic must survive. `doctor` is the command that has to be\n * able to describe a broken repository without becoming the next thing that\n * breaks in it.\n */\nasync function statusOf(path: string): Promise<ProblemReportStatus> {\n let text: string;\n try {\n text = await readFile(path, \"utf8\");\n } catch {\n // Unreadable is not a state the file claims — it is a state of the read.\n return \"unstated\";\n }\n const word = STATUS_LINE.exec(headerFields(text))?.[1]?.toLowerCase();\n return STATED.find((known) => known === word) ?? \"unstated\";\n}\n\n/**\n * Read `<root>/loam-reports/`, where `root` is the directory holding the\n * `loam.json` that resolved — the repository the reports were written at the\n * root of — falling back, when nothing parsed, to the directory of the\n * `loam.json` loam FOUND (`doctor.ts` says why that and not the cwd), and to the\n * run directory only when there is no config file anywhere above it.\n *\n * Every `*.md` in the directory is an entry, including a hand-written index\n * page: which files in there are \"really\" reports is a second naming\n * convention, and inventing one would make loam disagree with a directory\n * listing about what the directory holds. An unnumbered entry contributes\n * nothing to `next`, so an index costs the corpus nothing either.\n */\nexport async function scanReports(root: string): Promise<ProblemReports> {\n const dir = join(root, REPORTS_DIR);\n let names: string[];\n try {\n names = (await readdir(dir)).filter((name) => name.toLowerCase().endsWith(\".md\"));\n } catch (error) {\n // Absent is the state of every repository that has never had to write a\n // report, and it is the common one — `present: false`, nothing to say. Any\n // other errno means the directory IS there and this process could not list\n // it, which is a different sentence: the corpus exists and loam cannot see\n // into it, so `present` stays true and the counts stay honestly empty.\n const code = (error as NodeJS.ErrnoException).code;\n const present = code !== \"ENOENT\" && code !== \"ENOTDIR\";\n return { dir, present, total: 0, next: \"001\", entries: [] };\n }\n\n // Sorted here rather than left in readdir order, which is the filesystem's and\n // differs between one machine and another — and sorted by the ORDINAL the name\n // claims, numerically, because report order is what a reader scanning the list\n // expects and byte order stops being it at exactly the point the widening rule\n // was written for: `1000-…` sorts before `998-…`, so a directory that has\n // passed its thousandth report reads 1000, 1001, 998, 999 (verification\n // 2026-09-04, second pass). Unnumbered names — the shape reports carried before\n // the numbering existed — contribute no ordinal, so they sort last among\n // themselves by name, where a reader looking for \"the next one\" is not looking.\n const rank = (file: string): number => {\n const ordinal = ordinalOf(file);\n return ordinal === null ? Number.POSITIVE_INFINITY : Number.parseInt(ordinal, 10);\n };\n names.sort((a, b) => rank(a) - rank(b) || (a < b ? -1 : a > b ? 1 : 0));\n // Read together, like `staleAgentFiles` does: a report corpus is a flat\n // directory of small hand-written files, so there is nothing here worth a\n // concurrency cap, and `Promise.all` preserves the order just established.\n const entries = await Promise.all(names.map(async (file): Promise<ProblemReportEntry> => ({\n file,\n ordinal: ordinalOf(file),\n status: await statusOf(join(dir, file)),\n })));\n\n return {\n dir,\n present: true,\n total: entries.length,\n next: nextOrdinal(entries),\n entries,\n };\n}\n"]}
|
|
@@ -43,7 +43,7 @@ export const REFUSAL_MEANINGS = {
|
|
|
43
43
|
"seed-file-invalid": "`loam seed`'s fleet file is missing, unreadable, not YAML, the wrong shape, carries an illegal id — or no longer names every existing `services/<id>/` (that refusal lists the exact ids as `missingServices`); the message names the file and line, and editing the file and re-running succeeds.",
|
|
44
44
|
"seed-duplicate-service": "fleet.yaml declares one name twice — as two services, or as both a service and an external/subsystem; service ids, subsystem names and externals share one flat namespace, both declaration lines are named, and renaming one fixes it.",
|
|
45
45
|
"seed-unknown-subsystem": "A service's `subsystem:` in fleet.yaml names nothing `subsystems:` declares — the message carries a did-you-mean over the names the file really declares; add the subsystem or fix the spelling, then re-run.",
|
|
46
|
-
"seed-landscape-edited": "architecture/landscape.likec4 carries hand edits (the line-1 stamp's digest no longer matches) or was authored some other way — `loam seed` never overwrites human work and wrote nothing; fold the edits into fleet.yaml and delete the file, or keep the hand-authored map and stop using seed.",
|
|
46
|
+
"seed-landscape-edited": "architecture/landscape.likec4 carries hand edits (the line-1 stamp's digest no longer matches) or was authored some other way, which includes anything written ABOVE the scaffold's stub — `loam seed` never overwrites human work and wrote nothing; fold the edits into fleet.yaml and delete the file, or keep the hand-authored map and stop using seed.",
|
|
47
47
|
internal: "An unexpected throw — the one refusal with no stable meaning; it exists so a `--json` consumer still receives an envelope instead of a stack trace, and a repeatable `internal` is a loam defect worth reporting.",
|
|
48
48
|
};
|
|
49
49
|
//# sourceMappingURL=refusals.js.map
|