@ybotok/loam 0.2.0-alpha.1 → 0.2.0-alpha.2
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 +87 -0
- package/README.md +6 -6
- package/ROADMAP.md +123 -18
- package/dist/commands/archive/plan/gate.js +18 -12
- package/dist/commands/archive/plan/gate.js.map +1 -1
- package/dist/commands/archive/plan/landscape.js +19 -0
- package/dist/commands/archive/plan/landscape.js.map +1 -1
- package/dist/commands/archive/run.js +2 -1
- package/dist/commands/archive/run.js.map +1 -1
- package/dist/commands/validate/arch-coverage.js +11 -1
- package/dist/commands/validate/arch-coverage.js.map +1 -1
- package/dist/commands/validate/fleet/obligations.js +43 -9
- package/dist/commands/validate/fleet/obligations.js.map +1 -1
- package/dist/commands/validate/fleet/usecases/steps.js +3 -3
- package/dist/commands/validate/fleet/usecases/steps.js.map +1 -1
- package/dist/commands/validate/service/service.js +1 -0
- package/dist/commands/validate/service/service.js.map +1 -1
- package/dist/commands/validate/service/specs.js +33 -1
- package/dist/commands/validate/service/specs.js.map +1 -1
- package/dist/core/agent/workflows/check.js +3 -0
- package/dist/core/agent/workflows/check.js.map +1 -1
- package/dist/core/brief/unchecked.js +1 -0
- package/dist/core/brief/unchecked.js.map +1 -1
- package/dist/core/c4/arch.js +72 -180
- package/dist/core/c4/arch.js.map +1 -1
- package/dist/core/c4/likec4.js +2 -0
- package/dist/core/c4/likec4.js.map +1 -1
- package/dist/core/c4/parsed/deployment.js +158 -0
- package/dist/core/c4/parsed/deployment.js.map +1 -0
- package/dist/core/c4/project/load.js +4 -0
- package/dist/core/c4/project/load.js.map +1 -1
- package/dist/core/c4/project/staged.js +220 -0
- package/dist/core/c4/project/staged.js.map +1 -0
- package/dist/core/c4/resolve/service.js +46 -0
- package/dist/core/c4/resolve/service.js.map +1 -1
- package/dist/core/c4/resolve/steps.js +144 -0
- package/dist/core/c4/resolve/steps.js.map +1 -0
- package/dist/core/c4/splice/delta-blocks.js +15 -2
- package/dist/core/c4/splice/delta-blocks.js.map +1 -1
- package/dist/core/c4/workspace.js +2 -0
- package/dist/core/c4/workspace.js.map +1 -1
- package/dist/core/coherence/coherence.js +36 -0
- package/dist/core/coherence/coherence.js.map +1 -1
- package/dist/core/deployment/delta.js +188 -0
- package/dist/core/deployment/delta.js.map +1 -0
- package/dist/core/deployment/objects.js +145 -0
- package/dist/core/deployment/objects.js.map +1 -0
- package/dist/core/pack/living.js +19 -0
- package/dist/core/pack/living.js.map +1 -1
- package/dist/core/repo/authored/paths.js +18 -0
- package/dist/core/repo/authored/paths.js.map +1 -1
- package/dist/core/repo/paths.js +43 -2
- package/dist/core/repo/paths.js.map +1 -1
- package/dist/core/status/feature/artifacts.js +29 -4
- package/dist/core/status/feature/artifacts.js.map +1 -1
- package/dist/core/status/fleet/fleet.js +11 -1
- package/dist/core/status/fleet/fleet.js.map +1 -1
- package/dist/core/usecases/delta/overlay.js +28 -191
- package/dist/core/usecases/delta/overlay.js.map +1 -1
- package/dist/core/usecases/fleet.js.map +1 -1
- package/dist/core/usecases/operations.js +2 -2
- package/dist/core/usecases/operations.js.map +1 -1
- package/dist/core/vocabulary/issue.js +3 -0
- package/dist/core/vocabulary/issue.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,93 @@ case for a change — the alternative that was rejected, the defect it came from
|
|
|
10
10
|
generalises — lives where it is maintained: [SCHEMA.md](SCHEMA.md) for the rule,
|
|
11
11
|
[ROADMAP.md](ROADMAP.md) for the priority and its exit criteria, and the commit that landed it._
|
|
12
12
|
|
|
13
|
+
## [0.2.0-alpha.2] - 2026-09-02
|
|
14
|
+
|
|
15
|
+
The deployment axis, and the two defects the axis surfaced on its way in. Still a prerelease on the
|
|
16
|
+
`alpha` line: `npm i @ybotok/loam@alpha` installs it. Nothing below is breaking, but two entries are
|
|
17
|
+
changes a repo can NOTICE — a fleet that had already placed a `#obl-` tag on a `deploymentNode`
|
|
18
|
+
starts seeing findings it was never shown, and `loam status` stops asking an architecture-only
|
|
19
|
+
feature for a business spec.
|
|
20
|
+
|
|
21
|
+
### Added — the deployment axis: topology joined to requirements
|
|
22
|
+
|
|
23
|
+
A LikeC4 `deployment { }` block has always been legal in a docs repo and was completely unread. The
|
|
24
|
+
parser resolved every `instanceOf`, and after that no requirement could name a node, no `#obl-` tag
|
|
25
|
+
on a datacenter was graded, and the context pack an agent implements from did not mention topology
|
|
26
|
+
at all. Five things change, and a fleet that draws no topology is unaffected by every one of them —
|
|
27
|
+
the axis opts in on the block's existence, the way the obligation vocabulary opts in on its file.
|
|
28
|
+
|
|
29
|
+
- **`Covers:` gains a fourth entry form.** `node:<id>` names a deployment node or an instance, and
|
|
30
|
+
`node:a -> node:b` names an edge between them; both match literally. This is the form that lets an
|
|
31
|
+
architecture requirement about replication name the two clusters it is written about, and a
|
|
32
|
+
topology change that renames one now reports `covers.unknown` with the real ids offered — spelled
|
|
33
|
+
with the prefix, because that is the line the author has to type. Both sides of an edge carry the
|
|
34
|
+
prefix or neither; a bare id stays an element entry.
|
|
35
|
+
- **An obligation on a deployment object is graded.** `obligation.unknown`, `obligation.unapplied`
|
|
36
|
+
and `obligation.uncovered` now walk the topology as well as the logical model. This is a
|
|
37
|
+
behaviour change a repo can notice: a `#obl-` tag on a `deploymentNode` used to be invisible to
|
|
38
|
+
every check, so a fleet that had placed one starts seeing findings it was never shown. A
|
|
39
|
+
datacenter is owned by no service, so `obligation.uncovered` drops `subject` there and names the
|
|
40
|
+
requirement's home in general terms rather than picking a team.
|
|
41
|
+
- **A feature can bring topology.** `features/<FEAT>/deployment/<name>.likec4` is a create-only
|
|
42
|
+
document `loam archive` copies into `architecture/` and `loam unarchive` takes back — the third
|
|
43
|
+
whole-file slot, beside `usecases/` and `glossary/`. Say `extend` in it to add to a region the
|
|
44
|
+
living map already declares; two features may extend one region from documents of their own and
|
|
45
|
+
both archive. Two new codes, both errors and neither overridable: **`deployment.doc-exists`** when
|
|
46
|
+
the feature's document names a file the living tree already holds, and **`deployment.doc-invalid`**
|
|
47
|
+
when it cannot be read against the map the feature's own merge would leave behind. The second is
|
|
48
|
+
what makes the ordinary case legal — a document instancing a service only this feature's
|
|
49
|
+
`delta.likec4` adds resolves, because the corpus inside a feature window is feature ∪ living — and
|
|
50
|
+
refuses the case where the merge does not land what the document names. `delta.likec4` still
|
|
51
|
+
refuses a `deployment { }` block and now names this slot instead of the living landscape.
|
|
52
|
+
A third, **`deployment.doc-reserved`**, refuses a name loam owns inside `architecture/` —
|
|
53
|
+
`subsystems.likec4`, `landscape.likec4`, or a path under `usecases/` — whether or not the target
|
|
54
|
+
exists, because two of the three fail silently when it does not: nothing parses the generated
|
|
55
|
+
subsystem views and `loam subsystem sync` overwrites them wholesale.
|
|
56
|
+
- **`loam context` carries the topology.** `--json` gains `living.deployment` — the service's
|
|
57
|
+
instances, and the deployment edges touching them from either end.
|
|
58
|
+
- **One more statement in the adoption brief's unchecked list**, now sixteen: nothing here knows
|
|
59
|
+
whether the second cluster exists, is reachable, or holds the data a requirement claims, and no
|
|
60
|
+
check counts datacenters — how many a service needs is a decision the fleet writes down as a
|
|
61
|
+
requirement, never one loam evaluates.
|
|
62
|
+
|
|
63
|
+
### Fixed — the never-overridable archive refusal names what actually refused
|
|
64
|
+
|
|
65
|
+
It said `N element binding(s) name an illegal service id` for every code in that register, and had
|
|
66
|
+
done since the use-case slot put a second one there — about features that bind nothing. The headline
|
|
67
|
+
now counts the refusals and the closing line names their codes; each issue keeps its own sentence.
|
|
68
|
+
The `--json` envelope is unchanged: consumers branch on `issues[].code` and `overridable`, which is
|
|
69
|
+
what that split exists for.
|
|
70
|
+
|
|
71
|
+
### Fixed — `loam status` no longer invents work for an architecture-only feature
|
|
72
|
+
|
|
73
|
+
A feature whose only requirement delta is `specs/<svc>/arch.spec.md` reported two things that were
|
|
74
|
+
not true, and both are corrected. The `spec` row read `missing` and `next.author-spec` told the
|
|
75
|
+
author to write business requirements the change does not have — while `loam validate --feature`
|
|
76
|
+
accepted the same tree at exit 0 and `coherence`'s `service.no-requirement-delta` stayed silent,
|
|
77
|
+
because every other reader counts the service DIRECTORY and not which corpus is in it. And the
|
|
78
|
+
`verification` row read `blocked` with `blockedBy: ["delta", "spec", "openapi", "asyncapi"]`,
|
|
79
|
+
telling the reader there was nothing to derive a checklist from — while `featureChecklist` reads
|
|
80
|
+
`arch.spec.md` and `loam verify` on that feature returns a real checklist.
|
|
81
|
+
|
|
82
|
+
The fleet form carried the same defect independently, and it broke the rule stated one line below
|
|
83
|
+
it in its own source: `loam status` with no feature reported `missing <svc>/spec` and staged the
|
|
84
|
+
feature `missing`, over a feature `loam status <FEAT>` called `ready`. Both forms now accept either
|
|
85
|
+
corpus as the service's requirement delta, and `<svc>/spec` is named only when neither exists.
|
|
86
|
+
|
|
87
|
+
Three payload-visible consequences, all in `loam status --json`: `artifacts[]`'s `spec` row is
|
|
88
|
+
`required: false` when the service's `arch.spec.md` delta exists (it was unconditionally `true`),
|
|
89
|
+
`arch-spec` now counts as a verification feeder, so `blockedBy` gained `"arch-spec"` in the
|
|
90
|
+
one case that is still genuinely blocked — a feature with no requirement delta of either kind —
|
|
91
|
+
and `features[]`'s `missing` and `stage` no longer report an architecture-only feature as
|
|
92
|
+
incomplete. No finding code, flag or exit code changes. This is the change to read if a script
|
|
93
|
+
branched on `artifacts[].required` or on `features[].missing`.
|
|
94
|
+
|
|
95
|
+
The case is the one `arch.spec.md` exists for: an outbox, a retry policy, an alert — obligations no
|
|
96
|
+
business scenario was ever going to carry, so the feature that adds one has no business requirement
|
|
97
|
+
to write. Found by authoring loam's own architecture change through loam's own lifecycle, which is
|
|
98
|
+
the trigger ROADMAP's self-hosting section records for that axis.
|
|
99
|
+
|
|
13
100
|
## [0.2.0-alpha.1] - 2026-09-01
|
|
14
101
|
|
|
15
102
|
The first release of the 0.2 line, and the whole accumulated body since `0.1.0-beta.3` — 116
|
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
[](https://github.com/ybotok/loam/actions/workflows/ci.yml)
|
|
8
8
|
[](https://www.npmjs.com/package/@ybotok/loam)
|
|
9
9
|
|
|
10
|
-
> **Pre-release: `0.2.0-alpha.
|
|
10
|
+
> **Pre-release: `0.2.0-alpha.2`**, the second release of the 0.2 line and the newest published
|
|
11
11
|
> version of [`@ybotok/loam`](https://www.npmjs.com/package/@ybotok/loam). It publishes under the
|
|
12
12
|
> `alpha` npm dist-tag: `npm i @ybotok/loam@alpha`, or the exact version, always installs this
|
|
13
13
|
> line. `latest` is moved onto a release by hand once it has published, so a plain
|
|
@@ -108,7 +108,7 @@ both halves are written down: the question in the brief or the checklist, the an
|
|
|
108
108
|
|
|
109
109
|
Most of this category will tell you what it checks. loam ships the other list too, in the binary:
|
|
110
110
|
**[`src/core/brief/unchecked.ts`](https://github.com/ybotok/loam/blob/main/src/core/brief/unchecked.ts)
|
|
111
|
-
is
|
|
111
|
+
is sixteen statements of what no check will ever tell you**, printed into the adoption brief beside
|
|
112
112
|
the checks that do run. It says, in loam's own words, that nothing here knows whether the model is
|
|
113
113
|
the architecture the code actually has, whether a requirement is *true*, whether an `operationId` is
|
|
114
114
|
the one the code serves, or whether `sources` names the files anybody read. And it says the one that
|
|
@@ -358,7 +358,7 @@ cannot hold.
|
|
|
358
358
|
| `loam new <FEAT>` | `--title <text>` `--touches <id>` `--new-service <id>` `--capability <id>` | Scaffold a feature: intent, C4 delta, a requirement delta per service named by `--touches`/`--new-service`, and a capability delta per promise named by `--capability` (all repeatable) |
|
|
359
359
|
| `loam show <service\|FEAT>` | `--type <kind>` | Everything loam knows about one service or feature. A feature's JSON includes a semantic `review` pack: intent, exact architecture objects, API/event slices, dependencies, artifacts, verification, blockers and next actions. `--type` forces an ambiguous reading |
|
|
360
360
|
| `loam subsystem <verb> [names...]` | `--into <name>` `--under <name>` `--title <text>` `--description <text>` `--owner <name>` | Manage the grouping tree under `services/` — a navigable tree no identity depends on. `new`/`rm`, `move … --into`, `rename`, `list`, `history`, `sync` |
|
|
361
|
-
| `loam adopt` | `--service <id>` `--subsystem <name>` `--targets` | Brief an agent to write this service's baseline into the docs repo as `draft`: the target paths, the grammar of each, what the landscape already says, and the checks that follow. Writes nothing. `--targets` narrows it to what varies by service — the walk, the 37 checks and the
|
|
361
|
+
| `loam adopt` | `--service <id>` `--subsystem <name>` `--targets` | Brief an agent to write this service's baseline into the docs repo as `draft`: the target paths, the grammar of each, what the landscape already says, and the checks that follow. Writes nothing. `--targets` narrows it to what varies by service — the walk, the 37 checks and the sixteen statements of what nothing checks are the same bytes everywhere, so a `full` field names the one run that carries them: 42,873 bytes to 17,805 |
|
|
362
362
|
| `loam delta <FEAT>` | `--service <id>` | Project a feature onto one service: the intent, its requirement deltas with every body and Given/When/Then line verbatim, the endpoints it adds or retires, and the edges around it. Doubles as a coding-agent task |
|
|
363
363
|
| `loam gherkin [<FEAT>]` | `--service <id>` `--dry-run` | Emit spec scenarios as digest-stamped Gherkin `.feature` files into the service repo's `<gherkinDir>/loam/` |
|
|
364
364
|
| `loam rebase [<FEAT>]` | `--service <id>` `--dry-run` `--living` | Pin the feature to the living versions it was written against, on the requirement axis and the contract axis. `--living` takes no feature: it pins the living corpus's `Realizes:` entries to the capability requirements they name, which is what makes `capability.realizes-stale` able to fire later |
|
|
@@ -486,7 +486,7 @@ always preserved byte for byte, with the entry printed for manual merge.
|
|
|
486
486
|
maps, what is lost, what must be added.
|
|
487
487
|
- [ROADMAP.md](ROADMAP.md) — the evidence-backed improvement plan, priorities, non-goals and exit
|
|
488
488
|
criteria.
|
|
489
|
-
- [CHANGELOG.md](CHANGELOG.md) — released beta.1–alpha.
|
|
489
|
+
- [CHANGELOG.md](CHANGELOG.md) — released beta.1–alpha.2 plus the changes on `main` under
|
|
490
490
|
`[Unreleased]`.
|
|
491
491
|
- [docs/BENCHMARKS.md](https://github.com/ybotok/loam/blob/main/docs/BENCHMARKS.md) — what a
|
|
492
492
|
fleet-sized run costs, with the method, the machine and the committed runs behind every number.
|
|
@@ -522,12 +522,12 @@ authored-capability axes with the `Realizes:` join, use cases graded as `dynamic
|
|
|
522
522
|
`architecture/obligations.yaml` architectural obligations checked through `#obl-` tags and
|
|
523
523
|
`Covers:`. Known limits, each with its owner in [ROADMAP.md](ROADMAP.md): the complete gate still
|
|
524
524
|
needs repeatable CI and installed-package evidence observed from an actual push. Speculative
|
|
525
|
-
`render`, health composition and UI generation come later. Behind that status stand **
|
|
525
|
+
`render`, health composition and UI generation come later. Behind that status stand **157 test files** (counted 2026-09-02): the count is graded
|
|
526
526
|
against a live readdir by `test/docs-facts.test.ts`, so this sentence fails the suite the moment it
|
|
527
527
|
trails the tree.
|
|
528
528
|
|
|
529
529
|
The package is published as [`@ybotok/loam`](https://www.npmjs.com/package/@ybotok/loam), currently
|
|
530
|
-
`0.2.0-alpha.
|
|
530
|
+
`0.2.0-alpha.2`: the unscoped `loam` name is taken by an unrelated GDAL wrapper, so the package ships
|
|
531
531
|
under the maintainer's own npm user scope. Releases are tag-driven and maintainer-only
|
|
532
532
|
([CONTRIBUTING.md](CONTRIBUTING.md)); the version this line names is pinned to `package.json` by a
|
|
533
533
|
drift test, so it cannot silently trail a release.
|
package/ROADMAP.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Roadmap
|
|
2
2
|
|
|
3
|
-
_Assessed 2026-
|
|
3
|
+
_Assessed 2026-09-02._
|
|
4
4
|
|
|
5
5
|
This is loam's canonical improvement sequence. It records priorities and exit criteria, not delivery
|
|
6
6
|
dates. An item is complete only when its exit criteria are supported by repeatable evidence; moving
|
|
@@ -18,20 +18,21 @@ acceptance tests, and implementation evidence remain ordinary files in repositor
|
|
|
18
18
|
state inside a service. The CLI is small enough to audit, and every writer — not only archive — now
|
|
19
19
|
commits through a locked, journaled transaction that a crash cannot leave half-applied.
|
|
20
20
|
|
|
21
|
-
The tree contains **
|
|
21
|
+
The tree contains **424 TypeScript modules in 129 source packages**, with an acyclic package graph
|
|
22
22
|
checked by
|
|
23
23
|
[scripts/package-graph.mjs](https://github.com/ybotok/loam/blob/main/scripts/package-graph.mjs). The
|
|
24
24
|
CLI exposes **29 commands** from [src/cli.ts](https://github.com/ybotok/loam/blob/main/src/cli.ts),
|
|
25
|
-
and the suite stands at **
|
|
25
|
+
and the suite stands at **157 test files**. Those four counts are deliberately stated OUTSIDE the
|
|
26
26
|
dated snapshot below: each derives from the tree in one readdir, so
|
|
27
27
|
[test/docs-facts.test.ts](https://github.com/ybotok/loam/blob/main/test/docs-facts.test.ts) grades
|
|
28
28
|
them live and this paragraph cannot quietly trail the code the way its predecessor did.
|
|
29
29
|
|
|
30
|
-
_Measured 2026-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
30
|
+
_Measured 2026-09-02 at `0.2.0-alpha.2`, and dated because neither number has a cheap derivation:_
|
|
31
|
+
lint, typecheck, `npm run arch:check` and `npm run meta:check` green; `npm test` passing
|
|
32
|
+
**3,399/3,399 tests**, with two `skipIf(asRoot)` cases the root gate container cannot run; the
|
|
33
|
+
coverage gate passing with **92.25% statements, 84.04% branches, 96.62% functions, and 94.18%
|
|
34
|
+
lines** against its thresholds of 91 / 82 / 95 / 93; and `npm run release:check` plus
|
|
35
|
+
`npm run test:package` green against the real tarball.
|
|
35
36
|
|
|
36
37
|
One qualification remains, and it is observation rather than code: nothing has been pushed since the
|
|
37
38
|
CI `stability` job and the tarball-reading package smoke landed, so repeatable CI executions — the
|
|
@@ -98,14 +99,16 @@ The strongest foundations to preserve are:
|
|
|
98
99
|
## Now
|
|
99
100
|
|
|
100
101
|
Nothing is queued behind code. Every integrity, enforcement and lifecycle item that preceded this
|
|
101
|
-
has landed (see [Recently landed](#recently-landed)) — the authored business axis
|
|
102
|
-
|
|
103
|
-
|
|
102
|
+
has landed (see [Recently landed](#recently-landed)) — the authored business axis, the use-case
|
|
103
|
+
axis, and as of 2026-09-02 the deployment axis, which is the first item this file ever wrote
|
|
104
|
+
directly into this section rather than promoting from [Later](#later--promote-only-from-evidence).
|
|
104
105
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
106
|
+
### Release evidence, unchanged
|
|
107
|
+
|
|
108
|
+
What is left is the one thing this file cannot close by writing code, and it stays
|
|
109
|
+
where it is rather than being promoted for the shape of it: the CI `stability` job and the
|
|
110
|
+
installed-package smoke have still to be OBSERVED green from a pushed commit. That is release
|
|
111
|
+
evidence to collect, not an improvement to make.
|
|
109
112
|
|
|
110
113
|
An item returns to this section from [Later](#later--promote-only-from-evidence) when fleet evidence
|
|
111
114
|
names its operator, repeated task, failure mode, frequency, current workaround and measurable
|
|
@@ -122,6 +125,105 @@ changes, exit criteria, and what each review surfaced — are in this file's his
|
|
|
122
125
|
working tree and not committed, and the range goes in with the commit rather than being guessed at
|
|
123
126
|
here.
|
|
124
127
|
|
|
128
|
+
#### The deployment axis — topology joined to requirements
|
|
129
|
+
|
|
130
|
+
**Promoted 2026-09-02, and authored as a feature in loam's own docs repo** rather than as an item
|
|
131
|
+
text here: `meta/docs/features/archive/FEAT-1-the-deployment-axis/`, whose `intent.md` carries the
|
|
132
|
+
four measurements and whose `specs/loam/arch.spec.md` carries the five rules the change establishes,
|
|
133
|
+
each with scenarios. This entry is the decision and the gate; that feature is the specification, and
|
|
134
|
+
the two must not be allowed to become two accounts of the same change.
|
|
135
|
+
|
|
136
|
+
**Landed 2026-09-02, complete.** Every step is in, and step 4 was corrected rather than built
|
|
137
|
+
(below) — the item specified two finding codes that the implementation showed it should not mint.
|
|
138
|
+
FEAT-1 archived once every edge it claimed existed —
|
|
139
|
+
and it archived having been CORRECTED against the tree twice, which is the argument for the route
|
|
140
|
+
rather than a footnote to it: the delta predicted a `core/kernel/` dependency that turned out to be
|
|
141
|
+
type-only (a brand costs an annotation, not an import) and missed `core/coherence/` entirely.
|
|
142
|
+
`npm run meta:check` was red for the whole of the build and went green with the archive, which is
|
|
143
|
+
the axis working rather than a gap in it.
|
|
144
|
+
|
|
145
|
+
The whole change is reviewable as five commits on `feat/deployment-axis`, and the one thing to read
|
|
146
|
+
first is what the implementation taught the plan: two of the codes this item specified were not
|
|
147
|
+
minted, and the reasons are in step 4.
|
|
148
|
+
|
|
149
|
+
**What is wrong today.** A LikeC4 `deployment { }` block is legal in a docs repo and completely
|
|
150
|
+
unread. The parser resolves every `instanceOf` in it — a container renamed out from under a
|
|
151
|
+
deployment node fails the gate as `landscape.invalid` — and after that nothing: no requirement can
|
|
152
|
+
name a node, no report counts one, and the context pack an agent implements from does not mention
|
|
153
|
+
topology at all. Two of the four measurements taken on a copy of `examples/docs` are the reason this
|
|
154
|
+
is an item rather than a wish. A `#obl-` tag on a `deploymentNode` is invisible to
|
|
155
|
+
`validate/fleet/obligations.ts`, which walks the logical model only — the same undeclared tag raises
|
|
156
|
+
`obligation.unknown` on a container and nothing on a datacenter, which is fail-open and a defect on
|
|
157
|
+
its own terms. And `parsedModel().deployment` already returns nodes with their tags, instances
|
|
158
|
+
carrying the id of the logical element each one instances, and relationships carrying metadata, so
|
|
159
|
+
the read costs no second parse, no new file format and no change to the frozen CLI surface.
|
|
160
|
+
|
|
161
|
+
**Required changes**, in the order the dependency graph forces:
|
|
162
|
+
|
|
163
|
+
1. Walk the deployment model in every `#obl-` grade. Fail-open becomes fail-closed; no new code.
|
|
164
|
+
2. A fourth `Covers:` entry form, `node:<id>`, with the same prefix accepted on either side of the
|
|
165
|
+
edge form. No new code.
|
|
166
|
+
3. The parse adapter in `src/core/c4/parsed/`, and the deployment records added to
|
|
167
|
+
`test/likec4-model-parity.test.ts` — a two-stage substitution nothing measures is an assumption.
|
|
168
|
+
`src/core/c4/` holds exactly five files, so this is a sub-package and not a sixth module.
|
|
169
|
+
4. **No new finding code, and this is a correction the implementation earned.** The item planned
|
|
170
|
+
two. The first, for a tagged node or edge no living arch requirement covers, turned out to be
|
|
171
|
+
`obligation.uncovered` asked of a second model: once step 1 walks the topology, the existing code
|
|
172
|
+
answers it, and a caller acts identically either way — write a `Covers:` line. A second code for
|
|
173
|
+
one question is exactly what the add-a-code checklist refuses. The second, for a container no
|
|
174
|
+
deployment instances, is declined outright: it asserts that everything modelled ought to be
|
|
175
|
+
deployed somewhere, which is a COMPLETENESS claim, and completeness is the one thing
|
|
176
|
+
`src/core/brief/unchecked.ts` says loam never checks. It is the same category error as the
|
|
177
|
+
at-least-two-datacenters rule below, one axis over.
|
|
178
|
+
5. `features/<FEAT>/deployment/<name>.likec4`, create-only and copied whole — `core/usecases/delta/`
|
|
179
|
+
one axis over. It works because `extend` was measured to resolve across documents of one project,
|
|
180
|
+
so no text splice is needed; `delta-blocks.ts` goes on refusing a `deployment { }` block inside
|
|
181
|
+
`delta.likec4` and its message names the new slot. The collision is on the FILE, never on what is
|
|
182
|
+
inside it: two features may both extend one living region from documents of their own and both
|
|
183
|
+
archive, which is the shape the fleet-wide change actually takes.
|
|
184
|
+
|
|
185
|
+
**Graded against the merge preview**, which shipped one commit behind the slot and is the
|
|
186
|
+
second code, `deployment.doc-invalid` (error, never overridable). This axis needs the post-merge
|
|
187
|
+
corpus more literally than the use-case one: a feature that stands a NEW service up in a cluster
|
|
188
|
+
declares it in `delta.likec4` and instances it in the topology, so the element the document names
|
|
189
|
+
exists only after the merge. The staging that makes that readable moved OUT of
|
|
190
|
+
`core/usecases/delta/overlay.ts` into `core/c4/project/staged.ts` rather than being copied — the
|
|
191
|
+
two axes would have disagreed first about the merge preview, which is the one thing both
|
|
192
|
+
refusals rest on. The extraction dropped `core/usecases/` → `core/kernel/`, which is a one-line
|
|
193
|
+
edge and was edited into the model directly.
|
|
194
|
+
6. The surfaces: the topology in the context pack (`context --json`'s `living.deployment`), the code
|
|
195
|
+
table `test/codes-drift.test.ts` requires, and one more statement in
|
|
196
|
+
`src/core/brief/unchecked.ts`. `loam explain` needed nothing — it parses the /loam-check fix
|
|
197
|
+
tables at runtime, so the rows added there ARE the entries. The scorecard row the item listed was
|
|
198
|
+
dropped: the fleet scorecard aggregates at the bound `service`, and a topology row there would
|
|
199
|
+
count deployment nodes against a denominator no service owns.
|
|
200
|
+
|
|
201
|
+
**Exit criteria.** Every step above green under the full gate; `covers.unknown` demonstrated firing
|
|
202
|
+
on a covered node that a topology change renamed, which is the acceptance criterion the trigger
|
|
203
|
+
names; the parity suite extended and shown to fail against an un-extended adapter; and
|
|
204
|
+
`test/self-model.test.ts` re-pinned once FEAT-1 archives.
|
|
205
|
+
|
|
206
|
+
**Out of scope, and each refusal is load-bearing.** No reading of Terraform, Helm or cluster state —
|
|
207
|
+
that is the extractor the non-goals forbid. No check that a service is deployed in at least two
|
|
208
|
+
datacenters: it would need `criticality` promoted from prose to a read field, and it would turn
|
|
209
|
+
`architecture/obligations.yaml` into the policy engine that file says in its own words it is not. A
|
|
210
|
+
multi-datacenter rule is an authored requirement whose `Covers:` line names the nodes; loam grades
|
|
211
|
+
the join and never the architecture. And no `environment` concept above LikeC4, because
|
|
212
|
+
`deploymentNode` is that mechanism and the fleet names its own kinds.
|
|
213
|
+
|
|
214
|
+
**The evidence, and its one gap stated plainly.** Operator: a team standing up a standby cluster in
|
|
215
|
+
a second datacenter. Repeated task: keeping RTO/RPO requirements attached to a topology that keeps
|
|
216
|
+
changing. Failure mode: a requirement about replication staying green after the node it described
|
|
217
|
+
was renamed or removed. Frequency: every topology change, which is the cadence a migration runs at.
|
|
218
|
+
Current workaround: a `Covers:` line that resolves to nothing, or no written requirement at all.
|
|
219
|
+
Acceptance criterion: a topology change that orphans a covered requirement is convicted without a
|
|
220
|
+
human noticing it first. **That is one operator, not two.** The maintainer judged the trigger
|
|
221
|
+
sufficient on the same basis recorded for landscape scaling and the authored business axis, and the
|
|
222
|
+
[Later](#later--promote-only-from-evidence) exit criteria are amended below to say so rather than
|
|
223
|
+
leaving the two paragraphs to contradict each other. Steps 1 and 2 do not rest on that judgement at
|
|
224
|
+
all: the first is a fail-open defect and the second is a grammar entry, and neither is a new axis.
|
|
225
|
+
|
|
226
|
+
|
|
125
227
|
- **A feature can bring a use case.** `features/<FEAT>/usecases/<name>.likec4` — a views-only
|
|
126
228
|
document `loam archive` copies into `architecture/usecases/` and `loam unarchive` takes back,
|
|
127
229
|
create-only, refused by `usecase.flow-exists` when the living tree already holds that file. All
|
|
@@ -264,8 +366,11 @@ item gets promoted — it went from here to `## Now` and from there to
|
|
|
264
366
|
|
|
265
367
|
Exit criteria for promoting a Later item:
|
|
266
368
|
|
|
267
|
-
- Evidence comes from at least two independent operators or fleets, except landscape scaling
|
|
268
|
-
authored business axis, whose recorded
|
|
369
|
+
- Evidence comes from at least two independent operators or fleets, except landscape scaling, the
|
|
370
|
+
authored business axis and — added 2026-09-02 — the deployment axis in [Now](#now), whose recorded
|
|
371
|
+
triggers are sufficient. Three exceptions is where this criterion starts describing the rule
|
|
372
|
+
rather than the exception: the next single-operator promotion has to say why the criterion should
|
|
373
|
+
survive, or retire it.
|
|
269
374
|
- An ADR compares the existing workflow, external tooling, and the smallest loam-owned change.
|
|
270
375
|
- A prototype is measured against a predeclared acceptance criterion and preserves every non-goal
|
|
271
376
|
below.
|
|
@@ -276,7 +381,7 @@ Exit criteria for promoting a Later item:
|
|
|
276
381
|
|
|
277
382
|
Placed in a section of its own, and the placement is the argument. This is not a `## Now` item, and
|
|
278
383
|
nothing here may be mistaken for one. It is not a "Later" candidate either: promotion
|
|
279
|
-
from that section requires evidence from at least two independent operators or fleets (bar the
|
|
384
|
+
from that section requires evidence from at least two independent operators or fleets (bar the three
|
|
280
385
|
whose recorded triggers were judged sufficient when they were written), and loam's own repository is
|
|
281
386
|
one operator by construction, so a Later entry could only ever be a permanent exception to the rule
|
|
282
387
|
it sits under. Nor is its value speculative — the trigger below is a defect this repository has
|
|
@@ -142,19 +142,23 @@ export async function gate(config, featureId, opts, say) {
|
|
|
142
142
|
// the rest. The register is issue.ts's `approveOverrides` — the same data
|
|
143
143
|
// the `overridable` key in the envelope is resolved from — never a code
|
|
144
144
|
// string spelled here, because two spellings of "what the flag moves" would
|
|
145
|
-
// drift.
|
|
146
|
-
// the id grammar refuses,
|
|
147
|
-
//
|
|
148
|
-
//
|
|
149
|
-
//
|
|
150
|
-
// mechanical fact about the
|
|
151
|
-
// feature
|
|
152
|
-
//
|
|
153
|
-
//
|
|
154
|
-
//
|
|
145
|
+
// drift. What the register HOLDS has grown: it began as one code (a
|
|
146
|
+
// `metadata { service }` binding the id grammar refuses, where the merge
|
|
147
|
+
// would splice the name into the living map verbatim and probe
|
|
148
|
+
// `services/<binding>/` with it) and now holds the create-only collisions of
|
|
149
|
+
// every whole-file axis besides. They share the doctrine and nothing else —
|
|
150
|
+
// each is a mechanical fact about what the merge would write, never a
|
|
151
|
+
// judgment about the feature — which is why the message below counts them
|
|
152
|
+
// and lets each issue say its own sentence.
|
|
153
|
+
//
|
|
154
|
+
// IT USED TO NAME BINDINGS, and had been wrong for every other code in the
|
|
155
|
+
// register since the use-case slot landed: a feature whose flow collided
|
|
156
|
+
// with a living one was refused with "1 element binding(s) name an illegal
|
|
157
|
+
// service id", about a feature that binds nothing. A refusal headline that
|
|
158
|
+
// describes the wrong breach sends its reader to the wrong file first.
|
|
155
159
|
const nonOverridable = issues.filter((i) => !approveOverrides(i));
|
|
156
160
|
if (nonOverridable.length > 0) {
|
|
157
|
-
const msg = `archive ${id} — BLOCKED: ${nonOverridable.length}
|
|
161
|
+
const msg = `archive ${id} — BLOCKED: ${nonOverridable.length} issue(s) --approve does not override; each names what the merge would write`;
|
|
158
162
|
if (json) {
|
|
159
163
|
refuseJson("not-coherent", msg, issues, `features/${dirName}`);
|
|
160
164
|
return null;
|
|
@@ -166,7 +170,9 @@ export async function gate(config, featureId, opts, say) {
|
|
|
166
170
|
// line says which of the marks the flag cannot move.
|
|
167
171
|
for (const i of issues)
|
|
168
172
|
console.error(` ${SEVERITY_MARK[i.severity]} ${i.message}`);
|
|
169
|
-
console.error(`\nOf these, --approve does not override
|
|
173
|
+
console.error(`\nOf these, --approve does not override ${nonOverridable.length}: ${[...new Set(nonOverridable.map((i) => i.code))].sort().join(", ")}. ` +
|
|
174
|
+
"Each is a mechanical fact about what the merge would write — a name loam cannot resolve, or a file it would replace wholesale — not a judgement the flag can move. " +
|
|
175
|
+
"Fix those; the other issues gate as usual.");
|
|
170
176
|
sayExplain("not-coherent");
|
|
171
177
|
process.exitCode = 1;
|
|
172
178
|
return null;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"gate.js","sourceRoot":"","sources":["../../../../src/commands/archive/plan/gate.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Every refusal that happens before a byte is planned.\n *\n * They are gathered here because they share one doctrine that is easy to lose:\n * `--approve` overrides judgments about the FEATURE, and nothing else. A service\n * directory whose name the id grammar refuses, a living document holding git\n * conflict markers, a requirement outside `## Requirements` — those are\n * mechanical facts about paths and files, so `--approve` does not reach them,\n * and the dry run is gated too, since a plan built on one describes a merge loam\n * must never perform.\n *\n * `null` means the refusal has already been printed and the exit code set.\n */\nimport { existsSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { fail, repoPath, sayExplain } from \"../../../core/envelope/json.js\";\nimport { approveOverrides, gatesArchive, type Issue } from \"../../../core/vocabulary/issue.js\";\nimport { recoverInterruptedCommit } from \"../../../core/staging/recovery/recover.js\";\nimport { readUtf8 } from \"../../../core/staging/writes.js\";\nimport { loadFile } from \"../../../core/c4/likec4.js\";\nimport { archiveDir as archiveRoot, featurePaths, featureSpecPaths, SPEC_AXES } from \"../../../core/repo/paths.js\";\nimport { livingCapabilityPaths } from \"../../../core/repo/authored/paths.js\";\nimport { featureCapabilityDeltas } from \"../../../core/capabilities/delta/tree.js\";\nimport { locateServicePaths } from \"../../../core/repo/service-target.js\";\nimport { featureSpecServices, missingFeatureMessage, resolveFeature } from \"../../../core/repo/repo.js\";\nimport { featureCoherence } from \"../../../core/coherence/coherence.js\";\nimport {\n invalidSpecServiceFindings,\n livingMergeConflicts,\n unknownDeltaServices,\n} from \"../../../core/coherence/living.js\";\nimport { SEVERITY_MARK } from \"../../../core/vocabulary/report.js\";\nimport { isRequirementsHeading, parseRequirements } from \"../../../core/document/parse.js\";\nimport { refuseFindings, refuseJson, sayRecovery, type ArchiveOptions } from \"./refusal.js\";\nimport { type Gated } from \"./state.js\";\nimport type { DocsDir } from \"../../../core/kernel/ids/dirs.js\";\nimport type { FleetContext } from \"../../../core/fleet-context.js\";\n\nexport async function gate(\n config: { docsDir: DocsDir; fleet?: FleetContext },\n featureId: string,\n opts: ArchiveOptions,\n say: (line?: string) => void,\n): Promise<Gated | null> {\n const json = opts.json === true;\n const dryRun = opts.dryRun === true;\n // the living docs half-written, and every other command in loam would have\n // planned against them as if they were the truth. Under the same lock, so the\n // repair cannot race a writer. It refuses when the half-merged files have been\n // edited since — that is a human's call, not a merge's.\n const recovered = await recoverInterruptedCommit(config.docsDir);\n if (recovered !== null && !json) sayRecovery(recovered);\n\n const feature = await resolveFeature(config.docsDir, featureId, \"exclude\");\n if (!feature) {\n fail(json, \"unknown-target\", await missingFeatureMessage(config.docsDir, featureId));\n return null;\n }\n // The raw argument's last appearance: from here on the feature answers only to\n // its canonical id — tags, coherence, and the self-exclusion scans all match\n // on `id`, and `archive FEAT-5-slug` must plan exactly like `archive FEAT-5`.\n const { id, dirName, dir: featureDir } = feature;\n\n // Parse delta.likec4 ONCE for the whole run: the coherence gate and the\n // architecture merge below read the same file, and loading it spins up a\n // fresh Langium workspace each time. Nothing writes it in between.\n const deltaLikec4 = featurePaths(featureDir).delta;\n const deltaDoc = existsSync(deltaLikec4) ? await loadFile(deltaLikec4) : undefined;\n\n const deltaServices = await featureSpecServices(featureDir);\n // The business corpus's delta tree, walked once HERE and carried on `Gated`:\n // the conflict-marker scan, the strayed-requirement scan and the merge itself\n // all ask which capability documents this feature rewrites, and three walks\n // are three chances to answer differently. `featureCoherence` below walks it\n // again on its own — it is called without `config.fleet`, the same\n // no-context path `core/coherence/declared.ts` already documents paying for\n // the arch delta — so this is not the run's only walk, only the one every\n // phase downstream of the gate shares.\n const capabilityDeltas = (\n config.fleet === undefined\n ? await featureCapabilityDeltas(featureDir)\n : await config.fleet.featureCapabilityDeltas(featureDir)\n ).docs;\n\n // A `specs/<svc>/` whose NAME is not a legal service id, refused before\n // anything joins that name into a path: the conflict scan below probes\n // `services/<svc>/<axis>` with it, and the requirements merge would\n // materialise `services/<svc>/` from it — a directory `service.id-invalid`\n // then calls an error on the next `validate --all`, and one no loam command\n // can address or re-create. The set comes from coherence/living.ts's\n // `invalidSpecServiceFindings` — the same function validate reads — so the\n // two gates cannot drift.\n //\n // --approve does not apply, exactly as with the conflict-marker refusal\n // below: --approve overrides judgments about the FEATURE, and a name the id\n // grammar refuses is a mechanical fact about the path it would become. The\n // dry run is gated too: a plan built on that name describes a merge loam\n // must never perform.\n const illegalServices = await invalidSpecServiceFindings(featureDir);\n if (illegalServices.length > 0) {\n const msg = `archive ${id} — BLOCKED: ${illegalServices.length} per-service delta(s) named by an illegal service id; --approve does not override this`;\n if (json) {\n refuseFindings(\"not-coherent\", msg, illegalServices, `features/${dirName}`);\n return null;\n }\n console.error(`${msg}:`);\n for (const f of illegalServices) console.error(` ✗ ${f.message}`);\n console.error(`\\n--approve does not override this — the directory the merge would create is one loam can never address, mechanically.`);\n // Every arm in this file refuses by hand — it prints a headline, a list and\n // a closing sentence rather than the one message `fail()` takes — so the\n // code and its lookup have to be asked for explicitly. `sayExplain` is\n // called rather than the format being copied six times, and the code passed\n // is the SAME one the `--json` branch a few lines up emits: text and\n // envelope must name one refusal, or a reader who switches modes to find\n // the code is handed a different answer than the one they saw.\n sayExplain(\"not-coherent\");\n process.exitCode = 1;\n return null;\n }\n\n // Git conflict markers in a LIVING document this merge would rewrite, before\n // anything else is asked of it. Both sides of somebody's merge are in the\n // file, so nothing it says is anyone's text — and the requirements rewrite\n // deletes whichever marker lines fall inside the section it owns, turning a\n // conflict anyone can see into a file nobody can tell is wrong. Checked ahead\n // of the coherence gate for `inspectLandscape`'s reason: a conflicted\n // document parses, so every verdict downstream of it is an opinion about half\n // of two people's work.\n //\n // --approve does not apply, exactly as with the strayed-requirement refusal\n // below: --approve overrides judgments about the FEATURE, and this is a fact\n // about the living document.\n const conflicted = await livingMergeConflicts(config.docsDir, deltaServices, capabilityDeltas.map((d) => d.id));\n if (conflicted.length > 0) {\n const msg = `archive ${id} — BLOCKED: ${conflicted.length} living document(s) still hold git conflict markers`;\n if (json) {\n refuseFindings(\"merge-failed\", msg, conflicted, `features/${dirName}`);\n return null;\n }\n console.error(`${msg}:`);\n for (const f of conflicted) console.error(` ✗ ${f.message}`);\n console.error(`\\n--approve does not override this — the loss is mechanical, not a judgment call.`);\n sayExplain(\"merge-failed\");\n process.exitCode = 1;\n return null;\n }\n\n // Gate: GATING issues block the archive. Severity and gating are two\n // different questions (issue.ts): errors gate because the merge would write\n // something wrong, and the rare warning marked `gates` blocks too — the\n // document is legal but the merge would drop authored content. Advisory\n // warnings are printed (and carried into --json) but never block, matching\n // validate's own doctrine (report.ts: \"warnings never gate\"). --approve\n // overrides the gating issues ONLY, and must say exactly which ones it is\n // walking past. A dry run is gated too: a plan for a merge that would be\n // refused describes nothing that will happen.\n const issues = await featureCoherence({ docsDir: config.docsDir, featureDir, featureId: id, preloadedDelta: deltaDoc });\n // The issues --approve cannot move, carved out before the gate below reads\n // the rest. The register is issue.ts's `approveOverrides` — the same data\n // the `overridable` key in the envelope is resolved from — never a code\n // string spelled here, because two spellings of \"what the flag moves\" would\n // drift. Today the register holds one code: a `metadata { service }` binding\n // the id grammar refuses, anywhere in a tagged element's spliced block. The\n // landscape merge would splice that name into the living map verbatim, and\n // the new-service scan would probe `services/<binding>/` with it — a '../'\n // collapses the probe out of services/ altogether — so the refusal is a\n // mechanical fact about the path the name becomes, not a judgment about the\n // feature. Same doctrine as the illegal-specs-name refusal above; the dry\n // run is gated the same way, and the --json envelope stays not-coherent\n // with every issue attached, exactly like the overridable branch below, so\n // a consumer branches on the code and not on which refusal path fired.\n const nonOverridable = issues.filter((i) => !approveOverrides(i));\n if (nonOverridable.length > 0) {\n const msg = `archive ${id} — BLOCKED: ${nonOverridable.length} element binding(s) name an illegal service id; --approve does not override this`;\n if (json) {\n refuseJson(\"not-coherent\", msg, issues, `features/${dirName}`);\n return null;\n }\n console.error(`${msg}:`);\n // The whole list, not just the carve-out: the reader is about to fix the\n // feature, and the issues --approve COULD have moved are work they still\n // owe — hiding them here made the second refusal a surprise. The closing\n // line says which of the marks the flag cannot move.\n for (const i of issues) console.error(` ${SEVERITY_MARK[i.severity]} ${i.message}`);\n console.error(\n `\\nOf these, --approve does not override the ${nonOverridable.length} illegal binding(s) — the merge would write a name loam can never resolve into services/, mechanically. Fix the binding(s); the other issues gate as usual.`,\n );\n sayExplain(\"not-coherent\");\n process.exitCode = 1;\n return null;\n }\n const gating = issues.filter(gatesArchive);\n const advisory = issues.filter((i) => !gatesArchive(i));\n if (gating.length > 0 && !opts.approve) {\n const msg = `archive ${id} — BLOCKED: not coherent (${gating.length} gating issue(s), ${advisory.length} advisory warning(s))`;\n if (json) {\n refuseJson(\"not-coherent\", msg, issues, `features/${dirName}`);\n return null;\n }\n console.error(`${msg}:`);\n for (const i of issues) console.error(` ${SEVERITY_MARK[i.severity]} ${i.message}`);\n console.error(`\\nFix the gating issues (advisory warnings never block), or re-run with --approve to override them (may corrupt the living docs).`);\n sayExplain(\"not-coherent\");\n process.exitCode = 1;\n return null;\n }\n if (gating.length > 0) {\n say(`⚠ archiving despite ${gating.length} gating issue(s) (--approve):`);\n for (const i of gating) say(` ${SEVERITY_MARK[i.severity]} ${i.message}`);\n say();\n }\n if (advisory.length > 0) {\n say(`⚠ ${advisory.length} warning(s) (non-blocking):`);\n for (const i of advisory) say(` ⚠ ${i.message}`);\n say();\n }\n\n // A `specs/<svc>/` addressed to a service that exists nowhere. `validate`\n // graded this and archive did not, which is the half that costs something:\n // `specs/<svc>/` is what the merge MATERIALISES `services/<svc>/` from, so a\n // one-character slip in `--touches` archived at exit 0 and left the fleet a\n // living service nobody meant to adopt. The set comes from coherence/living.ts's\n // `unknownDeltaServices` — the same function validate reads — so the two\n // gates cannot drift into disagreeing about which ids are real.\n //\n // Gated exactly like a coherence error, --approve included: which service you\n // meant is a judgment about the FEATURE. A feature that genuinely introduces\n // one has a better escape hatch than --approve anyway — tag its element in\n // delta.likec4, which is also what makes the fleet map true afterwards.\n const unknownServices = await unknownDeltaServices(config.docsDir, featureDir, id, { preloadedDelta: deltaDoc });\n if (unknownServices.length > 0 && !opts.approve) {\n const msg = `archive ${id} — BLOCKED: ${unknownServices.length} per-service delta(s) address a service that does not exist`;\n if (json) {\n refuseFindings(\"not-coherent\", msg, unknownServices, `features/${dirName}`);\n return null;\n }\n console.error(`${msg}:`);\n for (const f of unknownServices) console.error(` ✗ ${f.message}`);\n console.error(`\\nFix the id, or introduce the service in this feature's delta.likec4 — or re-run with --approve to create it anyway.`);\n sayExplain(\"not-coherent\");\n process.exitCode = 1;\n return null;\n }\n if (unknownServices.length > 0) {\n say(`⚠ archiving despite ${unknownServices.length} unknown service(s) (--approve) — this merge creates them:`);\n for (const f of unknownServices) say(` ✗ ${f.message}`);\n say();\n }\n\n // A LIVING requirement outside `## Requirements` is a merge loam cannot do\n // correctly: the rewrite replaces only the requirements RUN inside that\n // section (rewriteRequirementsRun) while parseRequirements collects from\n // every section — so the strayed requirement keeps its authored copy in the\n // preserved prose AND lands again in the rewritten run, and the next\n // archive's MODIFIED replaces only one of them. Refused rather than\n // repaired: excising blocks from prose programmatically would be archive\n // editing text it does not understand. --approve does not apply — it\n // overrides judgments about the FEATURE, and this is neither.\n const strayed: Issue[] = [];\n const scanStrayed = async (livingPath: string, subject: string): Promise<void> => {\n if (!existsSync(livingPath)) return;\n for (const r of parseRequirements(await readUtf8(livingPath))) {\n // The ONE definition of the heading (spec.ts): the guard and the rewrite\n // boundary match the same way, so they cannot disagree about \"outside\".\n if (r.section !== undefined && isRequirementsHeading(r.section)) continue;\n const where = r.section === undefined ? \"above every heading\" : `under '${r.section}'`;\n strayed.push({\n severity: \"error\",\n code: \"living.requirement-outside-requirements\",\n subject,\n message: `${subject}: living requirement '${r.name}' sits ${where} in ${repoPath(config.docsDir, livingPath)} — the merge rewrites only '## Requirements', so it would land in the file twice. Re-home it under '## Requirements' first, then re-run.`,\n });\n }\n };\n for (const svc of deltaServices) {\n for (const axis of SPEC_AXES) {\n if (!existsSync(featureSpecPaths(featureDir, svc)[axis.key])) continue;\n // The shared context, because this sits in a deltaServices × SPEC_AXES\n // loop: without it every iteration re-walks and re-reads the fleet.\n await scanStrayed((await locateServicePaths(config.docsDir, svc, config.fleet))[axis.key], svc);\n }\n }\n // The business corpus, for the identical mechanical reason: a capability\n // document is narrative ABOVE `## Requirements`, `rewriteRequirementsRun`\n // rewrites only the run inside that heading, and a requirement filed under\n // `## Notes` would keep its authored copy in the preserved prose AND land\n // again in the rewritten run.\n for (const doc of capabilityDeltas) {\n await scanStrayed(livingCapabilityPaths(config.docsDir, doc.id).spec, doc.id);\n }\n if (strayed.length > 0) {\n const msg = `archive ${id} — BLOCKED: ${strayed.length} living requirement(s) outside '## Requirements'`;\n if (json) {\n refuseJson(\"living-outside-requirements\", msg, strayed, `features/${dirName}`);\n return null;\n }\n console.error(`${msg}:`);\n for (const i of strayed) console.error(` ✗ ${i.message}`);\n console.error(`\\n--approve does not override this — the duplication is mechanical, not a judgment call.`);\n sayExplain(\"living-outside-requirements\");\n process.exitCode = 1;\n return null;\n }\n\n // Pre-flight: the archive destination must be free, or the final move would fail\n // after the living docs were already rewritten.\n const archiveDir = archiveRoot(config.docsDir);\n const archiveDest = join(archiveDir, dirName);\n if (existsSync(archiveDest)) {\n fail(json, \"archive-exists\", `archive ${id} — BLOCKED: features/archive/${dirName} already exists. Remove or rename it, then re-run.`);\n return null;\n }\n\n say(`archive ${id}${dryRun ? \" (dry run)\" : \"\"}\\n`);\n\n return {\n id,\n dirName,\n featureDir,\n deltaDoc,\n deltaServices,\n capabilityDeltas,\n gating,\n advisory,\n archiveDir,\n archiveDest,\n recovered,\n issues,\n };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"gate.js","sourceRoot":"","sources":["../../../../src/commands/archive/plan/gate.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Every refusal that happens before a byte is planned.\n *\n * They are gathered here because they share one doctrine that is easy to lose:\n * `--approve` overrides judgments about the FEATURE, and nothing else. A service\n * directory whose name the id grammar refuses, a living document holding git\n * conflict markers, a requirement outside `## Requirements` — those are\n * mechanical facts about paths and files, so `--approve` does not reach them,\n * and the dry run is gated too, since a plan built on one describes a merge loam\n * must never perform.\n *\n * `null` means the refusal has already been printed and the exit code set.\n */\nimport { existsSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { fail, repoPath, sayExplain } from \"../../../core/envelope/json.js\";\nimport { approveOverrides, gatesArchive, type Issue } from \"../../../core/vocabulary/issue.js\";\nimport { recoverInterruptedCommit } from \"../../../core/staging/recovery/recover.js\";\nimport { readUtf8 } from \"../../../core/staging/writes.js\";\nimport { loadFile } from \"../../../core/c4/likec4.js\";\nimport { archiveDir as archiveRoot, featurePaths, featureSpecPaths, SPEC_AXES } from \"../../../core/repo/paths.js\";\nimport { livingCapabilityPaths } from \"../../../core/repo/authored/paths.js\";\nimport { featureCapabilityDeltas } from \"../../../core/capabilities/delta/tree.js\";\nimport { locateServicePaths } from \"../../../core/repo/service-target.js\";\nimport { featureSpecServices, missingFeatureMessage, resolveFeature } from \"../../../core/repo/repo.js\";\nimport { featureCoherence } from \"../../../core/coherence/coherence.js\";\nimport {\n invalidSpecServiceFindings,\n livingMergeConflicts,\n unknownDeltaServices,\n} from \"../../../core/coherence/living.js\";\nimport { SEVERITY_MARK } from \"../../../core/vocabulary/report.js\";\nimport { isRequirementsHeading, parseRequirements } from \"../../../core/document/parse.js\";\nimport { refuseFindings, refuseJson, sayRecovery, type ArchiveOptions } from \"./refusal.js\";\nimport { type Gated } from \"./state.js\";\nimport type { DocsDir } from \"../../../core/kernel/ids/dirs.js\";\nimport type { FleetContext } from \"../../../core/fleet-context.js\";\n\nexport async function gate(\n config: { docsDir: DocsDir; fleet?: FleetContext },\n featureId: string,\n opts: ArchiveOptions,\n say: (line?: string) => void,\n): Promise<Gated | null> {\n const json = opts.json === true;\n const dryRun = opts.dryRun === true;\n // the living docs half-written, and every other command in loam would have\n // planned against them as if they were the truth. Under the same lock, so the\n // repair cannot race a writer. It refuses when the half-merged files have been\n // edited since — that is a human's call, not a merge's.\n const recovered = await recoverInterruptedCommit(config.docsDir);\n if (recovered !== null && !json) sayRecovery(recovered);\n\n const feature = await resolveFeature(config.docsDir, featureId, \"exclude\");\n if (!feature) {\n fail(json, \"unknown-target\", await missingFeatureMessage(config.docsDir, featureId));\n return null;\n }\n // The raw argument's last appearance: from here on the feature answers only to\n // its canonical id — tags, coherence, and the self-exclusion scans all match\n // on `id`, and `archive FEAT-5-slug` must plan exactly like `archive FEAT-5`.\n const { id, dirName, dir: featureDir } = feature;\n\n // Parse delta.likec4 ONCE for the whole run: the coherence gate and the\n // architecture merge below read the same file, and loading it spins up a\n // fresh Langium workspace each time. Nothing writes it in between.\n const deltaLikec4 = featurePaths(featureDir).delta;\n const deltaDoc = existsSync(deltaLikec4) ? await loadFile(deltaLikec4) : undefined;\n\n const deltaServices = await featureSpecServices(featureDir);\n // The business corpus's delta tree, walked once HERE and carried on `Gated`:\n // the conflict-marker scan, the strayed-requirement scan and the merge itself\n // all ask which capability documents this feature rewrites, and three walks\n // are three chances to answer differently. `featureCoherence` below walks it\n // again on its own — it is called without `config.fleet`, the same\n // no-context path `core/coherence/declared.ts` already documents paying for\n // the arch delta — so this is not the run's only walk, only the one every\n // phase downstream of the gate shares.\n const capabilityDeltas = (\n config.fleet === undefined\n ? await featureCapabilityDeltas(featureDir)\n : await config.fleet.featureCapabilityDeltas(featureDir)\n ).docs;\n\n // A `specs/<svc>/` whose NAME is not a legal service id, refused before\n // anything joins that name into a path: the conflict scan below probes\n // `services/<svc>/<axis>` with it, and the requirements merge would\n // materialise `services/<svc>/` from it — a directory `service.id-invalid`\n // then calls an error on the next `validate --all`, and one no loam command\n // can address or re-create. The set comes from coherence/living.ts's\n // `invalidSpecServiceFindings` — the same function validate reads — so the\n // two gates cannot drift.\n //\n // --approve does not apply, exactly as with the conflict-marker refusal\n // below: --approve overrides judgments about the FEATURE, and a name the id\n // grammar refuses is a mechanical fact about the path it would become. The\n // dry run is gated too: a plan built on that name describes a merge loam\n // must never perform.\n const illegalServices = await invalidSpecServiceFindings(featureDir);\n if (illegalServices.length > 0) {\n const msg = `archive ${id} — BLOCKED: ${illegalServices.length} per-service delta(s) named by an illegal service id; --approve does not override this`;\n if (json) {\n refuseFindings(\"not-coherent\", msg, illegalServices, `features/${dirName}`);\n return null;\n }\n console.error(`${msg}:`);\n for (const f of illegalServices) console.error(` ✗ ${f.message}`);\n console.error(`\\n--approve does not override this — the directory the merge would create is one loam can never address, mechanically.`);\n // Every arm in this file refuses by hand — it prints a headline, a list and\n // a closing sentence rather than the one message `fail()` takes — so the\n // code and its lookup have to be asked for explicitly. `sayExplain` is\n // called rather than the format being copied six times, and the code passed\n // is the SAME one the `--json` branch a few lines up emits: text and\n // envelope must name one refusal, or a reader who switches modes to find\n // the code is handed a different answer than the one they saw.\n sayExplain(\"not-coherent\");\n process.exitCode = 1;\n return null;\n }\n\n // Git conflict markers in a LIVING document this merge would rewrite, before\n // anything else is asked of it. Both sides of somebody's merge are in the\n // file, so nothing it says is anyone's text — and the requirements rewrite\n // deletes whichever marker lines fall inside the section it owns, turning a\n // conflict anyone can see into a file nobody can tell is wrong. Checked ahead\n // of the coherence gate for `inspectLandscape`'s reason: a conflicted\n // document parses, so every verdict downstream of it is an opinion about half\n // of two people's work.\n //\n // --approve does not apply, exactly as with the strayed-requirement refusal\n // below: --approve overrides judgments about the FEATURE, and this is a fact\n // about the living document.\n const conflicted = await livingMergeConflicts(config.docsDir, deltaServices, capabilityDeltas.map((d) => d.id));\n if (conflicted.length > 0) {\n const msg = `archive ${id} — BLOCKED: ${conflicted.length} living document(s) still hold git conflict markers`;\n if (json) {\n refuseFindings(\"merge-failed\", msg, conflicted, `features/${dirName}`);\n return null;\n }\n console.error(`${msg}:`);\n for (const f of conflicted) console.error(` ✗ ${f.message}`);\n console.error(`\\n--approve does not override this — the loss is mechanical, not a judgment call.`);\n sayExplain(\"merge-failed\");\n process.exitCode = 1;\n return null;\n }\n\n // Gate: GATING issues block the archive. Severity and gating are two\n // different questions (issue.ts): errors gate because the merge would write\n // something wrong, and the rare warning marked `gates` blocks too — the\n // document is legal but the merge would drop authored content. Advisory\n // warnings are printed (and carried into --json) but never block, matching\n // validate's own doctrine (report.ts: \"warnings never gate\"). --approve\n // overrides the gating issues ONLY, and must say exactly which ones it is\n // walking past. A dry run is gated too: a plan for a merge that would be\n // refused describes nothing that will happen.\n const issues = await featureCoherence({ docsDir: config.docsDir, featureDir, featureId: id, preloadedDelta: deltaDoc });\n // The issues --approve cannot move, carved out before the gate below reads\n // the rest. The register is issue.ts's `approveOverrides` — the same data\n // the `overridable` key in the envelope is resolved from — never a code\n // string spelled here, because two spellings of \"what the flag moves\" would\n // drift. What the register HOLDS has grown: it began as one code (a\n // `metadata { service }` binding the id grammar refuses, where the merge\n // would splice the name into the living map verbatim and probe\n // `services/<binding>/` with it) and now holds the create-only collisions of\n // every whole-file axis besides. They share the doctrine and nothing else —\n // each is a mechanical fact about what the merge would write, never a\n // judgment about the feature — which is why the message below counts them\n // and lets each issue say its own sentence.\n //\n // IT USED TO NAME BINDINGS, and had been wrong for every other code in the\n // register since the use-case slot landed: a feature whose flow collided\n // with a living one was refused with \"1 element binding(s) name an illegal\n // service id\", about a feature that binds nothing. A refusal headline that\n // describes the wrong breach sends its reader to the wrong file first.\n const nonOverridable = issues.filter((i) => !approveOverrides(i));\n if (nonOverridable.length > 0) {\n const msg = `archive ${id} — BLOCKED: ${nonOverridable.length} issue(s) --approve does not override; each names what the merge would write`;\n if (json) {\n refuseJson(\"not-coherent\", msg, issues, `features/${dirName}`);\n return null;\n }\n console.error(`${msg}:`);\n // The whole list, not just the carve-out: the reader is about to fix the\n // feature, and the issues --approve COULD have moved are work they still\n // owe — hiding them here made the second refusal a surprise. The closing\n // line says which of the marks the flag cannot move.\n for (const i of issues) console.error(` ${SEVERITY_MARK[i.severity]} ${i.message}`);\n console.error(\n `\\nOf these, --approve does not override ${nonOverridable.length}: ${[...new Set(nonOverridable.map((i) => i.code))].sort().join(\", \")}. ` +\n \"Each is a mechanical fact about what the merge would write — a name loam cannot resolve, or a file it would replace wholesale — not a judgement the flag can move. \" +\n \"Fix those; the other issues gate as usual.\",\n );\n sayExplain(\"not-coherent\");\n process.exitCode = 1;\n return null;\n }\n const gating = issues.filter(gatesArchive);\n const advisory = issues.filter((i) => !gatesArchive(i));\n if (gating.length > 0 && !opts.approve) {\n const msg = `archive ${id} — BLOCKED: not coherent (${gating.length} gating issue(s), ${advisory.length} advisory warning(s))`;\n if (json) {\n refuseJson(\"not-coherent\", msg, issues, `features/${dirName}`);\n return null;\n }\n console.error(`${msg}:`);\n for (const i of issues) console.error(` ${SEVERITY_MARK[i.severity]} ${i.message}`);\n console.error(`\\nFix the gating issues (advisory warnings never block), or re-run with --approve to override them (may corrupt the living docs).`);\n sayExplain(\"not-coherent\");\n process.exitCode = 1;\n return null;\n }\n if (gating.length > 0) {\n say(`⚠ archiving despite ${gating.length} gating issue(s) (--approve):`);\n for (const i of gating) say(` ${SEVERITY_MARK[i.severity]} ${i.message}`);\n say();\n }\n if (advisory.length > 0) {\n say(`⚠ ${advisory.length} warning(s) (non-blocking):`);\n for (const i of advisory) say(` ⚠ ${i.message}`);\n say();\n }\n\n // A `specs/<svc>/` addressed to a service that exists nowhere. `validate`\n // graded this and archive did not, which is the half that costs something:\n // `specs/<svc>/` is what the merge MATERIALISES `services/<svc>/` from, so a\n // one-character slip in `--touches` archived at exit 0 and left the fleet a\n // living service nobody meant to adopt. The set comes from coherence/living.ts's\n // `unknownDeltaServices` — the same function validate reads — so the two\n // gates cannot drift into disagreeing about which ids are real.\n //\n // Gated exactly like a coherence error, --approve included: which service you\n // meant is a judgment about the FEATURE. A feature that genuinely introduces\n // one has a better escape hatch than --approve anyway — tag its element in\n // delta.likec4, which is also what makes the fleet map true afterwards.\n const unknownServices = await unknownDeltaServices(config.docsDir, featureDir, id, { preloadedDelta: deltaDoc });\n if (unknownServices.length > 0 && !opts.approve) {\n const msg = `archive ${id} — BLOCKED: ${unknownServices.length} per-service delta(s) address a service that does not exist`;\n if (json) {\n refuseFindings(\"not-coherent\", msg, unknownServices, `features/${dirName}`);\n return null;\n }\n console.error(`${msg}:`);\n for (const f of unknownServices) console.error(` ✗ ${f.message}`);\n console.error(`\\nFix the id, or introduce the service in this feature's delta.likec4 — or re-run with --approve to create it anyway.`);\n sayExplain(\"not-coherent\");\n process.exitCode = 1;\n return null;\n }\n if (unknownServices.length > 0) {\n say(`⚠ archiving despite ${unknownServices.length} unknown service(s) (--approve) — this merge creates them:`);\n for (const f of unknownServices) say(` ✗ ${f.message}`);\n say();\n }\n\n // A LIVING requirement outside `## Requirements` is a merge loam cannot do\n // correctly: the rewrite replaces only the requirements RUN inside that\n // section (rewriteRequirementsRun) while parseRequirements collects from\n // every section — so the strayed requirement keeps its authored copy in the\n // preserved prose AND lands again in the rewritten run, and the next\n // archive's MODIFIED replaces only one of them. Refused rather than\n // repaired: excising blocks from prose programmatically would be archive\n // editing text it does not understand. --approve does not apply — it\n // overrides judgments about the FEATURE, and this is neither.\n const strayed: Issue[] = [];\n const scanStrayed = async (livingPath: string, subject: string): Promise<void> => {\n if (!existsSync(livingPath)) return;\n for (const r of parseRequirements(await readUtf8(livingPath))) {\n // The ONE definition of the heading (spec.ts): the guard and the rewrite\n // boundary match the same way, so they cannot disagree about \"outside\".\n if (r.section !== undefined && isRequirementsHeading(r.section)) continue;\n const where = r.section === undefined ? \"above every heading\" : `under '${r.section}'`;\n strayed.push({\n severity: \"error\",\n code: \"living.requirement-outside-requirements\",\n subject,\n message: `${subject}: living requirement '${r.name}' sits ${where} in ${repoPath(config.docsDir, livingPath)} — the merge rewrites only '## Requirements', so it would land in the file twice. Re-home it under '## Requirements' first, then re-run.`,\n });\n }\n };\n for (const svc of deltaServices) {\n for (const axis of SPEC_AXES) {\n if (!existsSync(featureSpecPaths(featureDir, svc)[axis.key])) continue;\n // The shared context, because this sits in a deltaServices × SPEC_AXES\n // loop: without it every iteration re-walks and re-reads the fleet.\n await scanStrayed((await locateServicePaths(config.docsDir, svc, config.fleet))[axis.key], svc);\n }\n }\n // The business corpus, for the identical mechanical reason: a capability\n // document is narrative ABOVE `## Requirements`, `rewriteRequirementsRun`\n // rewrites only the run inside that heading, and a requirement filed under\n // `## Notes` would keep its authored copy in the preserved prose AND land\n // again in the rewritten run.\n for (const doc of capabilityDeltas) {\n await scanStrayed(livingCapabilityPaths(config.docsDir, doc.id).spec, doc.id);\n }\n if (strayed.length > 0) {\n const msg = `archive ${id} — BLOCKED: ${strayed.length} living requirement(s) outside '## Requirements'`;\n if (json) {\n refuseJson(\"living-outside-requirements\", msg, strayed, `features/${dirName}`);\n return null;\n }\n console.error(`${msg}:`);\n for (const i of strayed) console.error(` ✗ ${i.message}`);\n console.error(`\\n--approve does not override this — the duplication is mechanical, not a judgment call.`);\n sayExplain(\"living-outside-requirements\");\n process.exitCode = 1;\n return null;\n }\n\n // Pre-flight: the archive destination must be free, or the final move would fail\n // after the living docs were already rewritten.\n const archiveDir = archiveRoot(config.docsDir);\n const archiveDest = join(archiveDir, dirName);\n if (existsSync(archiveDest)) {\n fail(json, \"archive-exists\", `archive ${id} — BLOCKED: features/archive/${dirName} already exists. Remove or rename it, then re-run.`);\n return null;\n }\n\n say(`archive ${id}${dryRun ? \" (dry run)\" : \"\"}\\n`);\n\n return {\n id,\n dirName,\n featureDir,\n deltaDoc,\n deltaServices,\n capabilityDeltas,\n gating,\n advisory,\n archiveDir,\n archiveDest,\n recovered,\n issues,\n };\n}\n"]}
|
|
@@ -27,6 +27,7 @@ import {} from "./state.js";
|
|
|
27
27
|
import { ArchiveFailure } from "./refusal.js";
|
|
28
28
|
import { featurePaths } from "../../../core/repo/paths.js";
|
|
29
29
|
import { featureFlows, livingFlowPath, USECASE_SUBDIR } from "../../../core/usecases/delta/flows.js";
|
|
30
|
+
import { featureDeployments, livingDeploymentPath } from "../../../core/deployment/delta.js";
|
|
30
31
|
export async function planLandscape(config, gated, plan, say) {
|
|
31
32
|
const { id, featureDir, deltaDoc, deltaServices } = gated;
|
|
32
33
|
const deltaLikec4 = featurePaths(featureDir).delta;
|
|
@@ -158,4 +159,22 @@ export async function planFlows(config, gated, plan, say) {
|
|
|
158
159
|
say(` use case: ${flow.rel} — created architecture/${USECASE_SUBDIR}/${flow.rel}`);
|
|
159
160
|
}
|
|
160
161
|
}
|
|
162
|
+
/**
|
|
163
|
+
* The same copy for the third whole-file axis: the topology a feature brings.
|
|
164
|
+
*
|
|
165
|
+
* Beside `planFlows` rather than folded into it, because the two write to
|
|
166
|
+
* different places — a flow into `architecture/usecases/`, a topology straight
|
|
167
|
+
* into `architecture/` — and one function taking a subdirectory argument would
|
|
168
|
+
* make that difference a parameter instead of a decision each axis states.
|
|
169
|
+
* `unarchive` needs nothing here either: a create is undone by deleting.
|
|
170
|
+
*/
|
|
171
|
+
export async function planDeployments(config, gated, plan, say) {
|
|
172
|
+
const docs = await featureDeployments(gated.featureDir);
|
|
173
|
+
if (docs.length === 0)
|
|
174
|
+
return;
|
|
175
|
+
for (const doc of docs) {
|
|
176
|
+
plan.writes.push(planWrite(livingDeploymentPath(config.docsDir, doc.rel), await readUtf8(doc.path)));
|
|
177
|
+
say(` deployment: ${doc.rel} — created architecture/${doc.rel}`);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
161
180
|
//# sourceMappingURL=landscape.js.map
|