@ybotok/loam 0.2.0-alpha.4 → 0.2.0-alpha.6
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 +947 -0
- package/MIGRATING-from-OpenSpec.md +7 -1
- package/QUICKSTART.md +8 -3
- package/README.md +20 -13
- package/ROADMAP.md +329 -22
- package/SCHEMA.md +472 -24
- 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 +5 -2
- 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/plan.js +97 -3
- 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/sync.js +184 -39
- 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 +124 -0
- package/dist/commands/subsystem/txn/report.js.map +1 -0
- package/dist/commands/subsystem/txn/txn.js +29 -3
- package/dist/commands/subsystem/txn/txn.js.map +1 -1
- package/dist/commands/subsystem/txn/views.js +113 -16
- 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/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 +11 -10
- package/dist/commands/validate/fleet/census.js.map +1 -1
- package/dist/commands/validate/fleet/landscape.js +128 -114
- 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 +103 -0
- package/dist/commands/validate/fleet/map/attest.js.map +1 -0
- package/dist/commands/validate/fleet/map/bindings.js +96 -0
- package/dist/commands/validate/fleet/map/bindings.js.map +1 -0
- package/dist/commands/validate/fleet/map/consumers.js +298 -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/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 +13 -9
- package/dist/commands/validate/fleet/views/ids.js.map +1 -1
- package/dist/commands/validate/fleet/views/projects.js +278 -0
- package/dist/commands/validate/fleet/views/projects.js.map +1 -0
- package/dist/commands/validate/fleet/views/root-documents.js +97 -0
- package/dist/commands/validate/fleet/views/root-documents.js.map +1 -0
- package/dist/commands/validate/fleet/views/stale.js +2 -2
- 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 +18 -2
- 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 +51 -2
- 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.js +14 -4
- 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 +125 -6
- package/dist/core/agent/workflows/adopt.js.map +1 -1
- package/dist/core/agent/workflows/check.js +35 -19
- 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/brief/brief.js +63 -11
- package/dist/core/brief/brief.js.map +1 -1
- package/dist/core/brief/checks.js +138 -3
- 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 +30 -26
- package/dist/core/brief/targets.js.map +1 -1
- package/dist/core/brief/unchecked.js +12 -2
- 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 +52 -4
- 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 +9 -4
- package/dist/core/c4/project/documents.js.map +1 -1
- package/dist/core/c4/project/load.js +77 -38
- package/dist/core/c4/project/load.js.map +1 -1
- package/dist/core/c4/project/staged.js +46 -12
- 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 +4 -0
- 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 +31 -22
- 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 +48 -9
- 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/families.js +1 -1
- package/dist/core/explain/families.js.map +1 -1
- 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/partners.js +5 -9
- 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 +40 -0
- package/dist/core/repo/paths.js.map +1 -1
- package/dist/core/repo/tree/render/projects.js +182 -0
- package/dist/core/repo/tree/render/projects.js.map +1 -0
- package/dist/core/repo/tree/render/stale.js +2 -3
- package/dist/core/repo/tree/render/stale.js.map +1 -1
- package/dist/core/repo/tree/render/views.js +51 -3
- 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/vocabulary/maturity.js +10 -0
- package/dist/core/vocabulary/maturity.js.map +1 -1
- package/examples/README.md +23 -0
- package/examples/docs/architecture/landscape.likec4 +41 -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 +53 -67
- package/examples/docs/services/payment-service/arch.spec.md +7 -4
- package/examples/docs/services/payment-service/model.likec4 +32 -33
- package/examples/docs/services/platform/identity-service/arch.spec.md +7 -4
- package/examples/docs/services/platform/identity-service/model.likec4 +29 -33
- package/examples/docs/services/platform/notification-service/health.yaml +8 -3
- package/examples/docs/services/platform/notification-service/model.likec4 +39 -59
- package/package.json +1 -1
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The use-case axis of the SERVICE target: the `dynamic view`s a service's own
|
|
3
|
+
* LikeC4 project declares, graded as the hop sequences behind its requirements.
|
|
4
|
+
*
|
|
5
|
+
* The fleet target grades flows drawn over the map (`../../fleet/usecases/`);
|
|
6
|
+
* this grades flows drawn over ONE service's containers, which only that
|
|
7
|
+
* service's project can resolve — the whole reason the slot exists
|
|
8
|
+
* (`core/usecases/service/flows.ts` carries the report). The step grades are
|
|
9
|
+
* the fleet's own `stepFindings`, reused with the service's project as the
|
|
10
|
+
* model, the enumerated fleet as `services`, and one injection: where a
|
|
11
|
+
* finding says the view is. A second verdict→code mapping here would be the
|
|
12
|
+
* drift that package's banner forbids.
|
|
13
|
+
*
|
|
14
|
+
* ONLY VIEWS CARRYING A RESERVED TAG ARE GRADED, as at fleet altitude and for
|
|
15
|
+
* the same reason: a service's `views.likec4` full of hand-drawn diagrams must
|
|
16
|
+
* not turn red on upgrade. The reader applies the opt-in; this module never
|
|
17
|
+
* sees an untagged view. Graded is narrower than LOADED, and the difference is
|
|
18
|
+
* the reader's gate (`core/usecases/service/flows.ts`): an untagged sibling is
|
|
19
|
+
* not even loaded while no view in the project opts in and no sibling mentions
|
|
20
|
+
* either reserved prefix anywhere in its bytes — but once a view in
|
|
21
|
+
* model.likec4 itself opts in, or a sibling mentions `req-` or `cap-` in a
|
|
22
|
+
* comment or a title, every sibling is staged, and a project that does not
|
|
23
|
+
* read is one `usecase.flow-invalid` whether or not any view in it opted in.
|
|
24
|
+
*
|
|
25
|
+
* A SIBLING THAT BREAKS THE PROJECT is ONE `usecase.flow-invalid` — reused,
|
|
26
|
+
* because its meaning is exactly this ("loam could not read the flows against
|
|
27
|
+
* the map they are drawn over, so it graded none of them, and the renderer
|
|
28
|
+
* refuses the same project") applied to the service project instead of the
|
|
29
|
+
* merge preview — and NEVER `c4.invalid`: that code means "model.likec4 does
|
|
30
|
+
* not parse, every model check is suspended", and here the model parses and
|
|
31
|
+
* every model check RAN. Reusing it would tell an agent to fix the wrong file
|
|
32
|
+
* and tell the brief's `checks[]` row ("every other check stops here") a lie.
|
|
33
|
+
*/
|
|
34
|
+
import { existsSync } from "node:fs";
|
|
35
|
+
import { relative } from "node:path";
|
|
36
|
+
import { serviceResolver } from "../../../../core/c4/resolve/service.js";
|
|
37
|
+
import { compareIds } from "../../../../core/repo/entries.js";
|
|
38
|
+
import { errorText } from "../../../../core/c4/likec4.js";
|
|
39
|
+
import { CAP_TAG_PREFIX, REQ_TAG_PREFIX } from "../../../../core/capabilities/usecase-join.js";
|
|
40
|
+
import { stepFindings } from "../../fleet/usecases/steps.js";
|
|
41
|
+
import { serviceTagFindings } from "./tags.js";
|
|
42
|
+
/** `<svc>: services/<tree>/<file> — dynamic view '<id>'` — the service target's spelling of `viewPlace`. */
|
|
43
|
+
function servicePlace(scope, view) {
|
|
44
|
+
// An absent `sourcePath` names the directory rather than guessing a file,
|
|
45
|
+
// for `viewFile`'s reason; nothing at the 1.59.2 pin reaches that arm.
|
|
46
|
+
return `${scope.service}: ${scope.treePath}/${view.sourcePath ?? ""} — dynamic view '${view.id}'`;
|
|
47
|
+
}
|
|
48
|
+
/** The repo-relative spelling of a project error's document, or the whole project when it carries none. */
|
|
49
|
+
function fileOf(scope, error) {
|
|
50
|
+
return error.sourceFsPath === undefined
|
|
51
|
+
? `${scope.treePath}/`
|
|
52
|
+
: relative(scope.docsDir, error.sourceFsPath).split(/[\\/]/).join("/");
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* The one finding a broken project earns. `details` is one `errorText` line
|
|
56
|
+
* per error, each prefixed by its file, because the project may hold several
|
|
57
|
+
* siblings and the fault may sit in any of them — including `model.likec4`
|
|
58
|
+
* itself when a sibling re-declares its `specification` block, an error LikeC4
|
|
59
|
+
* blames on both files. `locations` names the first broken file, in the sorted
|
|
60
|
+
* order the reader produced.
|
|
61
|
+
*/
|
|
62
|
+
function flowInvalidFinding(scope, errors) {
|
|
63
|
+
const files = [...new Set(errors.map((error) => fileOf(scope, error)))];
|
|
64
|
+
const [first = `${scope.treePath}/`, ...others] = files;
|
|
65
|
+
const named = others.length === 0 ? first : `${first} and ${others.length} more file(s)`;
|
|
66
|
+
return {
|
|
67
|
+
severity: "error",
|
|
68
|
+
code: "usecase.flow-invalid",
|
|
69
|
+
subject: scope.service,
|
|
70
|
+
message: `${scope.service}: ${named} has ${errors.length} error(s) — every .likec4 beside model.likec4 is one LikeC4 ` +
|
|
71
|
+
"project, read the way the renderer reads it, and the renderer refuses the whole project over one broken " +
|
|
72
|
+
"file; no flow in it was graded. model.likec4 itself parses and is graded alone.",
|
|
73
|
+
// The hint first, for `c4.invalid`'s reason (`../model/grade.ts`):
|
|
74
|
+
// `capDetails` keeps ten lines, one broken document can carry more than ten
|
|
75
|
+
// errors, and a diagnosis appended after them is dropped exactly when the
|
|
76
|
+
// reader needs it.
|
|
77
|
+
details: [...tagHint(errors), ...errors.map((error) => `${fileOf(scope, error)}: ${errorText(error)}`)],
|
|
78
|
+
locations: [{ path: first, role: "primary" }],
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
/** `Could not resolve reference to Tag named '#req-PAY-AUTH'.` — measured at the 1.59.2 pin. */
|
|
82
|
+
const UNDECLARED_TAG = /reference to Tag named '#?([\w.-]+)'/;
|
|
83
|
+
/**
|
|
84
|
+
* The one line an undeclared RESERVED tag earns.
|
|
85
|
+
*
|
|
86
|
+
* The opt-in is a tag, LikeC4 refuses a tag no `specification` declares, and the
|
|
87
|
+
* refusal is a bare `Could not resolve reference to Tag named …` — so an author
|
|
88
|
+
* following the protocol's "tag the view `#req-<id>`" got a parse error naming
|
|
89
|
+
* the thing they had just been told to write, with nothing saying a tag must be
|
|
90
|
+
* declared first (verification 2026-09-04, D10). One line, from the first such
|
|
91
|
+
* error only: the second one has the same repair.
|
|
92
|
+
*/
|
|
93
|
+
function tagHint(errors) {
|
|
94
|
+
for (const error of errors) {
|
|
95
|
+
const match = UNDECLARED_TAG.exec(error.message);
|
|
96
|
+
const name = match?.[1];
|
|
97
|
+
if (name === undefined)
|
|
98
|
+
continue;
|
|
99
|
+
// `cap-` and `req-` do NOT share a repair here, and covering both with one
|
|
100
|
+
// hint sent an author two steps where one would do: declaring `tag cap-…`
|
|
101
|
+
// makes the file parse and the very next run says `usecase.capability-
|
|
102
|
+
// unresolved` — "no capability can be claimed inside this service's own
|
|
103
|
+
// project. Drop the tag." A capability is claimed at fleet altitude, so the
|
|
104
|
+
// tag can never resolve to anything beside a model, whatever is declared
|
|
105
|
+
// (verification 2026-09-04).
|
|
106
|
+
if (name.startsWith(CAP_TAG_PREFIX)) {
|
|
107
|
+
return [
|
|
108
|
+
`\`#${name}\` claims a capability, and a capability is claimed at FLEET altitude only — a flow beside ` +
|
|
109
|
+
"model.likec4 is read in this service's own project, where no capability can be resolved. Declaring " +
|
|
110
|
+
`\`tag ${name}\` makes the file parse and earns \`usecase.capability-unresolved\` on the next run: ` +
|
|
111
|
+
`drop the tag, and tag the flow \`#${REQ_TAG_PREFIX}<Requirement-ID>\` instead, or draw it in ` +
|
|
112
|
+
"`architecture/usecases/` where a capability tag belongs",
|
|
113
|
+
];
|
|
114
|
+
}
|
|
115
|
+
if (!name.startsWith(REQ_TAG_PREFIX))
|
|
116
|
+
continue;
|
|
117
|
+
return [
|
|
118
|
+
`declare \`tag ${name}\` in the \`specification { }\` block this project reads — model.likec4's own ` +
|
|
119
|
+
"for a model that stands alone, the fleet map's for one that extends it (or a tags-only " +
|
|
120
|
+
"`specification` in a document beside the model)",
|
|
121
|
+
];
|
|
122
|
+
}
|
|
123
|
+
return [];
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* The FILE each finding is about, as `locations[0]`.
|
|
127
|
+
*
|
|
128
|
+
* The step and tag grades carry no `locations` of their own, so `findingJson`
|
|
129
|
+
* fell back to the target's scope — `services/<tree>` and nothing else — and an
|
|
130
|
+
* agent reading `--json`, which is the reason the envelope exists, had to parse
|
|
131
|
+
* the prose to learn which `.likec4` to open. `usecase.flow-invalid` on this
|
|
132
|
+
* same arm already names its file; these say the file the same way, with the
|
|
133
|
+
* service directory kept beside it as `scope` (`../spine.ts`'s shape).
|
|
134
|
+
*/
|
|
135
|
+
function atView(scope, view, findings) {
|
|
136
|
+
const file = view.sourcePath === undefined ? `${scope.treePath}/` : `${scope.treePath}/${view.sourcePath}`;
|
|
137
|
+
return findings.map((finding) => finding.locations === undefined
|
|
138
|
+
? {
|
|
139
|
+
...finding,
|
|
140
|
+
locations: [
|
|
141
|
+
{ path: file, role: "primary" },
|
|
142
|
+
{ path: scope.treePath, role: "scope" },
|
|
143
|
+
],
|
|
144
|
+
}
|
|
145
|
+
: finding);
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Every use-case finding a service's own project earns.
|
|
149
|
+
*
|
|
150
|
+
* Views are sorted by (file, view id) for the fleet grader's reason: nothing in
|
|
151
|
+
* loam has measured that the parse preserves declaration order, and a report
|
|
152
|
+
* whose row order depends on it would reorder under a dependency bump.
|
|
153
|
+
*/
|
|
154
|
+
export function serviceUseCaseFindings(scope) {
|
|
155
|
+
const { scan } = scope;
|
|
156
|
+
if (scan.kind === "none")
|
|
157
|
+
return [];
|
|
158
|
+
if (scan.kind === "unreadable")
|
|
159
|
+
return [flowInvalidFinding(scope, scan.errors)];
|
|
160
|
+
const place = (view) => servicePlace(scope, view);
|
|
161
|
+
const grading = {
|
|
162
|
+
model: scan.model,
|
|
163
|
+
services: scope.services,
|
|
164
|
+
// Built over the PROJECT's elements with the fleet set, exactly as the
|
|
165
|
+
// attribution's own fallback tier resolves — so "which service is this
|
|
166
|
+
// container" cannot be answered one way by `attributeStep` and another by
|
|
167
|
+
// the provider guard.
|
|
168
|
+
resolve: serviceResolver(scan.model.elements, scan.model.known),
|
|
169
|
+
place,
|
|
170
|
+
};
|
|
171
|
+
const findings = [];
|
|
172
|
+
const views = [...scan.views].sort((a, b) => compareIds(a.sourcePath ?? "", b.sourcePath ?? "") || compareIds(a.id, b.id));
|
|
173
|
+
for (const view of views) {
|
|
174
|
+
const forView = [...serviceTagFindings(view, scope, place(view))];
|
|
175
|
+
for (const step of view.steps)
|
|
176
|
+
forView.push(...stepFindings({ view, step }, grading));
|
|
177
|
+
findings.push(...atView(scope, view, forView));
|
|
178
|
+
}
|
|
179
|
+
return findings;
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* The `Requirement-ID`s a service-local `#req-` tag may name: every identified
|
|
183
|
+
* requirement of spec.md (living — a REMOVED one is on its way out and keeps no
|
|
184
|
+
* promise) and of arch.spec.md, as ONE set. `undefined` when NEITHER document
|
|
185
|
+
* exists, which the tag grade reads as "no requirement to satisfy" rather than
|
|
186
|
+
* "none flattening to this slug" — the same two answers the fleet's
|
|
187
|
+
* `requirementsOf` gives for a capability with no document at all.
|
|
188
|
+
*
|
|
189
|
+
* An id both documents declare resolves once (a set dedupes it); two ids that
|
|
190
|
+
* merely flatten alike — `PAY.AUTH` in one file, `PAY-AUTH` in the other — are
|
|
191
|
+
* the `many` arm, because a Requirement-ID is unique inside one document and a
|
|
192
|
+
* flow beside the model has two.
|
|
193
|
+
*
|
|
194
|
+
* It lives beside the grade it feeds rather than in `../service.ts`, which is
|
|
195
|
+
* the ORDER and nothing else: the set is this axis's input, its two answers are
|
|
196
|
+
* this axis's vocabulary, and the order module had grown past its line limit
|
|
197
|
+
* carrying it.
|
|
198
|
+
*/
|
|
199
|
+
export function requirementIdsOf(paths, livingReqs, archReqs) {
|
|
200
|
+
if (!existsSync(paths.spec) && !existsSync(paths.archSpec))
|
|
201
|
+
return undefined;
|
|
202
|
+
const ids = new Set();
|
|
203
|
+
for (const req of [...livingReqs, ...archReqs.filter((r) => r.kind !== "REMOVED")]) {
|
|
204
|
+
if (req.id !== undefined)
|
|
205
|
+
ids.add(req.id);
|
|
206
|
+
}
|
|
207
|
+
return ids;
|
|
208
|
+
}
|
|
209
|
+
//# sourceMappingURL=usecases.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"usecases.js","sourceRoot":"","sources":["../../../../../src/commands/validate/service/usecases/usecases.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The use-case axis of the SERVICE target: the `dynamic view`s a service's own\n * LikeC4 project declares, graded as the hop sequences behind its requirements.\n *\n * The fleet target grades flows drawn over the map (`../../fleet/usecases/`);\n * this grades flows drawn over ONE service's containers, which only that\n * service's project can resolve — the whole reason the slot exists\n * (`core/usecases/service/flows.ts` carries the report). The step grades are\n * the fleet's own `stepFindings`, reused with the service's project as the\n * model, the enumerated fleet as `services`, and one injection: where a\n * finding says the view is. A second verdict→code mapping here would be the\n * drift that package's banner forbids.\n *\n * ONLY VIEWS CARRYING A RESERVED TAG ARE GRADED, as at fleet altitude and for\n * the same reason: a service's `views.likec4` full of hand-drawn diagrams must\n * not turn red on upgrade. The reader applies the opt-in; this module never\n * sees an untagged view. Graded is narrower than LOADED, and the difference is\n * the reader's gate (`core/usecases/service/flows.ts`): an untagged sibling is\n * not even loaded while no view in the project opts in and no sibling mentions\n * either reserved prefix anywhere in its bytes — but once a view in\n * model.likec4 itself opts in, or a sibling mentions `req-` or `cap-` in a\n * comment or a title, every sibling is staged, and a project that does not\n * read is one `usecase.flow-invalid` whether or not any view in it opted in.\n *\n * A SIBLING THAT BREAKS THE PROJECT is ONE `usecase.flow-invalid` — reused,\n * because its meaning is exactly this (\"loam could not read the flows against\n * the map they are drawn over, so it graded none of them, and the renderer\n * refuses the same project\") applied to the service project instead of the\n * merge preview — and NEVER `c4.invalid`: that code means \"model.likec4 does\n * not parse, every model check is suspended\", and here the model parses and\n * every model check RAN. Reusing it would tell an agent to fix the wrong file\n * and tell the brief's `checks[]` row (\"every other check stops here\") a lie.\n */\nimport { existsSync } from \"node:fs\";\nimport { relative } from \"node:path\";\nimport type { LikeC4Error } from \"../../../../core/c4/likec4.js\";\nimport type { Requirement } from \"../../../../core/document/spec.js\";\nimport type { ServicePaths } from \"../../../../core/repo/paths.js\";\nimport type { ParsedView } from \"../../../../core/c4/parsed/dynamic-views.js\";\nimport { serviceResolver } from \"../../../../core/c4/resolve/service.js\";\nimport type { DocsDir } from \"../../../../core/kernel/ids/dirs.js\";\nimport { compareIds } from \"../../../../core/repo/entries.js\";\nimport type { ServiceFlowScan } from \"../../../../core/usecases/service/flows.js\";\nimport type { Finding } from \"../../../../core/vocabulary/report.js\";\nimport { errorText } from \"../../../../core/c4/likec4.js\";\nimport { CAP_TAG_PREFIX, REQ_TAG_PREFIX } from \"../../../../core/capabilities/usecase-join.js\";\nimport { stepFindings, type StepGrading } from \"../../fleet/usecases/steps.js\";\nimport { serviceTagFindings, type ServiceTagScope } from \"./tags.js\";\n\n/** What the service's use-case grades run over. */\nexport interface ServiceUseCaseScope extends ServiceTagScope {\n /** The docs root, so a project error's absolute path can be spelled repo-relative. */\n docsDir: DocsDir;\n scan: ServiceFlowScan;\n /**\n * The `services/<id>/` directories that exist — the SAME enumerated set the\n * fleet target hands `stepFindings`, never an empty one: `StepGrading`\n * documents why the intra-service exemption is a guard and not a scope.\n */\n services: ReadonlySet<string>;\n}\n\n/** `<svc>: services/<tree>/<file> — dynamic view '<id>'` — the service target's spelling of `viewPlace`. */\nfunction servicePlace(scope: ServiceUseCaseScope, view: ParsedView): string {\n // An absent `sourcePath` names the directory rather than guessing a file,\n // for `viewFile`'s reason; nothing at the 1.59.2 pin reaches that arm.\n return `${scope.service}: ${scope.treePath}/${view.sourcePath ?? \"\"} — dynamic view '${view.id}'`;\n}\n\n/** The repo-relative spelling of a project error's document, or the whole project when it carries none. */\nfunction fileOf(scope: ServiceUseCaseScope, error: LikeC4Error): string {\n return error.sourceFsPath === undefined\n ? `${scope.treePath}/`\n : relative(scope.docsDir, error.sourceFsPath).split(/[\\\\/]/).join(\"/\");\n}\n\n/**\n * The one finding a broken project earns. `details` is one `errorText` line\n * per error, each prefixed by its file, because the project may hold several\n * siblings and the fault may sit in any of them — including `model.likec4`\n * itself when a sibling re-declares its `specification` block, an error LikeC4\n * blames on both files. `locations` names the first broken file, in the sorted\n * order the reader produced.\n */\nfunction flowInvalidFinding(scope: ServiceUseCaseScope, errors: readonly LikeC4Error[]): Finding {\n const files = [...new Set(errors.map((error) => fileOf(scope, error)))];\n const [first = `${scope.treePath}/`, ...others] = files;\n const named = others.length === 0 ? first : `${first} and ${others.length} more file(s)`;\n return {\n severity: \"error\",\n code: \"usecase.flow-invalid\",\n subject: scope.service,\n message:\n `${scope.service}: ${named} has ${errors.length} error(s) — every .likec4 beside model.likec4 is one LikeC4 ` +\n \"project, read the way the renderer reads it, and the renderer refuses the whole project over one broken \" +\n \"file; no flow in it was graded. model.likec4 itself parses and is graded alone.\",\n // The hint first, for `c4.invalid`'s reason (`../model/grade.ts`):\n // `capDetails` keeps ten lines, one broken document can carry more than ten\n // errors, and a diagnosis appended after them is dropped exactly when the\n // reader needs it.\n details: [...tagHint(errors), ...errors.map((error) => `${fileOf(scope, error)}: ${errorText(error)}`)],\n locations: [{ path: first, role: \"primary\" }],\n };\n}\n\n/** `Could not resolve reference to Tag named '#req-PAY-AUTH'.` — measured at the 1.59.2 pin. */\nconst UNDECLARED_TAG = /reference to Tag named '#?([\\w.-]+)'/;\n\n/**\n * The one line an undeclared RESERVED tag earns.\n *\n * The opt-in is a tag, LikeC4 refuses a tag no `specification` declares, and the\n * refusal is a bare `Could not resolve reference to Tag named …` — so an author\n * following the protocol's \"tag the view `#req-<id>`\" got a parse error naming\n * the thing they had just been told to write, with nothing saying a tag must be\n * declared first (verification 2026-09-04, D10). One line, from the first such\n * error only: the second one has the same repair.\n */\nfunction tagHint(errors: readonly LikeC4Error[]): string[] {\n for (const error of errors) {\n const match = UNDECLARED_TAG.exec(error.message);\n const name = match?.[1];\n if (name === undefined) continue;\n // `cap-` and `req-` do NOT share a repair here, and covering both with one\n // hint sent an author two steps where one would do: declaring `tag cap-…`\n // makes the file parse and the very next run says `usecase.capability-\n // unresolved` — \"no capability can be claimed inside this service's own\n // project. Drop the tag.\" A capability is claimed at fleet altitude, so the\n // tag can never resolve to anything beside a model, whatever is declared\n // (verification 2026-09-04).\n if (name.startsWith(CAP_TAG_PREFIX)) {\n return [\n `\\`#${name}\\` claims a capability, and a capability is claimed at FLEET altitude only — a flow beside ` +\n \"model.likec4 is read in this service's own project, where no capability can be resolved. Declaring \" +\n `\\`tag ${name}\\` makes the file parse and earns \\`usecase.capability-unresolved\\` on the next run: ` +\n `drop the tag, and tag the flow \\`#${REQ_TAG_PREFIX}<Requirement-ID>\\` instead, or draw it in ` +\n \"`architecture/usecases/` where a capability tag belongs\",\n ];\n }\n if (!name.startsWith(REQ_TAG_PREFIX)) continue;\n return [\n `declare \\`tag ${name}\\` in the \\`specification { }\\` block this project reads — model.likec4's own ` +\n \"for a model that stands alone, the fleet map's for one that extends it (or a tags-only \" +\n \"`specification` in a document beside the model)\",\n ];\n }\n return [];\n}\n\n/**\n * The FILE each finding is about, as `locations[0]`.\n *\n * The step and tag grades carry no `locations` of their own, so `findingJson`\n * fell back to the target's scope — `services/<tree>` and nothing else — and an\n * agent reading `--json`, which is the reason the envelope exists, had to parse\n * the prose to learn which `.likec4` to open. `usecase.flow-invalid` on this\n * same arm already names its file; these say the file the same way, with the\n * service directory kept beside it as `scope` (`../spine.ts`'s shape).\n */\nfunction atView(scope: ServiceUseCaseScope, view: ParsedView, findings: Finding[]): Finding[] {\n const file = view.sourcePath === undefined ? `${scope.treePath}/` : `${scope.treePath}/${view.sourcePath}`;\n return findings.map((finding) =>\n finding.locations === undefined\n ? {\n ...finding,\n locations: [\n { path: file, role: \"primary\" as const },\n { path: scope.treePath, role: \"scope\" as const },\n ],\n }\n : finding,\n );\n}\n\n/**\n * Every use-case finding a service's own project earns.\n *\n * Views are sorted by (file, view id) for the fleet grader's reason: nothing in\n * loam has measured that the parse preserves declaration order, and a report\n * whose row order depends on it would reorder under a dependency bump.\n */\nexport function serviceUseCaseFindings(scope: ServiceUseCaseScope): Finding[] {\n const { scan } = scope;\n if (scan.kind === \"none\") return [];\n if (scan.kind === \"unreadable\") return [flowInvalidFinding(scope, scan.errors)];\n\n const place = (view: ParsedView): string => servicePlace(scope, view);\n const grading: StepGrading = {\n model: scan.model,\n services: scope.services,\n // Built over the PROJECT's elements with the fleet set, exactly as the\n // attribution's own fallback tier resolves — so \"which service is this\n // container\" cannot be answered one way by `attributeStep` and another by\n // the provider guard.\n resolve: serviceResolver(scan.model.elements, scan.model.known),\n place,\n };\n const findings: Finding[] = [];\n const views = [...scan.views].sort(\n (a, b) => compareIds(a.sourcePath ?? \"\", b.sourcePath ?? \"\") || compareIds(a.id, b.id),\n );\n for (const view of views) {\n const forView: Finding[] = [...serviceTagFindings(view, scope, place(view))];\n for (const step of view.steps) forView.push(...stepFindings({ view, step }, grading));\n findings.push(...atView(scope, view, forView));\n }\n return findings;\n}\n\n/**\n * The `Requirement-ID`s a service-local `#req-` tag may name: every identified\n * requirement of spec.md (living — a REMOVED one is on its way out and keeps no\n * promise) and of arch.spec.md, as ONE set. `undefined` when NEITHER document\n * exists, which the tag grade reads as \"no requirement to satisfy\" rather than\n * \"none flattening to this slug\" — the same two answers the fleet's\n * `requirementsOf` gives for a capability with no document at all.\n *\n * An id both documents declare resolves once (a set dedupes it); two ids that\n * merely flatten alike — `PAY.AUTH` in one file, `PAY-AUTH` in the other — are\n * the `many` arm, because a Requirement-ID is unique inside one document and a\n * flow beside the model has two.\n *\n * It lives beside the grade it feeds rather than in `../service.ts`, which is\n * the ORDER and nothing else: the set is this axis's input, its two answers are\n * this axis's vocabulary, and the order module had grown past its line limit\n * carrying it.\n */\nexport function requirementIdsOf(\n paths: ServicePaths,\n livingReqs: readonly Requirement[],\n archReqs: readonly Requirement[],\n): ReadonlySet<string> | undefined {\n if (!existsSync(paths.spec) && !existsSync(paths.archSpec)) return undefined;\n const ids = new Set<string>();\n for (const req of [...livingReqs, ...archReqs.filter((r) => r.kind !== \"REMOVED\")]) {\n if (req.id !== undefined) ids.add(req.id);\n }\n return ids;\n}\n"]}
|
|
@@ -6,7 +6,7 @@ import { resolveServiceTarget } from "../../core/repo/service-target.js";
|
|
|
6
6
|
import { loadConfig } from "../../core/envelope/config.js";
|
|
7
7
|
import { emitJson, fail, NO_SERVICE_MESSAGE, reportNoConfig } from "../../core/envelope/json.js";
|
|
8
8
|
import { DocsRepoUnavailableError } from "../../core/repo/state.js";
|
|
9
|
-
import { agentsPath as agentsFile,
|
|
9
|
+
import { agentsPath as agentsFile, landscapePath as landscapeFile } from "../../core/repo/paths.js";
|
|
10
10
|
import { listFeatures, listServices, missingFeatureMessage, resolveFeature } from "../../core/repo/repo.js";
|
|
11
11
|
import { countSeverity, reportValid, targetJson, targetValid, } from "../../core/vocabulary/report.js";
|
|
12
12
|
import { LOAM_VERSION } from "../../core/envelope/version.js";
|
|
@@ -16,8 +16,7 @@ import { docsRepoReady, reportDocsRepoError, reportRepositoryUnavailable, } from
|
|
|
16
16
|
import { interruptedCommitFinding } from "../../core/staging/recovery/finding.js";
|
|
17
17
|
import { scopeJson, scopeLine, scopeSince } from "../../core/diff/scope/changed-paths.js";
|
|
18
18
|
import { validateLandscape } from "./fleet/landscape.js";
|
|
19
|
-
import {
|
|
20
|
-
import { readLandscape } from "./fleet/load.js";
|
|
19
|
+
import { prefetchFleetDocuments, readLandscape } from "./fleet/load.js";
|
|
21
20
|
import { buildScorecard } from "./fleet/scorecard/scorecard.js";
|
|
22
21
|
import { printScorecard } from "./fleet/scorecard/print.js";
|
|
23
22
|
import { validateFeature } from "./feature.js";
|
|
@@ -91,20 +90,9 @@ export function registerValidate(program) {
|
|
|
91
90
|
let scope = null;
|
|
92
91
|
try {
|
|
93
92
|
if (opts.all) {
|
|
94
|
-
// ONE
|
|
95
|
-
//
|
|
96
|
-
//
|
|
97
|
-
// 13.7s median over the 120-service benchmark (docs/BENCHMARKS.md).
|
|
98
|
-
// So --all enumerates its documents up front — the landscape, every
|
|
99
|
-
// service model, every active feature's delta — and batch-parses
|
|
100
|
-
// them into the fleet context's memo; every loadLikeC4 below, the
|
|
101
|
-
// landscape read included, is then a seeded hit. The enumerations are
|
|
102
|
-
// the same memoized promises the target loops reuse. If the batch
|
|
103
|
-
// CANNOT run (a sandbox denying tmpdir writes), prefetch seeds
|
|
104
|
-
// nothing and every load falls back to today's per-path parse:
|
|
105
|
-
// identical findings, the old speed. Single-service validate and
|
|
106
|
-
// list keep their untouched code paths on purpose — the ≤10%
|
|
107
|
-
// regression bound in docs/BENCHMARKS.md holds by construction.
|
|
93
|
+
// ONE workspace per document kind for the whole run — the block moved
|
|
94
|
+
// to `fleet/load.ts` (`prefetchFleetDocuments`), which is where the
|
|
95
|
+
// rule about which shape parses in which workspace belongs.
|
|
108
96
|
const lp = landscapeFile(docsDir);
|
|
109
97
|
const enumerated = await listServices(docsDir, fleet);
|
|
110
98
|
const active = await listFeatures(docsDir, {}, fleet);
|
|
@@ -123,15 +111,13 @@ export function registerValidate(program) {
|
|
|
123
111
|
}
|
|
124
112
|
const services = scope === null ? enumerated : scope.services;
|
|
125
113
|
const features = scope === null ? active : scope.features;
|
|
126
|
-
await fleet
|
|
127
|
-
...(existsSync(lp) ? [lp] : []),
|
|
128
|
-
...services.filter((svc) => svc.has.model).map((svc) => servicePathsAt(svc.dir).model),
|
|
129
|
-
...features.filter((feat) => feat.has.delta).map((feat) => featurePaths(feat.dir).delta),
|
|
130
|
-
]);
|
|
114
|
+
await prefetchFleetDocuments({ docsDir, fleet, services, features });
|
|
131
115
|
// Read whether or not the landscape is a TARGET: every service check
|
|
132
116
|
// below joins against it, so a scoped run needs the map in hand even
|
|
133
|
-
// when it does not grade the map itself.
|
|
134
|
-
|
|
117
|
+
// when it does not grade the map itself. Through the fleet index, so
|
|
118
|
+
// the single-service path — which now reads the same project — answers
|
|
119
|
+
// out of the same memo rather than parsing it a second time.
|
|
120
|
+
const land = existsSync(lp) ? await readLandscape(() => fleet.architecture(docsDir)) : null;
|
|
135
121
|
// The fleet-level cross-check first: it frames everything below it, and a
|
|
136
122
|
// service nobody drew is worth knowing before its own findings scroll past.
|
|
137
123
|
// Under `--base` it is a target only when `architecture/` changed — the
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validate.js","sourceRoot":"","sources":["../../../src/commands/validate/validate.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `loam validate` — argument grammar, target selection, and the run's one\n * filesystem snapshot.\n *\n * The checks themselves live beside it, one module per target kind\n * (`landscape.ts`, `service/`, `feature/`) over the document checks they share\n * (`requirements.ts`). Every one of them produces findings and none of them\n * print: printing is `report.ts`'s, and it happens once, after the last target\n * is graded.\n */\nimport type { Command } from \"commander\";\nimport { existsSync } from \"node:fs\";\nimport { readFile } from \"node:fs/promises\";\nimport { agentsStaleFinding, binaryBehindFinding } from \"../../core/agent/agents-stamp.js\";\nimport { inOrder } from \"../../core/kernel/concurrency.js\";\nimport type { RawServiceId } from \"../../core/kernel/ids/service.js\";\nimport { resolveServiceTarget } from \"../../core/repo/service-target.js\";\nimport { loadConfig } from \"../../core/envelope/config.js\";\nimport { emitJson, fail, NO_SERVICE_MESSAGE, reportNoConfig } from \"../../core/envelope/json.js\";\nimport { DocsRepoUnavailableError } from \"../../core/repo/state.js\";\nimport { agentsPath as agentsFile, featurePaths, landscapePath as landscapeFile, servicePathsAt } from \"../../core/repo/paths.js\";\nimport { listFeatures, listServices, missingFeatureMessage, resolveFeature } from \"../../core/repo/repo.js\";\nimport {\n countSeverity,\n reportValid,\n targetJson,\n targetValid,\n type TargetReport,\n} from \"../../core/vocabulary/report.js\";\nimport { LOAM_VERSION } from \"../../core/envelope/version.js\";\nimport { FleetContext } from \"../../core/fleet-context.js\";\nimport { fixesFor } from \"../policy/format.js\";\nimport {\n docsRepoReady,\n reportDocsRepoError,\n reportRepositoryUnavailable,\n} from \"../policy/gate.js\";\nimport { interruptedCommitFinding } from \"../../core/staging/recovery/finding.js\";\nimport { scopeJson, scopeLine, scopeSince, type ValidateScope } from \"../../core/diff/scope/changed-paths.js\";\nimport { validateLandscape } from \"./fleet/landscape.js\";\nimport { loadArchitecture } from \"../../core/c4/project/architecture.js\";\nimport { readLandscape } from \"./fleet/load.js\";\nimport { buildScorecard } from \"./fleet/scorecard/scorecard.js\";\nimport { printScorecard } from \"./fleet/scorecard/print.js\";\nimport { validateFeature } from \"./feature.js\";\nimport { validateService } from \"./service/service.js\";\nimport { ambiguousTarget, capDetails, guarded, renderText, summary, unverifiableSubjects } from \"./report.js\";\n\ninterface ValidateOptions {\n service?: string;\n feature?: string;\n all?: boolean;\n base?: string;\n strict?: boolean;\n errorsOnly?: boolean;\n json?: boolean;\n}\n\nexport function registerValidate(program: Command): void {\n program\n .command(\"validate\")\n .argument(\"[target]\", \"service or feature id (a feature wins when both match; --service/--feature force the reading)\")\n .description(\"Validate a service (C4 + requirement coverage) or a feature (delta + coverage)\")\n .option(\"--service <id>\", \"service to validate (defaults to the configured service)\")\n .option(\"--feature <id>\", \"validate a feature delta instead of a service\")\n .option(\"--all\", \"validate every service and every active feature\")\n .option(\"--base <ref>\", \"with --all: grade only the targets changed since a base git ref of the docs repo — the adoption ratchet\")\n .option(\"--strict\", \"exit 1 on any warning too — a per-invocation CI lever; the report and --json payload do not change\")\n .option(\"--errors-only\", \"print only errors and warnings — a rendering lever; the --json payload does not change\")\n .option(\"--json\", \"emit the machine contract instead of the human view\")\n .action(async (target: string | undefined, opts: ValidateOptions) => {\n const json = opts.json === true;\n\n if (target !== undefined && (opts.all || opts.service || opts.feature)) {\n fail(json, \"invalid-option\", `'${target}' already names the target; drop --all/--service/--feature.`);\n return;\n }\n if (opts.all && (opts.service || opts.feature)) {\n fail(json, \"invalid-option\", \"--all validates everything; drop --service/--feature.\");\n return;\n }\n // Two targets is no target: silently validating only the feature taught\n // callers that --service had been honoured when it had been dropped.\n if (opts.service && opts.feature) {\n fail(json, \"invalid-option\", \"--service and --feature name different targets; pass one or the other.\");\n return;\n }\n // `--base` NARROWS a whole-fleet run; it does not name one. Beside a\n // positional, `--service` or `--feature` it is a scope over a scope — a\n // contradiction rather than a narrowing, whose \"this service if it\n // changed\" reading nobody could tell apart from a green over a service\n // never looked at. `--base` and not `--since`: `--base <ref>` is already\n // loam's word for a base git ref of the docs repo on diff and vouch.\n if (opts.base !== undefined && opts.all !== true) {\n fail(\n json,\n \"invalid-option\",\n target !== undefined || opts.service || opts.feature\n ? \"--base narrows --all to what changed since a ref; a named target is already a scope. Pass one or the other.\"\n : \"--base narrows --all to what changed since a ref — pass --all with it.\",\n );\n return;\n }\n\n const loaded = await loadConfig();\n if (loaded.kind !== \"loaded\") {\n reportNoConfig(json, loaded);\n return;\n }\n const config = loaded.config;\n const { docsDir } = config;\n // Before anything is enumerated: a docsDir that is not a docs repo must\n // refuse, not report an empty fleet. See docsRepoReady. `--feature` is the\n // one mode that never enumerates services, so it asks for less.\n if (!docsRepoReady(json, docsDir, opts.feature ? \"docs\" : \"services\")) return;\n // One invocation, one filesystem snapshot. Nothing is global: a later\n // command gets a fresh index and therefore cannot observe stale files.\n const fleet = new FleetContext();\n\n // `sources` are paths into a service's own repository, so they only mean\n // something when loam is standing in that repository — which is exactly\n // what loam.json's `service` records.\n const repoOf = (service: string): string | undefined =>\n config.service === service ? process.cwd() : undefined;\n\n /** One service target, from a name the enumeration or the grammar approved. */\n const serviceTarget = (id: RawServiceId): Promise<TargetReport> =>\n guarded({ kind: \"service\", id }, () =>\n validateService({ docsDir, service: id, repoDir: repoOf(id), gherkinDir: config.gherkinDir, contracts: config.contracts, fleet }),\n );\n\n const targets: TargetReport[] = [];\n /** What the positional argument turned out to name, for the JSON payload. */\n let resolvedKind: \"service\" | \"feature\" | undefined;\n /** The `--base` narrowing, once it is known. Null on every unscoped run — including every run without --base. */\n let scope: ValidateScope | null = null;\n\n try {\n if (opts.all) {\n // ONE LikeC4 workspace for the whole run. The per-path load pays a\n // fresh Langium workspace per document (~100ms each even warm), which\n // made the fleet's main CI command O(documents) workspace spins —\n // 13.7s median over the 120-service benchmark (docs/BENCHMARKS.md).\n // So --all enumerates its documents up front — the landscape, every\n // service model, every active feature's delta — and batch-parses\n // them into the fleet context's memo; every loadLikeC4 below, the\n // landscape read included, is then a seeded hit. The enumerations are\n // the same memoized promises the target loops reuse. If the batch\n // CANNOT run (a sandbox denying tmpdir writes), prefetch seeds\n // nothing and every load falls back to today's per-path parse:\n // identical findings, the old speed. Single-service validate and\n // list keep their untouched code paths on purpose — the ≤10%\n // regression bound in docs/BENCHMARKS.md holds by construction.\n const lp = landscapeFile(docsDir);\n const enumerated = await listServices(docsDir, fleet);\n const active = await listFeatures(docsDir, {}, fleet);\n // `--base` narrows the enumerations and nothing else: a kept target is\n // graded by exactly the checks `--all` would have run on it, at the\n // same severities, through the same `--strict`. It matches through the\n // ENUMERATIONS' own directories — changed-paths.ts says why a split\n // on \"/\" silently drops every filed service.\n if (opts.base !== undefined) {\n const narrowed = await scopeSince({ docsDir, ref: opts.base, services: enumerated, features: active });\n if (narrowed.kind === \"refused\") {\n fail(json, narrowed.code, narrowed.message);\n return;\n }\n scope = narrowed;\n }\n const services = scope === null ? enumerated : scope.services;\n const features = scope === null ? active : scope.features;\n await fleet.prefetchLikeC4([\n ...(existsSync(lp) ? [lp] : []),\n ...services.filter((svc) => svc.has.model).map((svc) => servicePathsAt(svc.dir).model),\n ...features.filter((feat) => feat.has.delta).map((feat) => featurePaths(feat.dir).delta),\n ]);\n // Read whether or not the landscape is a TARGET: every service check\n // below joins against it, so a scoped run needs the map in hand even\n // when it does not grade the map itself.\n const land = existsSync(lp) ? await readLandscape(() => loadArchitecture(docsDir)) : null;\n // The fleet-level cross-check first: it frames everything below it, and a\n // service nobody drew is worth knowing before its own findings scroll past.\n // Under `--base` it is a target only when `architecture/` changed — the\n // landscape owns no service directory, so that is its whole footprint.\n if (scope === null || scope.landscape) {\n const landscape = await validateLandscape(docsDir, land, fleet);\n targets.push(landscape);\n // The agent contract check, --all only: AGENTS.md is written once and\n // never refreshed (the ownership contract), so the one thing the\n // docs-repo-wide mode owes it is detection — a stamp older than the\n // binary means agents are branching on tables the binary no longer\n // honours. It grades the repo, not any service, so it rides on the\n // landscape target.\n const agentsPath = agentsFile(docsDir);\n const agentsText = existsSync(agentsPath) ? await readFile(agentsPath, \"utf8\") : null;\n const agents = agentsStaleFinding(agentsText, LOAM_VERSION);\n if (agents !== null) landscape.findings.push(agents);\n // The other direction, and the one that changes what a PASS means:\n // this binary predates the corpus, so a directive it cannot parse is\n // read as prose and produces no join to fail.\n const behind = binaryBehindFinding(agentsText, LOAM_VERSION);\n if (behind !== null) landscape.findings.push(behind);\n }\n targets.push(\n ...(await inOrder(services, (svc) =>\n guarded({ kind: \"service\", id: svc.id }, () =>\n validateService({\n docsDir,\n service: svc.id,\n repoDir: repoOf(svc.id),\n preloaded: land,\n gherkinDir: config.gherkinDir,\n contracts: config.contracts,\n fleet,\n landscapeReported: true,\n }),\n ),\n )),\n );\n targets.push(\n ...(await inOrder(features, (feat) =>\n guarded({ kind: \"feature\", id: feat.id }, () =>\n validateFeature(docsDir, feat, land, fleet),\n ),\n )),\n );\n } else if (opts.feature) {\n const feature = await resolveFeature(docsDir, opts.feature, \"exclude\", fleet);\n if (!feature) {\n fail(json, \"unknown-target\", await missingFeatureMessage(docsDir, opts.feature, fleet));\n return;\n }\n targets.push(\n await guarded({ kind: \"feature\", id: feature.id }, () =>\n validateFeature(docsDir, feature, undefined, fleet),\n ),\n );\n } else if (target !== undefined) {\n // The positional reads the way `show` reads one: try the feature first\n // (ids like FEAT-101 are distinctive, service names are arbitrary), then\n // the service. --service/--feature stay as the explicit spellings for a\n // name that could be both — and when both readings exist, the run says\n // so out loud instead of silently taking one (`target.ambiguous`).\n const feature = await resolveFeature(docsDir, target, \"exclude\", fleet);\n const isService = (await listServices(docsDir, fleet)).some((s) => s.id === target);\n if (feature) {\n resolvedKind = \"feature\";\n const report = await guarded({ kind: \"feature\", id: feature.id }, () =>\n validateFeature(docsDir, feature, undefined, fleet),\n );\n if (isService) report.findings.unshift(ambiguousTarget(target, \"feature\"));\n targets.push(report);\n } else {\n // Neither reading exists. An archived feature is its own diagnosis\n // (\"already archived\", not \"no such thing\"); anything else reads as a\n // service, so the did-you-mean hints in service.unknown fire.\n if (!isService && (await resolveFeature(docsDir, target, \"only\", fleet)) !== null) {\n fail(json, \"unknown-target\", await missingFeatureMessage(docsDir, target, fleet));\n return;\n }\n resolvedKind = \"service\";\n const resolved = await resolveServiceTarget(docsDir, target, \"target\", fleet);\n if (!resolved.ok) return fail(json, \"invalid-option\", resolved.problem);\n targets.push(await serviceTarget(resolved.id));\n }\n } else {\n const service = opts.service ?? config.service;\n if (!service) {\n fail(json, \"invalid-option\", NO_SERVICE_MESSAGE);\n return;\n }\n const resolved = await resolveServiceTarget(docsDir, service, \"--service\", fleet);\n if (!resolved.ok) return fail(json, \"invalid-option\", resolved.problem);\n targets.push(await serviceTarget(resolved.id));\n }\n } catch (err) {\n // The docs repo went away between the gate above and the enumeration.\n if (err instanceof DocsRepoUnavailableError) {\n reportDocsRepoError(json, err);\n return;\n }\n // An IO failure that `guarded` could not localise to one target — it\n // happened in the ENUMERATION, which reads every living spec's\n // frontmatter to build the service list. There is no partial report to\n // give: without the list, no service was checked, including the ones\n // that are fine. Say which file, and refuse; the one thing that must\n // not happen is a stack trace on stdout's sibling stream and an\n // `internal` envelope that names nothing.\n reportRepositoryUnavailable(\n json,\n err,\n \"the service list itself is unknown and nothing was validated\",\n docsDir,\n );\n return;\n }\n\n // By subject, not by finding — the rule lives with the rollup in report.ts.\n const unverifiable = unverifiableSubjects(targets);\n // A journal in the repo outranks everything graded above: the docs may\n // be half-merged, and in CI a green over them merges. Led, not appended\n // — it is the reason nothing below can be trusted — and attached to the\n // first target because a finding lives in one.\n const interrupted = await interruptedCommitFinding(docsDir);\n if (interrupted !== null && targets.length > 0) targets[0]!.findings.unshift(interrupted);\n\n // The fleet scorecard, --all only: recomputed per invocation off the same\n // memoized reads this run already made — loam stores no history — and it\n // fails closed per axis, never whole (fleet/scorecard/scorecard.ts).\n //\n // Suppressed under `--base`: its denominators are the graded targets, so\n // over a narrowed set it reads \"2 of 2 services documented\" for a fleet\n // where ten more are not — and its axes license report.ts's grouping,\n // which would drop real warnings because an unlooked-at axis reads zero.\n const scorecard =\n opts.all && scope === null\n ? await buildScorecard({ docsDir, targets, fleet, boundService: config.service })\n : null;\n\n const valid = reportValid(targets);\n const capped = targets.map(capDetails);\n if (json) {\n emitJson({\n command: \"validate\",\n valid,\n summary: summary(targets),\n ...(resolvedKind === undefined ? {} : { resolvedKind }),\n // What this run looked at. Only under `--base`, so an unscoped payload\n // is today's document and its ABSENCE says `summary` counts the whole\n // fleet — scopeJson has why a scoped one may not stay silent.\n ...(scope === null ? {} : { scope: scopeJson(scope) }),\n // Emitted in every mode now that it is derived per service: a\n // single-service run knows exactly as much about its own blind spot\n // as `--all` knows about the fleet's.\n sourcesUnverifiableFromHere: unverifiable,\n ...(scorecard === null ? {} : { scorecard }),\n // What to do about every code this run raised — the one thing the\n // payload could not previously answer. `validate` is step 1 of every\n // generated protocol and the command an agent reads first, and until\n // now it replied in prose: a finding names a code and says what is\n // wrong, and nothing in it said that `loam explain <code>` knows what\n // to do next.\n //\n // A top-level MAP, not a field on each finding, for the reason\n // report.ts's DETAIL_LIMIT gives about evidence: a hundred-service\n // fleet raising one code a hundred times would ship the same\n // sentence a hundred times, in the interface an agent pipes into a\n // context window. The consumer joins `findings[].code` to\n // `fixes[code]` — one lookup, no round trip, no repetition.\n //\n // Keyed ONLY by codes a non-`ok` finding raised. A confirmation has\n // nothing to fix, and on a clean fleet the `ok` codes are the\n // overwhelming majority of findings; attaching fixes to them would\n // pay the whole vocabulary's byte cost to tell a reader that nothing\n // is wrong. `targets` is read here rather than `capped` because\n // capping truncates evidence, not findings — the two raise the same\n // codes, and reading the uncapped set keeps this rollup computed off\n // the same targets as `summary()`.\n //\n // Not behind a flag, deliberately. It is additive, so no consumer\n // that ignores it is affected; it is small, because it carries only\n // the handful of codes this run actually raised rather than the\n // 227-row vocabulary; and a flag would mean an agent has to already\n // know the fixes exist in order to ask for them — which is the exact\n // gap being closed. It is emitted on a green run too, as `{}`, so a\n // consumer never has to branch on the key's presence before looking\n // a code up in it.\n fixes: fixesFor(\n targets.flatMap((t) => t.findings.filter((f) => f.severity !== \"ok\").map((f) => f.code)),\n ),\n targets: capped.map(targetJson),\n });\n } else {\n // The scope FIRST: a reader has to know what the report below is a\n // report ABOUT, and in the zero-target case there is nothing below at\n // all — printed after the findings, the one run that most needs words\n // would read as a green over the whole system.\n if (scope !== null) console.log(scopeLine(scope));\n renderText(capped, {\n all: opts.all === true,\n errorsOnly: opts.errorsOnly === true,\n unverifiable,\n scorecard,\n });\n if (scorecard !== null) printScorecard(scorecard);\n }\n // --strict is a per-invocation CI lever: it fails the run on any error or\n // warning (ok-severity confirmations never trip it — virtually every\n // clean target emits some), and changes nothing else — `valid` keeps meaning \"no\n // errors\" in text and JSON alike, so two pipelines reading the same repo\n // may grade the same report differently and both are telling the truth.\n const strictFailed =\n opts.strict === true &&\n countSeverity(targets, \"error\") + countSeverity(targets, \"warn\") > 0;\n if (!valid || strictFailed) process.exitCode = 1;\n });\n}\n\nexport { targetValid };\n"]}
|
|
1
|
+
{"version":3,"file":"validate.js","sourceRoot":"","sources":["../../../src/commands/validate/validate.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `loam validate` — argument grammar, target selection, and the run's one\n * filesystem snapshot.\n *\n * The checks themselves live beside it, one module per target kind\n * (`landscape.ts`, `service/`, `feature/`) over the document checks they share\n * (`requirements.ts`). Every one of them produces findings and none of them\n * print: printing is `report.ts`'s, and it happens once, after the last target\n * is graded.\n */\nimport type { Command } from \"commander\";\nimport { existsSync } from \"node:fs\";\nimport { readFile } from \"node:fs/promises\";\nimport { agentsStaleFinding, binaryBehindFinding } from \"../../core/agent/agents-stamp.js\";\nimport { inOrder } from \"../../core/kernel/concurrency.js\";\nimport type { RawServiceId } from \"../../core/kernel/ids/service.js\";\nimport { resolveServiceTarget } from \"../../core/repo/service-target.js\";\nimport { loadConfig } from \"../../core/envelope/config.js\";\nimport { emitJson, fail, NO_SERVICE_MESSAGE, reportNoConfig } from \"../../core/envelope/json.js\";\nimport { DocsRepoUnavailableError } from \"../../core/repo/state.js\";\nimport { agentsPath as agentsFile, landscapePath as landscapeFile } from \"../../core/repo/paths.js\";\nimport { listFeatures, listServices, missingFeatureMessage, resolveFeature } from \"../../core/repo/repo.js\";\nimport {\n countSeverity,\n reportValid,\n targetJson,\n targetValid,\n type TargetReport,\n} from \"../../core/vocabulary/report.js\";\nimport { LOAM_VERSION } from \"../../core/envelope/version.js\";\nimport { FleetContext } from \"../../core/fleet-context.js\";\nimport { fixesFor } from \"../policy/format.js\";\nimport {\n docsRepoReady,\n reportDocsRepoError,\n reportRepositoryUnavailable,\n} from \"../policy/gate.js\";\nimport { interruptedCommitFinding } from \"../../core/staging/recovery/finding.js\";\nimport { scopeJson, scopeLine, scopeSince, type ValidateScope } from \"../../core/diff/scope/changed-paths.js\";\nimport { validateLandscape } from \"./fleet/landscape.js\";\nimport { prefetchFleetDocuments, readLandscape } from \"./fleet/load.js\";\nimport { buildScorecard } from \"./fleet/scorecard/scorecard.js\";\nimport { printScorecard } from \"./fleet/scorecard/print.js\";\nimport { validateFeature } from \"./feature.js\";\nimport { validateService } from \"./service/service.js\";\nimport { ambiguousTarget, capDetails, guarded, renderText, summary, unverifiableSubjects } from \"./report.js\";\n\ninterface ValidateOptions {\n service?: string;\n feature?: string;\n all?: boolean;\n base?: string;\n strict?: boolean;\n errorsOnly?: boolean;\n json?: boolean;\n}\n\nexport function registerValidate(program: Command): void {\n program\n .command(\"validate\")\n .argument(\"[target]\", \"service or feature id (a feature wins when both match; --service/--feature force the reading)\")\n .description(\"Validate a service (C4 + requirement coverage) or a feature (delta + coverage)\")\n .option(\"--service <id>\", \"service to validate (defaults to the configured service)\")\n .option(\"--feature <id>\", \"validate a feature delta instead of a service\")\n .option(\"--all\", \"validate every service and every active feature\")\n .option(\"--base <ref>\", \"with --all: grade only the targets changed since a base git ref of the docs repo — the adoption ratchet\")\n .option(\"--strict\", \"exit 1 on any warning too — a per-invocation CI lever; the report and --json payload do not change\")\n .option(\"--errors-only\", \"print only errors and warnings — a rendering lever; the --json payload does not change\")\n .option(\"--json\", \"emit the machine contract instead of the human view\")\n .action(async (target: string | undefined, opts: ValidateOptions) => {\n const json = opts.json === true;\n\n if (target !== undefined && (opts.all || opts.service || opts.feature)) {\n fail(json, \"invalid-option\", `'${target}' already names the target; drop --all/--service/--feature.`);\n return;\n }\n if (opts.all && (opts.service || opts.feature)) {\n fail(json, \"invalid-option\", \"--all validates everything; drop --service/--feature.\");\n return;\n }\n // Two targets is no target: silently validating only the feature taught\n // callers that --service had been honoured when it had been dropped.\n if (opts.service && opts.feature) {\n fail(json, \"invalid-option\", \"--service and --feature name different targets; pass one or the other.\");\n return;\n }\n // `--base` NARROWS a whole-fleet run; it does not name one. Beside a\n // positional, `--service` or `--feature` it is a scope over a scope — a\n // contradiction rather than a narrowing, whose \"this service if it\n // changed\" reading nobody could tell apart from a green over a service\n // never looked at. `--base` and not `--since`: `--base <ref>` is already\n // loam's word for a base git ref of the docs repo on diff and vouch.\n if (opts.base !== undefined && opts.all !== true) {\n fail(\n json,\n \"invalid-option\",\n target !== undefined || opts.service || opts.feature\n ? \"--base narrows --all to what changed since a ref; a named target is already a scope. Pass one or the other.\"\n : \"--base narrows --all to what changed since a ref — pass --all with it.\",\n );\n return;\n }\n\n const loaded = await loadConfig();\n if (loaded.kind !== \"loaded\") {\n reportNoConfig(json, loaded);\n return;\n }\n const config = loaded.config;\n const { docsDir } = config;\n // Before anything is enumerated: a docsDir that is not a docs repo must\n // refuse, not report an empty fleet. See docsRepoReady. `--feature` is the\n // one mode that never enumerates services, so it asks for less.\n if (!docsRepoReady(json, docsDir, opts.feature ? \"docs\" : \"services\")) return;\n // One invocation, one filesystem snapshot. Nothing is global: a later\n // command gets a fresh index and therefore cannot observe stale files.\n const fleet = new FleetContext();\n\n // `sources` are paths into a service's own repository, so they only mean\n // something when loam is standing in that repository — which is exactly\n // what loam.json's `service` records.\n const repoOf = (service: string): string | undefined =>\n config.service === service ? process.cwd() : undefined;\n\n /** One service target, from a name the enumeration or the grammar approved. */\n const serviceTarget = (id: RawServiceId): Promise<TargetReport> =>\n guarded({ kind: \"service\", id }, () =>\n validateService({ docsDir, service: id, repoDir: repoOf(id), gherkinDir: config.gherkinDir, contracts: config.contracts, fleet }),\n );\n\n const targets: TargetReport[] = [];\n /** What the positional argument turned out to name, for the JSON payload. */\n let resolvedKind: \"service\" | \"feature\" | undefined;\n /** The `--base` narrowing, once it is known. Null on every unscoped run — including every run without --base. */\n let scope: ValidateScope | null = null;\n\n try {\n if (opts.all) {\n // ONE workspace per document kind for the whole run — the block moved\n // to `fleet/load.ts` (`prefetchFleetDocuments`), which is where the\n // rule about which shape parses in which workspace belongs.\n const lp = landscapeFile(docsDir);\n const enumerated = await listServices(docsDir, fleet);\n const active = await listFeatures(docsDir, {}, fleet);\n // `--base` narrows the enumerations and nothing else: a kept target is\n // graded by exactly the checks `--all` would have run on it, at the\n // same severities, through the same `--strict`. It matches through the\n // ENUMERATIONS' own directories — changed-paths.ts says why a split\n // on \"/\" silently drops every filed service.\n if (opts.base !== undefined) {\n const narrowed = await scopeSince({ docsDir, ref: opts.base, services: enumerated, features: active });\n if (narrowed.kind === \"refused\") {\n fail(json, narrowed.code, narrowed.message);\n return;\n }\n scope = narrowed;\n }\n const services = scope === null ? enumerated : scope.services;\n const features = scope === null ? active : scope.features;\n await prefetchFleetDocuments({ docsDir, fleet, services, features });\n // Read whether or not the landscape is a TARGET: every service check\n // below joins against it, so a scoped run needs the map in hand even\n // when it does not grade the map itself. Through the fleet index, so\n // the single-service path — which now reads the same project — answers\n // out of the same memo rather than parsing it a second time.\n const land = existsSync(lp) ? await readLandscape(() => fleet.architecture(docsDir)) : null;\n // The fleet-level cross-check first: it frames everything below it, and a\n // service nobody drew is worth knowing before its own findings scroll past.\n // Under `--base` it is a target only when `architecture/` changed — the\n // landscape owns no service directory, so that is its whole footprint.\n if (scope === null || scope.landscape) {\n const landscape = await validateLandscape(docsDir, land, fleet);\n targets.push(landscape);\n // The agent contract check, --all only: AGENTS.md is written once and\n // never refreshed (the ownership contract), so the one thing the\n // docs-repo-wide mode owes it is detection — a stamp older than the\n // binary means agents are branching on tables the binary no longer\n // honours. It grades the repo, not any service, so it rides on the\n // landscape target.\n const agentsPath = agentsFile(docsDir);\n const agentsText = existsSync(agentsPath) ? await readFile(agentsPath, \"utf8\") : null;\n const agents = agentsStaleFinding(agentsText, LOAM_VERSION);\n if (agents !== null) landscape.findings.push(agents);\n // The other direction, and the one that changes what a PASS means:\n // this binary predates the corpus, so a directive it cannot parse is\n // read as prose and produces no join to fail.\n const behind = binaryBehindFinding(agentsText, LOAM_VERSION);\n if (behind !== null) landscape.findings.push(behind);\n }\n targets.push(\n ...(await inOrder(services, (svc) =>\n guarded({ kind: \"service\", id: svc.id }, () =>\n validateService({\n docsDir,\n service: svc.id,\n repoDir: repoOf(svc.id),\n preloaded: land,\n gherkinDir: config.gherkinDir,\n contracts: config.contracts,\n fleet,\n landscapeReported: true,\n }),\n ),\n )),\n );\n targets.push(\n ...(await inOrder(features, (feat) =>\n guarded({ kind: \"feature\", id: feat.id }, () =>\n validateFeature(docsDir, feat, land, fleet),\n ),\n )),\n );\n } else if (opts.feature) {\n const feature = await resolveFeature(docsDir, opts.feature, \"exclude\", fleet);\n if (!feature) {\n fail(json, \"unknown-target\", await missingFeatureMessage(docsDir, opts.feature, fleet));\n return;\n }\n targets.push(\n await guarded({ kind: \"feature\", id: feature.id }, () =>\n validateFeature(docsDir, feature, undefined, fleet),\n ),\n );\n } else if (target !== undefined) {\n // The positional reads the way `show` reads one: try the feature first\n // (ids like FEAT-101 are distinctive, service names are arbitrary), then\n // the service. --service/--feature stay as the explicit spellings for a\n // name that could be both — and when both readings exist, the run says\n // so out loud instead of silently taking one (`target.ambiguous`).\n const feature = await resolveFeature(docsDir, target, \"exclude\", fleet);\n const isService = (await listServices(docsDir, fleet)).some((s) => s.id === target);\n if (feature) {\n resolvedKind = \"feature\";\n const report = await guarded({ kind: \"feature\", id: feature.id }, () =>\n validateFeature(docsDir, feature, undefined, fleet),\n );\n if (isService) report.findings.unshift(ambiguousTarget(target, \"feature\"));\n targets.push(report);\n } else {\n // Neither reading exists. An archived feature is its own diagnosis\n // (\"already archived\", not \"no such thing\"); anything else reads as a\n // service, so the did-you-mean hints in service.unknown fire.\n if (!isService && (await resolveFeature(docsDir, target, \"only\", fleet)) !== null) {\n fail(json, \"unknown-target\", await missingFeatureMessage(docsDir, target, fleet));\n return;\n }\n resolvedKind = \"service\";\n const resolved = await resolveServiceTarget(docsDir, target, \"target\", fleet);\n if (!resolved.ok) return fail(json, \"invalid-option\", resolved.problem);\n targets.push(await serviceTarget(resolved.id));\n }\n } else {\n const service = opts.service ?? config.service;\n if (!service) {\n fail(json, \"invalid-option\", NO_SERVICE_MESSAGE);\n return;\n }\n const resolved = await resolveServiceTarget(docsDir, service, \"--service\", fleet);\n if (!resolved.ok) return fail(json, \"invalid-option\", resolved.problem);\n targets.push(await serviceTarget(resolved.id));\n }\n } catch (err) {\n // The docs repo went away between the gate above and the enumeration.\n if (err instanceof DocsRepoUnavailableError) {\n reportDocsRepoError(json, err);\n return;\n }\n // An IO failure that `guarded` could not localise to one target — it\n // happened in the ENUMERATION, which reads every living spec's\n // frontmatter to build the service list. There is no partial report to\n // give: without the list, no service was checked, including the ones\n // that are fine. Say which file, and refuse; the one thing that must\n // not happen is a stack trace on stdout's sibling stream and an\n // `internal` envelope that names nothing.\n reportRepositoryUnavailable(\n json,\n err,\n \"the service list itself is unknown and nothing was validated\",\n docsDir,\n );\n return;\n }\n\n // By subject, not by finding — the rule lives with the rollup in report.ts.\n const unverifiable = unverifiableSubjects(targets);\n // A journal in the repo outranks everything graded above: the docs may\n // be half-merged, and in CI a green over them merges. Led, not appended\n // — it is the reason nothing below can be trusted — and attached to the\n // first target because a finding lives in one.\n const interrupted = await interruptedCommitFinding(docsDir);\n if (interrupted !== null && targets.length > 0) targets[0]!.findings.unshift(interrupted);\n\n // The fleet scorecard, --all only: recomputed per invocation off the same\n // memoized reads this run already made — loam stores no history — and it\n // fails closed per axis, never whole (fleet/scorecard/scorecard.ts).\n //\n // Suppressed under `--base`: its denominators are the graded targets, so\n // over a narrowed set it reads \"2 of 2 services documented\" for a fleet\n // where ten more are not — and its axes license report.ts's grouping,\n // which would drop real warnings because an unlooked-at axis reads zero.\n const scorecard =\n opts.all && scope === null\n ? await buildScorecard({ docsDir, targets, fleet, boundService: config.service })\n : null;\n\n const valid = reportValid(targets);\n const capped = targets.map(capDetails);\n if (json) {\n emitJson({\n command: \"validate\",\n valid,\n summary: summary(targets),\n ...(resolvedKind === undefined ? {} : { resolvedKind }),\n // What this run looked at. Only under `--base`, so an unscoped payload\n // is today's document and its ABSENCE says `summary` counts the whole\n // fleet — scopeJson has why a scoped one may not stay silent.\n ...(scope === null ? {} : { scope: scopeJson(scope) }),\n // Emitted in every mode now that it is derived per service: a\n // single-service run knows exactly as much about its own blind spot\n // as `--all` knows about the fleet's.\n sourcesUnverifiableFromHere: unverifiable,\n ...(scorecard === null ? {} : { scorecard }),\n // What to do about every code this run raised — the one thing the\n // payload could not previously answer. `validate` is step 1 of every\n // generated protocol and the command an agent reads first, and until\n // now it replied in prose: a finding names a code and says what is\n // wrong, and nothing in it said that `loam explain <code>` knows what\n // to do next.\n //\n // A top-level MAP, not a field on each finding, for the reason\n // report.ts's DETAIL_LIMIT gives about evidence: a hundred-service\n // fleet raising one code a hundred times would ship the same\n // sentence a hundred times, in the interface an agent pipes into a\n // context window. The consumer joins `findings[].code` to\n // `fixes[code]` — one lookup, no round trip, no repetition.\n //\n // Keyed ONLY by codes a non-`ok` finding raised. A confirmation has\n // nothing to fix, and on a clean fleet the `ok` codes are the\n // overwhelming majority of findings; attaching fixes to them would\n // pay the whole vocabulary's byte cost to tell a reader that nothing\n // is wrong. `targets` is read here rather than `capped` because\n // capping truncates evidence, not findings — the two raise the same\n // codes, and reading the uncapped set keeps this rollup computed off\n // the same targets as `summary()`.\n //\n // Not behind a flag, deliberately. It is additive, so no consumer\n // that ignores it is affected; it is small, because it carries only\n // the handful of codes this run actually raised rather than the\n // 227-row vocabulary; and a flag would mean an agent has to already\n // know the fixes exist in order to ask for them — which is the exact\n // gap being closed. It is emitted on a green run too, as `{}`, so a\n // consumer never has to branch on the key's presence before looking\n // a code up in it.\n fixes: fixesFor(\n targets.flatMap((t) => t.findings.filter((f) => f.severity !== \"ok\").map((f) => f.code)),\n ),\n targets: capped.map(targetJson),\n });\n } else {\n // The scope FIRST: a reader has to know what the report below is a\n // report ABOUT, and in the zero-target case there is nothing below at\n // all — printed after the findings, the one run that most needs words\n // would read as a green over the whole system.\n if (scope !== null) console.log(scopeLine(scope));\n renderText(capped, {\n all: opts.all === true,\n errorsOnly: opts.errorsOnly === true,\n unverifiable,\n scorecard,\n });\n if (scorecard !== null) printScorecard(scorecard);\n }\n // --strict is a per-invocation CI lever: it fails the run on any error or\n // warning (ok-severity confirmations never trip it — virtually every\n // clean target emits some), and changes nothing else — `valid` keeps meaning \"no\n // errors\" in text and JSON alike, so two pipelines reading the same repo\n // may grade the same report differently and both are telling the truth.\n const strictFailed =\n opts.strict === true &&\n countSeverity(targets, \"error\") + countSeverity(targets, \"warn\") > 0;\n if (!valid || strictFailed) process.exitCode = 1;\n });\n}\n\nexport { targetValid };\n"]}
|
|
@@ -46,6 +46,7 @@ services/<svc>/
|
|
|
46
46
|
arch.spec.md its living ARCHITECTURE requirements (outbox, retries, alerts)
|
|
47
47
|
openapi.yaml its API contract
|
|
48
48
|
asyncapi.yaml its AsyncAPI 3 event contract
|
|
49
|
+
usecases/<name>.likec4 optional flows INSIDE this boundary — dynamic views over its containers, graded when tagged #req- (any .likec4 beside model.likec4 is read the same way)
|
|
49
50
|
adrs/ runbook.md health.yaml why it is like this, how to run it
|
|
50
51
|
features/<FEAT>/ a change in flight
|
|
51
52
|
intent.md why, in business terms
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"artifacts.js","sourceRoot":"","sources":["../../../../src/core/agent/agents-md/artifacts.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,CAAC,MAAM,SAAS,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAqMxB,CAAC","sourcesContent":["/**\n * The document's opening arc: what the docs repo HOLDS — the layout, how its\n * documents link to each other, the glossary, architectural obligations, and\n * the `loam.json` wiring every repository commits.\n *\n * The three sections that used to close this file — the architecture spec\n * axis, the generated Gherkin suite, and the frontmatter/vouch chain — are now\n * the `loam-authoring` reference page (../workflows/reference/authoring.ts),\n * printed by `loam instructions loam-authoring` rather than written into\n * AGENTS.md. They are grammars consulted while writing ONE document, and the\n * file they were in is auto-loaded on every session by hosts that truncate it\n * silently; ./command-map.ts's header carries the measurement that decided it.\n * What is left here is what an agent must know before it can form a question\n * at all.\n *\n * One section of the AGENTS.md template. ../agents-md.ts assembles the\n * document by PLAIN CONCATENATION — no join separator — so every section\n * starts at the first character of its opening line and ends with the newline\n * that closes its last one. Keep that shape when editing, or two sections glue\n * onto one line in every docs repo loam scaffolds from now on.\n */\nexport const ARTIFACTS = `# Working in this docs repo\n\nThis is a **loam** docs repo: the shared source of truth for a governed software\nsystem — its architecture, requirements, contracts and evidence. Everything here\nis plain files. \\`loam\\` reads and writes them; delete \\`loam\\` and the docs remain.\n\n**The bar this artifact set aims at is reproducibility**: a reader should be able\nto answer, from these files alone — what each governed boundary exposes, what it\nreaches, what shapes it exchanges, how it is run, and what pages whom —\nwithout opening the code. \\`loam validate\\` grades form and joins, never depth:\ngreen means the files agree with each other, and the bar is what \"done\" is\nmeasured against — a thin baseline that validates is thin, not done.\n\n## Layout\n\n\\`\\`\\`\narchitecture/landscape.likec4 the living C4 model of the whole system\narchitecture/permissions.yaml optional system-wide authorization vocabulary\narchitecture/obligations.yaml optional ARCHITECTURAL obligations (#obl-<name> on the map)\narchitecture/adrs/NNNN-*.md optional SYSTEM-level decisions (MADR) — only their links are graded\nglossary/<term>.md optional domain vocabulary — one file per term, nesting allowed\nservices/<svc>/\n model.likec4 this governed boundary's C4\n spec.md its living requirements (current state)\n arch.spec.md its living ARCHITECTURE requirements (outbox, retries, alerts)\n openapi.yaml its API contract\n asyncapi.yaml its AsyncAPI 3 event contract\n adrs/ runbook.md health.yaml why it is like this, how to run it\nfeatures/<FEAT>/ a change in flight\n intent.md why, in business terms\n delta.likec4 the architecture change, tagged #<FEAT>\n specs/<svc>/spec.md the requirement change for one governed boundary\n specs/<svc>/arch.spec.md the architectural requirement change, same delta grammar\n specs/<svc>/openapi.yaml the endpoints this feature adds\n specs/<svc>/asyncapi.yaml the event-contract delta (optional; a complete AsyncAPI 3 document)\n verification.yaml what was checked once the code was built\nfeatures/archive/<FEAT>/ shipped changes — the evolution history\n\\`\\`\\`\n\n## Linking between documents\n\nDocuments here link to each other with **standard markdown links**, and the\ntarget is a relative path to the file:\n\n\\`\\`\\`markdown\n[0001 — transactional outbox](../../architecture/adrs/0001-transactional-outbox.md)\n\\`\\`\\`\n\nTwo reasons, and the second is the one that decides it. A markdown link renders\nas a link in pull-request review, which is where these documents are actually\nread, while \\`[[0001 transactional outbox]]\\` renders there as the literal\nbrackets somebody typed. And its target is a real relative path, so \"does this\nlink resolve\" is a filesystem question with a yes-or-no answer — where resolving\na wikilink means reimplementing Obsidian's shortest-unique-path search across the\nwhole repo, which is guessing, and loam refuses and names rather than guessing.\n\nYou lose nothing by it. Obsidian reads a repo written this way with no\nintegration at all — graph view, backlinks and search all work on markdown links\n— and its per-vault \\`Use [[Wikilinks]]\\` setting turns OFF, after which its own\nautocomplete and rename-tracking produce markdown links too. The authoring\ncomfort is a toggle in somebody's editor, not a decision this repo carries.\n\n**\\`loam validate\\` resolves these links** (\\`link.unresolved\\`, error): a relative\ntarget that names nothing is a broken join, reported once per document with\nevery bad link and its line. A link here is a **join**, not decoration — a\nrequirement that names a term wants that term's document linked, and an ADR that\nsupersedes another wants to say which, the same kind of relationship\n\\`Operations:\\` and \\`Covers:\\` already state.\n\nThree things are deliberately NOT graded, so write them freely: a target outside\nthis repository (\\`../../some-service/README.md\\` — that tree may not be checked\nout beside these documents), a link to a heading (\\`#section\\`), and any link\ninside a fenced code block or an inline code span, which is how a document shows\nthe convention without being convicted for it.\n\nCASE **is** graded, and deliberately: \\`[Order](Order.md)\\` beside a file called\n\\`order.md\\` resolves on Windows and macOS and 404s on GitHub and every Linux\nrunner, so the message names the stored spelling on all of them alike.\n\n## The glossary — the system's domain words\n\n\\`glossary/<term>.md\\`, one file per term, nesting allowed\n(\\`glossary/payments/authorization.md\\` is the term \\`payments/authorization\\`). There is\nno \\`glossary.yaml\\`: a definition is prose, and the DIRECTORY is the list. The\ndirectory's existence is the opt-in — a system without one hears nothing.\n\n**A term is checkable because a link is a join.** Cite it from the document that\nuses the word — a requirement, a capability document, a feature's intent — with an\nordinary relative link:\n\n\\`\\`\\`markdown\nAn [Order](../../glossary/order.md) is what a customer has confirmed.\n\\`\\`\\`\n\nA citation that does not resolve is \\`link.unresolved\\` like any other; a term nothing\noutside \\`glossary/\\` cites is \\`glossary.unlinked\\` (warn). Terms citing each other is\nnormal and does NOT count as adoption — a glossary is a network of definitions, and\ntwo terms defining each other say nothing about whether the system uses either word.\n\\`loam list glossary\\` prints every term with the documents that cite it.\n\n**A feature may bring a new word with it**: write it at\n\\`features/<FEAT>/glossary/<term>.md\\` and \\`loam archive\\` copies it into \\`glossary/\\`,\n\\`loam unarchive\\` takes it back out. This route is CREATE-ONLY — a term the living\nglossary already defines is \\`glossary.term-exists\\`, which \\`--approve\\` does not\noverride, because the merge is a whole-file copy and would replace an authored\ndefinition wholesale. To CHANGE a definition, edit \\`glossary/<term>.md\\` directly in\nthe same pull request, where git produces an ordinary conflict.\n\nCite the new word at its FUTURE living path (\\`../../../../glossary/order.md\\` from a\n\\`specs/<svc>/\\` delta) — that link resolves while the feature is in flight, and the\nmerge re-expresses it from wherever the text lands, so it keeps resolving afterwards.\n\nWrite the definition for a reader who knows the business and not this system: what the\nword means, and what it is NOT (an order is not a cart). Never which service owns it —\nthat is the system map's question and it changes.\n\n\n## Architectural obligations — what the architect hands the team\n\nAn architect has always had one checked channel here: an edge carrying\n\\`metadata { op 'authorizePayment' }\\` obliges the provider to define that operationId,\nor \\`spine.op-undefined\\` fails the gate. Obligations are the same handoff for the rules\nthat VARY — an outbox on this publisher and not that one, a circuit breaker on two\nedges out of five — and they are three separate things on purpose:\n\n- an **ADR** in \\`architecture/adrs/\\` says WHAT was decided, as thin or as thick as the\n decision needs;\n- a **\\`#obl-<name>\\` tag** on an element or an edge in \\`architecture/landscape.likec4\\`\n says WHERE it applies, so one ADR governs three edges and not the fourth without the\n document forking;\n- **\\`architecture/obligations.yaml\\`** declares the names, so a mistyped tag is an error\n rather than a word nobody notices.\n\n\\`\\`\\`yaml\nobligations:\n outbox:\n description: Publishers write the event and the state change in one transaction.\n adr: architecture/adrs/0001-transactional-outbox.md # relative to the docs repo root\n\\`\\`\\`\n\n**Your side of it is \\`Covers:\\`**. A tagged object with no living requirement covering it\nis \\`obligation.uncovered\\` (warn), and the finding names the service, the object and the\nexact line to write — \\`Covers: <element>\\` or \\`Covers: <source> -> <target>\\` in that\nservice's \\`arch.spec.md\\`, with a scenario that proves it. An id is its own tag suffix,\nso it may hold letters, digits, \\`_\\` and \\`-\\` only.\n\n## \\`loam.json\\` — the wiring, in every repo\n\nA governed system is **N + 1 repositories**: this docs repo, and one per governed\nimplementation boundary. Paths and commands call that boundary a \\`service\\`; it\nmay be a modular monolith, a network service, a CLI or a worker. Every repo\ncommits its own \\`loam.json\\`, so an agent, human or CI runner can find the rest\nof the system. There is no global config; without a \\`loam.json\\`, commands refuse\nwith \\`no-config\\` rather than guessing.\n\n\\`\\`\\`json\n{ \"docsDir\": \"../docs-repo\", \"service\": \"payment-service\", \"gherkinDir\": \"features\" }\n\\`\\`\\`\n\n- \\`docsDir\\` — where the docs repo is, **stored exactly as it was passed** and\n resolved against the directory holding the \\`loam.json\\`. So \\`../docs-repo\\`\n keeps working on every machine that checks the repos out side by side, which\n is the point of committing the file. An absolute path is a warning\n (\\`doctor.docs-absolute\\`): it resolves only on the machine that ran \\`loam init\\`.\n The docs repo's own \\`loam.json\\` says \\`\"docsDir\": \".\"\\`.\n- \\`service\\` — the canonical id of the governed boundary THIS repo contains, i.e. the\n \\`services/<id>/\\` directory it is allowed to speak for. \\`loam vouch\\`,\n \\`loam gherkin\\` and \\`loam verify --service\\` when writing with \\`--record\\`,\n \\`--results\\` or \\`--contract-results\\` bind to it; without it they refuse\n (\\`repository-unavailable\\`) rather than write another boundary's documents\n from a repo that is not that boundary's. A read-only verify checklist needs no\n binding and works from the docs repo.\n- \\`gherkinDir\\` — optional, default \\`features\\`: the directory \\`loam gherkin\\`\n writes its own \\`loam/\\` subtree into.\n- \\`agentTools\\` — optional: the agent tools \\`loam init\\` has written command and\n skill files for here, accumulated across runs. Written by \\`init\\`, read by\n \\`loam doctor\\`; nothing else depends on it, and a config without it loads.\n\n**Step 0 of everything below, once per repo:**\n\n\\`\\`\\`sh\nloam init --docs ../docs-repo --service payment-service # in EACH service repo\nloam doctor # before you trust any of it\n\\`\\`\\`\n\n\\`loam init --docs <dir>\\` **joins** an existing docs repo; creating a new one\ntakes \\`--create\\`, so a mistyped path cannot quietly scaffold a second source of\ntruth beside the real one. \\`loam doctor\\` is the read-only preflight: it reads\nloam.json defensively, resolves \\`docsDir\\`, probes access, checks \\`services/\\` and\nthe system map, and reports the service binding — run it first when anything\nbehaves as though the system were empty.\n\n**Living vs delta.** \\`services/<svc>/spec.md\\` is the complete current state.\n\\`features/<FEAT>/specs/<svc>/spec.md\\` is a diff against it, reviewed as a diff, with\nrequirements grouped under \\`## ADDED\\`, \\`## MODIFIED\\` or \\`## REMOVED Requirements\\`.\n\\`loam archive\\` folds the delta into the living state.\n\n`;\n"]}
|
|
1
|
+
{"version":3,"file":"artifacts.js","sourceRoot":"","sources":["../../../../src/core/agent/agents-md/artifacts.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,CAAC,MAAM,SAAS,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAsMxB,CAAC","sourcesContent":["/**\n * The document's opening arc: what the docs repo HOLDS — the layout, how its\n * documents link to each other, the glossary, architectural obligations, and\n * the `loam.json` wiring every repository commits.\n *\n * The three sections that used to close this file — the architecture spec\n * axis, the generated Gherkin suite, and the frontmatter/vouch chain — are now\n * the `loam-authoring` reference page (../workflows/reference/authoring.ts),\n * printed by `loam instructions loam-authoring` rather than written into\n * AGENTS.md. They are grammars consulted while writing ONE document, and the\n * file they were in is auto-loaded on every session by hosts that truncate it\n * silently; ./command-map.ts's header carries the measurement that decided it.\n * What is left here is what an agent must know before it can form a question\n * at all.\n *\n * One section of the AGENTS.md template. ../agents-md.ts assembles the\n * document by PLAIN CONCATENATION — no join separator — so every section\n * starts at the first character of its opening line and ends with the newline\n * that closes its last one. Keep that shape when editing, or two sections glue\n * onto one line in every docs repo loam scaffolds from now on.\n */\nexport const ARTIFACTS = `# Working in this docs repo\n\nThis is a **loam** docs repo: the shared source of truth for a governed software\nsystem — its architecture, requirements, contracts and evidence. Everything here\nis plain files. \\`loam\\` reads and writes them; delete \\`loam\\` and the docs remain.\n\n**The bar this artifact set aims at is reproducibility**: a reader should be able\nto answer, from these files alone — what each governed boundary exposes, what it\nreaches, what shapes it exchanges, how it is run, and what pages whom —\nwithout opening the code. \\`loam validate\\` grades form and joins, never depth:\ngreen means the files agree with each other, and the bar is what \"done\" is\nmeasured against — a thin baseline that validates is thin, not done.\n\n## Layout\n\n\\`\\`\\`\narchitecture/landscape.likec4 the living C4 model of the whole system\narchitecture/permissions.yaml optional system-wide authorization vocabulary\narchitecture/obligations.yaml optional ARCHITECTURAL obligations (#obl-<name> on the map)\narchitecture/adrs/NNNN-*.md optional SYSTEM-level decisions (MADR) — only their links are graded\nglossary/<term>.md optional domain vocabulary — one file per term, nesting allowed\nservices/<svc>/\n model.likec4 this governed boundary's C4\n spec.md its living requirements (current state)\n arch.spec.md its living ARCHITECTURE requirements (outbox, retries, alerts)\n openapi.yaml its API contract\n asyncapi.yaml its AsyncAPI 3 event contract\n usecases/<name>.likec4 optional flows INSIDE this boundary — dynamic views over its containers, graded when tagged #req- (any .likec4 beside model.likec4 is read the same way)\n adrs/ runbook.md health.yaml why it is like this, how to run it\nfeatures/<FEAT>/ a change in flight\n intent.md why, in business terms\n delta.likec4 the architecture change, tagged #<FEAT>\n specs/<svc>/spec.md the requirement change for one governed boundary\n specs/<svc>/arch.spec.md the architectural requirement change, same delta grammar\n specs/<svc>/openapi.yaml the endpoints this feature adds\n specs/<svc>/asyncapi.yaml the event-contract delta (optional; a complete AsyncAPI 3 document)\n verification.yaml what was checked once the code was built\nfeatures/archive/<FEAT>/ shipped changes — the evolution history\n\\`\\`\\`\n\n## Linking between documents\n\nDocuments here link to each other with **standard markdown links**, and the\ntarget is a relative path to the file:\n\n\\`\\`\\`markdown\n[0001 — transactional outbox](../../architecture/adrs/0001-transactional-outbox.md)\n\\`\\`\\`\n\nTwo reasons, and the second is the one that decides it. A markdown link renders\nas a link in pull-request review, which is where these documents are actually\nread, while \\`[[0001 transactional outbox]]\\` renders there as the literal\nbrackets somebody typed. And its target is a real relative path, so \"does this\nlink resolve\" is a filesystem question with a yes-or-no answer — where resolving\na wikilink means reimplementing Obsidian's shortest-unique-path search across the\nwhole repo, which is guessing, and loam refuses and names rather than guessing.\n\nYou lose nothing by it. Obsidian reads a repo written this way with no\nintegration at all — graph view, backlinks and search all work on markdown links\n— and its per-vault \\`Use [[Wikilinks]]\\` setting turns OFF, after which its own\nautocomplete and rename-tracking produce markdown links too. The authoring\ncomfort is a toggle in somebody's editor, not a decision this repo carries.\n\n**\\`loam validate\\` resolves these links** (\\`link.unresolved\\`, error): a relative\ntarget that names nothing is a broken join, reported once per document with\nevery bad link and its line. A link here is a **join**, not decoration — a\nrequirement that names a term wants that term's document linked, and an ADR that\nsupersedes another wants to say which, the same kind of relationship\n\\`Operations:\\` and \\`Covers:\\` already state.\n\nThree things are deliberately NOT graded, so write them freely: a target outside\nthis repository (\\`../../some-service/README.md\\` — that tree may not be checked\nout beside these documents), a link to a heading (\\`#section\\`), and any link\ninside a fenced code block or an inline code span, which is how a document shows\nthe convention without being convicted for it.\n\nCASE **is** graded, and deliberately: \\`[Order](Order.md)\\` beside a file called\n\\`order.md\\` resolves on Windows and macOS and 404s on GitHub and every Linux\nrunner, so the message names the stored spelling on all of them alike.\n\n## The glossary — the system's domain words\n\n\\`glossary/<term>.md\\`, one file per term, nesting allowed\n(\\`glossary/payments/authorization.md\\` is the term \\`payments/authorization\\`). There is\nno \\`glossary.yaml\\`: a definition is prose, and the DIRECTORY is the list. The\ndirectory's existence is the opt-in — a system without one hears nothing.\n\n**A term is checkable because a link is a join.** Cite it from the document that\nuses the word — a requirement, a capability document, a feature's intent — with an\nordinary relative link:\n\n\\`\\`\\`markdown\nAn [Order](../../glossary/order.md) is what a customer has confirmed.\n\\`\\`\\`\n\nA citation that does not resolve is \\`link.unresolved\\` like any other; a term nothing\noutside \\`glossary/\\` cites is \\`glossary.unlinked\\` (warn). Terms citing each other is\nnormal and does NOT count as adoption — a glossary is a network of definitions, and\ntwo terms defining each other say nothing about whether the system uses either word.\n\\`loam list glossary\\` prints every term with the documents that cite it.\n\n**A feature may bring a new word with it**: write it at\n\\`features/<FEAT>/glossary/<term>.md\\` and \\`loam archive\\` copies it into \\`glossary/\\`,\n\\`loam unarchive\\` takes it back out. This route is CREATE-ONLY — a term the living\nglossary already defines is \\`glossary.term-exists\\`, which \\`--approve\\` does not\noverride, because the merge is a whole-file copy and would replace an authored\ndefinition wholesale. To CHANGE a definition, edit \\`glossary/<term>.md\\` directly in\nthe same pull request, where git produces an ordinary conflict.\n\nCite the new word at its FUTURE living path (\\`../../../../glossary/order.md\\` from a\n\\`specs/<svc>/\\` delta) — that link resolves while the feature is in flight, and the\nmerge re-expresses it from wherever the text lands, so it keeps resolving afterwards.\n\nWrite the definition for a reader who knows the business and not this system: what the\nword means, and what it is NOT (an order is not a cart). Never which service owns it —\nthat is the system map's question and it changes.\n\n\n## Architectural obligations — what the architect hands the team\n\nAn architect has always had one checked channel here: an edge carrying\n\\`metadata { op 'authorizePayment' }\\` obliges the provider to define that operationId,\nor \\`spine.op-undefined\\` fails the gate. Obligations are the same handoff for the rules\nthat VARY — an outbox on this publisher and not that one, a circuit breaker on two\nedges out of five — and they are three separate things on purpose:\n\n- an **ADR** in \\`architecture/adrs/\\` says WHAT was decided, as thin or as thick as the\n decision needs;\n- a **\\`#obl-<name>\\` tag** on an element or an edge in \\`architecture/landscape.likec4\\`\n says WHERE it applies, so one ADR governs three edges and not the fourth without the\n document forking;\n- **\\`architecture/obligations.yaml\\`** declares the names, so a mistyped tag is an error\n rather than a word nobody notices.\n\n\\`\\`\\`yaml\nobligations:\n outbox:\n description: Publishers write the event and the state change in one transaction.\n adr: architecture/adrs/0001-transactional-outbox.md # relative to the docs repo root\n\\`\\`\\`\n\n**Your side of it is \\`Covers:\\`**. A tagged object with no living requirement covering it\nis \\`obligation.uncovered\\` (warn), and the finding names the service, the object and the\nexact line to write — \\`Covers: <element>\\` or \\`Covers: <source> -> <target>\\` in that\nservice's \\`arch.spec.md\\`, with a scenario that proves it. An id is its own tag suffix,\nso it may hold letters, digits, \\`_\\` and \\`-\\` only.\n\n## \\`loam.json\\` — the wiring, in every repo\n\nA governed system is **N + 1 repositories**: this docs repo, and one per governed\nimplementation boundary. Paths and commands call that boundary a \\`service\\`; it\nmay be a modular monolith, a network service, a CLI or a worker. Every repo\ncommits its own \\`loam.json\\`, so an agent, human or CI runner can find the rest\nof the system. There is no global config; without a \\`loam.json\\`, commands refuse\nwith \\`no-config\\` rather than guessing.\n\n\\`\\`\\`json\n{ \"docsDir\": \"../docs-repo\", \"service\": \"payment-service\", \"gherkinDir\": \"features\" }\n\\`\\`\\`\n\n- \\`docsDir\\` — where the docs repo is, **stored exactly as it was passed** and\n resolved against the directory holding the \\`loam.json\\`. So \\`../docs-repo\\`\n keeps working on every machine that checks the repos out side by side, which\n is the point of committing the file. An absolute path is a warning\n (\\`doctor.docs-absolute\\`): it resolves only on the machine that ran \\`loam init\\`.\n The docs repo's own \\`loam.json\\` says \\`\"docsDir\": \".\"\\`.\n- \\`service\\` — the canonical id of the governed boundary THIS repo contains, i.e. the\n \\`services/<id>/\\` directory it is allowed to speak for. \\`loam vouch\\`,\n \\`loam gherkin\\` and \\`loam verify --service\\` when writing with \\`--record\\`,\n \\`--results\\` or \\`--contract-results\\` bind to it; without it they refuse\n (\\`repository-unavailable\\`) rather than write another boundary's documents\n from a repo that is not that boundary's. A read-only verify checklist needs no\n binding and works from the docs repo.\n- \\`gherkinDir\\` — optional, default \\`features\\`: the directory \\`loam gherkin\\`\n writes its own \\`loam/\\` subtree into.\n- \\`agentTools\\` — optional: the agent tools \\`loam init\\` has written command and\n skill files for here, accumulated across runs. Written by \\`init\\`, read by\n \\`loam doctor\\`; nothing else depends on it, and a config without it loads.\n\n**Step 0 of everything below, once per repo:**\n\n\\`\\`\\`sh\nloam init --docs ../docs-repo --service payment-service # in EACH service repo\nloam doctor # before you trust any of it\n\\`\\`\\`\n\n\\`loam init --docs <dir>\\` **joins** an existing docs repo; creating a new one\ntakes \\`--create\\`, so a mistyped path cannot quietly scaffold a second source of\ntruth beside the real one. \\`loam doctor\\` is the read-only preflight: it reads\nloam.json defensively, resolves \\`docsDir\\`, probes access, checks \\`services/\\` and\nthe system map, and reports the service binding — run it first when anything\nbehaves as though the system were empty.\n\n**Living vs delta.** \\`services/<svc>/spec.md\\` is the complete current state.\n\\`features/<FEAT>/specs/<svc>/spec.md\\` is a diff against it, reviewed as a diff, with\nrequirements grouped under \\`## ADDED\\`, \\`## MODIFIED\\` or \\`## REMOVED Requirements\\`.\n\\`loam archive\\` folds the delta into the living state.\n\n`;\n"]}
|
|
@@ -163,6 +163,7 @@ The map:
|
|
|
163
163
|
`;
|
|
164
164
|
const MAP_REST = `- \`loam validate --service <id>\` grades one service's own axes: \`service.unknown\`,
|
|
165
165
|
\`service.no-model\`, \`service.no-spec\`, \`service.no-openapi\`, \`c4.invalid\`,
|
|
166
|
+
\`c4.declaration-diverged\`, \`c4.element-unowned\`,
|
|
166
167
|
\`c4.no-relationships\`, \`requirements.missing-scenarios\`,
|
|
167
168
|
\`requirements.stepless-scenario\`, \`requirements.assertionless-scenario\`,
|
|
168
169
|
\`requirements.examples-unbound\`, \`requirements.examples-unreferenced\`,
|
|
@@ -220,7 +221,21 @@ const MAP_REST = `- \`loam validate --service <id>\` grades one service's own ax
|
|
|
220
221
|
\`landscape.platform-candidate\`, \`landscape.datastore-private\` and
|
|
221
222
|
\`landscape.datastore-shared\`, \`service.id-invalid\` (fleet scope only, and
|
|
222
223
|
graded before the map is opened, so it stands even when the landscape is
|
|
223
|
-
missing or unreadable),
|
|
224
|
+
missing or unreadable), the four renderer-wiring warnings a
|
|
225
|
+
\`loam subsystem sync\` repairs — \`service.likec4-config-missing\` (a model that
|
|
226
|
+
STANDS ALONE, declaring its own \`specification\`, with no
|
|
227
|
+
\`likec4.config.json\` beside it: it renders nowhere from the docs root),
|
|
228
|
+
\`service.likec4-config-stray\` (that file beside a model that EXTENDS the map,
|
|
229
|
+
where it registers a project that CLAIMS the model, out of the root project
|
|
230
|
+
where the map's kinds are),
|
|
231
|
+
\`service.model-excluded\` (the root \`likec4.config.json\` excludes an extending
|
|
232
|
+
model's directory, so the renderer never loads it) and
|
|
233
|
+
\`service.model-unexcluded\` (it does NOT exclude a standalone model's, so the
|
|
234
|
+
renderer merges that model into the map and calls every declaration a
|
|
235
|
+
duplicate) — plus \`c4.fleet-project-invalid\` (warn: every document reads clean
|
|
236
|
+
where loam grades it and the ONE project the renderer builds out of the map and
|
|
237
|
+
every extending model does not, because something is declared in two of them),
|
|
238
|
+
a per-service \`sources.unverifiable-from-here\`, and
|
|
224
239
|
one check on the docs repo's own AGENTS.md, \`agents.stale\`. That last one is
|
|
225
240
|
the reason to distrust THAT file rather than this page: it fires when its
|
|
226
241
|
version stamp on line 1 (\`<!-- generated by loam vX.Y.Z -->\`) is missing or
|
|
@@ -300,7 +315,8 @@ const MAP_REST = `- \`loam validate --service <id>\` grades one service's own ax
|
|
|
300
315
|
"who calls whom" is the one thing no generator can read off a repository.
|
|
301
316
|
The seeded landscape carries a line-1 stamp; while it stands unedited,
|
|
302
317
|
editing fleet.yaml and re-running regenerates the map, and the first hand
|
|
303
|
-
edit
|
|
318
|
+
edit anywhere in the file, including above the stub, makes it yours — seed
|
|
319
|
+
then refuses \`seed-landscape-edited\`
|
|
304
320
|
rather than overwrite. Then adopt each service: \`loam adopt --service <id>\`.
|
|
305
321
|
- \`loam instructions [<page>] [args...]\` prints one of the six workflow
|
|
306
322
|
protocols — \`loam-adopt\`, \`loam-feature\`, \`loam-implement\`, \`loam-check\`,
|