@ybotok/loam 0.1.0-beta.1 → 0.1.0-beta.3
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 +96 -0
- package/README.md +6 -1
- package/SCHEMA.md +22 -4
- package/dist/cli.js +4 -0
- package/dist/cli.js.map +1 -1
- package/dist/commands/adopt.js +17 -3
- package/dist/commands/adopt.js.map +1 -1
- package/dist/commands/archive.js +24 -8
- package/dist/commands/archive.js.map +1 -1
- package/dist/commands/delta.js +74 -47
- package/dist/commands/delta.js.map +1 -1
- package/dist/commands/dependencies.js +7 -2
- package/dist/commands/dependencies.js.map +1 -1
- package/dist/commands/docs-repo-gate.js +104 -0
- package/dist/commands/docs-repo-gate.js.map +1 -0
- package/dist/commands/explore.js +195 -0
- package/dist/commands/explore.js.map +1 -0
- package/dist/commands/format.js +17 -0
- package/dist/commands/format.js.map +1 -0
- package/dist/commands/gherkin.js +87 -48
- package/dist/commands/gherkin.js.map +1 -1
- package/dist/commands/init.js +3 -3
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/instructions.js +80 -0
- package/dist/commands/instructions.js.map +1 -0
- package/dist/commands/list.js +10 -76
- package/dist/commands/list.js.map +1 -1
- package/dist/commands/migrate-openspec.js +45 -28
- package/dist/commands/migrate-openspec.js.map +1 -1
- package/dist/commands/new.js +35 -34
- package/dist/commands/new.js.map +1 -1
- package/dist/commands/rebase.js +2 -4
- package/dist/commands/rebase.js.map +1 -1
- package/dist/commands/show.js +26 -31
- package/dist/commands/show.js.map +1 -1
- package/dist/commands/status.js +17 -25
- package/dist/commands/status.js.map +1 -1
- package/dist/commands/unarchive.js +12 -5
- package/dist/commands/unarchive.js.map +1 -1
- package/dist/commands/validate.js +306 -142
- package/dist/commands/validate.js.map +1 -1
- package/dist/commands/verify.js +33 -17
- package/dist/commands/verify.js.map +1 -1
- package/dist/commands/vouch.js +156 -60
- package/dist/commands/vouch.js.map +1 -1
- package/dist/core/agent.js +392 -42
- package/dist/core/agent.js.map +1 -1
- package/dist/core/agents-stamp.js +47 -10
- package/dist/core/agents-stamp.js.map +1 -1
- package/dist/core/arch.js +28 -2
- package/dist/core/arch.js.map +1 -1
- package/dist/core/asyncapi.js +253 -0
- package/dist/core/asyncapi.js.map +1 -0
- package/dist/core/brief.js +28 -6
- package/dist/core/brief.js.map +1 -1
- package/dist/core/coherence.js +37 -13
- package/dist/core/coherence.js.map +1 -1
- package/dist/core/concurrency.js +51 -0
- package/dist/core/concurrency.js.map +1 -0
- package/dist/core/config.js +10 -2
- package/dist/core/config.js.map +1 -1
- package/dist/core/delta.js +21 -2
- package/dist/core/delta.js.map +1 -1
- package/dist/core/dependencies.js +46 -42
- package/dist/core/dependencies.js.map +1 -1
- package/dist/core/docs.js +65 -3
- package/dist/core/docs.js.map +1 -1
- package/dist/core/doctor.js +79 -27
- package/dist/core/doctor.js.map +1 -1
- package/dist/core/document-bytes.js +59 -0
- package/dist/core/document-bytes.js.map +1 -0
- package/dist/core/explore.js +225 -0
- package/dist/core/explore.js.map +1 -0
- package/dist/core/fleet-context.js +29 -63
- package/dist/core/fleet-context.js.map +1 -1
- package/dist/core/frontmatter.js +1 -5
- package/dist/core/frontmatter.js.map +1 -1
- package/dist/core/gherkin.js +3 -61
- package/dist/core/gherkin.js.map +1 -1
- package/dist/core/health.js +1 -5
- package/dist/core/health.js.map +1 -1
- package/dist/core/ids.js +34 -5
- package/dist/core/ids.js.map +1 -1
- package/dist/core/json.js +8 -1
- package/dist/core/json.js.map +1 -1
- package/dist/core/likec4.js +19 -4
- package/dist/core/likec4.js.map +1 -1
- package/dist/core/maturity.js +66 -0
- package/dist/core/maturity.js.map +1 -0
- package/dist/core/openapi-merge.js +39 -6
- package/dist/core/openapi-merge.js.map +1 -1
- package/dist/core/openapi.js +17 -5
- package/dist/core/openapi.js.map +1 -1
- package/dist/core/openspec-inventory.js +64 -27
- package/dist/core/openspec-inventory.js.map +1 -1
- package/dist/core/path-safety.js +59 -0
- package/dist/core/path-safety.js.map +1 -1
- package/dist/core/provenance.js +102 -10
- package/dist/core/provenance.js.map +1 -1
- package/dist/core/records.js +33 -0
- package/dist/core/records.js.map +1 -0
- package/dist/core/repo.js +77 -1
- package/dist/core/repo.js.map +1 -1
- package/dist/core/report.js +9 -0
- package/dist/core/report.js.map +1 -1
- package/dist/core/results.js +1 -3
- package/dist/core/results.js.map +1 -1
- package/dist/core/spec.js +28 -4
- package/dist/core/spec.js.map +1 -1
- package/dist/core/staging.js +124 -17
- package/dist/core/staging.js.map +1 -1
- package/dist/core/status.js +152 -35
- package/dist/core/status.js.map +1 -1
- package/dist/core/steps.js +72 -0
- package/dist/core/steps.js.map +1 -0
- package/dist/core/verify.js +18 -4
- package/dist/core/verify.js.map +1 -1
- package/dist/core/version.js +17 -1
- package/dist/core/version.js.map +1 -1
- package/package.json +1 -1
package/dist/core/status.js
CHANGED
|
@@ -105,7 +105,7 @@ import { FleetContext } from "./fleet-context.js";
|
|
|
105
105
|
import { gatesArchive } from "./issue.js";
|
|
106
106
|
import { repoPath } from "./json.js";
|
|
107
107
|
import { featureProvenance } from "./provenance.js";
|
|
108
|
-
import { compareIds, featurePaths, featureSpecPaths, featureSpecServices, listFeatures, listServices, servicePaths, } from "./repo.js";
|
|
108
|
+
import { compareIds, featurePaths, featureSpecPaths, featureSpecServices, listFeatures, listServices, servicePaths, SPEC_AXES, } from "./repo.js";
|
|
109
109
|
import { requirementsMissingScenarios } from "./spec.js";
|
|
110
110
|
import { COMMIT_INTENT, readCommitIntent } from "./staging.js";
|
|
111
111
|
import { attestedNotice, featureChecklist, readVerificationState, tallyRecord, verificationPath, verificationVerdict, } from "./verify.js";
|
|
@@ -155,9 +155,21 @@ async function readInterruptedCommit(docsDir) {
|
|
|
155
155
|
return {
|
|
156
156
|
command: intent.command,
|
|
157
157
|
feature: intent.feature,
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
158
|
+
// Coerced here rather than trusted. `readCommitIntent` validates exactly the
|
|
159
|
+
// fields a REPAIR needs — the version, the command, the restore direction,
|
|
160
|
+
// the feature, the two move paths and the file digests — so `host`, `pid`
|
|
161
|
+
// and `at` arrive as whatever the journal happened to hold, `undefined`
|
|
162
|
+
// included. That is worse than null in both renderings: `JSON.stringify`
|
|
163
|
+
// drops an undefined value's key entirely, so `--json` loses the field
|
|
164
|
+
// instead of reporting it empty, and the two human lines print
|
|
165
|
+
// "(undefined, pid undefined, undefined)". These three are the label a
|
|
166
|
+
// person places the crash against and nothing branches on them, so the
|
|
167
|
+
// coercion belongs at this boundary — widening the validator would change
|
|
168
|
+
// which journals `archive` and `unarchive` agree to repair, which is a
|
|
169
|
+
// decision about somebody's half-written docs, not about a label.
|
|
170
|
+
host: typeof intent.host === "string" ? intent.host : null,
|
|
171
|
+
pid: typeof intent.pid === "number" ? intent.pid : null,
|
|
172
|
+
at: typeof intent.at === "string" ? intent.at : null,
|
|
161
173
|
files: intent.files.map((f) => f.path),
|
|
162
174
|
unreadable: false,
|
|
163
175
|
// The recovering command is the interrupted one re-run: it repairs first,
|
|
@@ -222,7 +234,7 @@ export async function featureStatus(docsDir, feature, opts = {}) {
|
|
|
222
234
|
// way round. `--service` is a lens on the table and the steps; a rollup that
|
|
223
235
|
// moved with it would report a feature `ready` because the one service you
|
|
224
236
|
// asked about happens to be written, while three others have nothing.
|
|
225
|
-
const graded = featureArtifacts(docsDir, feature, services, blocking, verification, await
|
|
237
|
+
const graded = featureArtifacts(docsDir, feature, services, blocking, verification, contractsHeldElsewhere(await contractOwners(docsDir, context), feature.id), governs);
|
|
226
238
|
const artifacts = narrowed === undefined ? graded : graded.filter((a) => a.service === null || a.service === narrowed);
|
|
227
239
|
// The dependency index rather than the `*-pending` issue codes: those name the
|
|
228
240
|
// other feature in prose, and prose is not an API — dependencies.ts opens with
|
|
@@ -452,11 +464,20 @@ function fileState(id, service, path, exists, required, faulted) {
|
|
|
452
464
|
* docs already carry the contract, or another feature in flight brings it — the
|
|
453
465
|
* same softening coherence applies with its `*-pending` codes, where an
|
|
454
466
|
* operation "defined by another feature still in flight" is an ordering fact
|
|
455
|
-
* and not a hole. And one way to owe nothing at all: a service
|
|
456
|
-
*
|
|
457
|
-
* calls has no API to write down,
|
|
458
|
-
*
|
|
459
|
-
* telling an author to invent a
|
|
467
|
+
* and not a hole. And one way to owe nothing at all: a service that ALREADY
|
|
468
|
+
* EXISTS under `services/`, has no living contract, and that this feature sends
|
|
469
|
+
* no operation to. A UI or a worker nobody calls has no API to write down,
|
|
470
|
+
* which is the reading `validate` and `list` already take — demanding one from
|
|
471
|
+
* an adopted service of that shape would be loam telling an author to invent a
|
|
472
|
+
* contract.
|
|
473
|
+
*
|
|
474
|
+
* That last exemption is deliberately withheld from a service the living docs
|
|
475
|
+
* have never heard of. A feature that introduces a service and sends it no
|
|
476
|
+
* operation still owes the file, because nothing else in the repository will
|
|
477
|
+
* ever describe its surface, and `c4-api.op-undefined` fires on an op-tagged
|
|
478
|
+
* delta edge whether or not any `Operations:` line was written — so answering
|
|
479
|
+
* "none owed" there printed `(not written — none owed)` in the same payload as
|
|
480
|
+
* an error about that very file.
|
|
460
481
|
*/
|
|
461
482
|
function owesContract(docsDir, svc, contracted, governsOperations) {
|
|
462
483
|
if (existsSync(servicePaths(docsDir, svc).openapi))
|
|
@@ -466,18 +487,49 @@ function owesContract(docsDir, svc, contracted, governsOperations) {
|
|
|
466
487
|
return !existsSync(servicePaths(docsDir, svc).dir) || governsOperations;
|
|
467
488
|
}
|
|
468
489
|
/**
|
|
469
|
-
*
|
|
490
|
+
* Which features in flight carry an `openapi.yaml` for which service — one
|
|
491
|
+
* enumeration of the whole index, service by service.
|
|
492
|
+
*
|
|
493
|
+
* The owners are recorded as feature IDS rather than directories because that
|
|
494
|
+
* is the comparison {@link contractsHeldElsewhere} makes: two directories that
|
|
495
|
+
* spell the same id are one feature as far as this question goes, and an index
|
|
496
|
+
* keyed by directory would let a duplicated id answer "somebody else has it"
|
|
497
|
+
* about its own twin.
|
|
498
|
+
*
|
|
470
499
|
* Enumeration only — the feature list and its `specs/` subdirectories are
|
|
471
500
|
* already in the request-scoped index by the time anything asks.
|
|
472
501
|
*/
|
|
473
|
-
async function
|
|
474
|
-
const out = new
|
|
502
|
+
async function contractOwners(docsDir, context) {
|
|
503
|
+
const out = new Map();
|
|
475
504
|
for (const f of await listFeatures(docsDir, {}, context)) {
|
|
476
|
-
if (f.id === self)
|
|
477
|
-
continue;
|
|
478
505
|
for (const svc of f.services) {
|
|
479
|
-
if (existsSync(featureSpecPaths(f.dir, svc).openapi))
|
|
506
|
+
if (!existsSync(featureSpecPaths(f.dir, svc).openapi))
|
|
507
|
+
continue;
|
|
508
|
+
const owners = out.get(svc);
|
|
509
|
+
if (owners === undefined)
|
|
510
|
+
out.set(svc, new Set([f.id]));
|
|
511
|
+
else
|
|
512
|
+
owners.add(f.id);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
return out;
|
|
516
|
+
}
|
|
517
|
+
/**
|
|
518
|
+
* Services some OTHER active feature already carries an `openapi.yaml` for.
|
|
519
|
+
*
|
|
520
|
+
* Derived from the shared index rather than re-walked, because the answer for
|
|
521
|
+
* feature A and the answer for feature B are two readings of one enumeration:
|
|
522
|
+
* asking it once per feature made the fleet form pay F²·S `existsSync` calls
|
|
523
|
+
* over the two quantities that actually grow.
|
|
524
|
+
*/
|
|
525
|
+
function contractsHeldElsewhere(owners, self) {
|
|
526
|
+
const out = new Set();
|
|
527
|
+
for (const [svc, ids] of owners) {
|
|
528
|
+
for (const id of ids) {
|
|
529
|
+
if (id !== self) {
|
|
480
530
|
out.add(svc);
|
|
531
|
+
break;
|
|
532
|
+
}
|
|
481
533
|
}
|
|
482
534
|
}
|
|
483
535
|
return out;
|
|
@@ -486,13 +538,22 @@ async function contractsInFlight(docsDir, self, context) {
|
|
|
486
538
|
* The record's own rung. `stale` and `unreadable` are `draft` because both mean
|
|
487
539
|
* the file on disk is the thing to fix; unconfirmed claims are `ready` because
|
|
488
540
|
* the file is fine and the answer lives in the code.
|
|
541
|
+
*
|
|
542
|
+
* All four states are spelled, `recorded` included, so the `never` below fails
|
|
543
|
+
* the build rather than the reader: a fifth record state added to
|
|
544
|
+
* {@link VerificationState} would otherwise fall through into the last branch
|
|
545
|
+
* and be graded by `fullyVerified` alone — silently answering `ready` about a
|
|
546
|
+
* record whose new state nobody here has decided a rung for.
|
|
489
547
|
*/
|
|
490
548
|
function verificationStatus(v) {
|
|
491
549
|
if (v.state === "absent")
|
|
492
550
|
return "missing";
|
|
493
551
|
if (v.state === "unreadable" || v.state === "stale")
|
|
494
552
|
return "draft";
|
|
495
|
-
|
|
553
|
+
if (v.state === "recorded")
|
|
554
|
+
return fullyVerified(v) ? "done" : "ready";
|
|
555
|
+
const unreachable = v.state;
|
|
556
|
+
throw new Error(`verificationStatus: no rung for verification state '${String(unreachable)}'`);
|
|
496
557
|
}
|
|
497
558
|
/**
|
|
498
559
|
* Read the record and, only when there is one, ask whether it still answers
|
|
@@ -602,8 +663,12 @@ function featureNext(feature, services, artifacts, findings, blockedBy, verifica
|
|
|
602
663
|
code: "next.author-openapi",
|
|
603
664
|
// Not "the service is not in the living docs": a service adopted
|
|
604
665
|
// without a contract is the commoner case, and that wording sent a
|
|
605
|
-
// reader looking for a directory that was right there.
|
|
606
|
-
|
|
666
|
+
// reader looking for a directory that was right there. And not "the
|
|
667
|
+
// operations it governs" either — this step also fires for a service
|
|
668
|
+
// the feature INTRODUCES, where owesContract asks nothing about
|
|
669
|
+
// operations and the delta may tag none, so naming them promised the
|
|
670
|
+
// author a list they would go looking for and not find.
|
|
671
|
+
statement: `Write ${api.path} — nothing gives ${svc} a living openapi.yaml, so this feature is the only thing that can write down its API surface.`,
|
|
607
672
|
command: `loam delta ${id} --service ${svc} --json`,
|
|
608
673
|
artifact: "openapi",
|
|
609
674
|
service: svc,
|
|
@@ -798,10 +863,11 @@ async function scanDeltas(docsDir, feature, services, context) {
|
|
|
798
863
|
const out = [];
|
|
799
864
|
for (const svc of services) {
|
|
800
865
|
const p = featureSpecPaths(feature.dir, svc);
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
866
|
+
// SPEC_AXES already carries the prose name of each axis — "requirements"
|
|
867
|
+
// and "arch requirements" are its `label`s verbatim — so the pair is read
|
|
868
|
+
// from there rather than spelled again, and a service's status can never
|
|
869
|
+
// walk a different set of spec files than its validation does.
|
|
870
|
+
for (const [file, label] of SPEC_AXES.map((axis) => [p[axis.key], `${svc}: ${axis.label}`])) {
|
|
805
871
|
if (!existsSync(file))
|
|
806
872
|
continue;
|
|
807
873
|
const reqs = await context.readRequirements(file);
|
|
@@ -839,10 +905,20 @@ export async function fleetStatus(docsDir, opts = {}) {
|
|
|
839
905
|
const narrowed = opts.service;
|
|
840
906
|
const all = await listServices(docsDir, context);
|
|
841
907
|
const services = narrowed === undefined ? all : all.filter((s) => s.id === narrowed);
|
|
908
|
+
// The service this repository says it IS, when the fleet has never heard of
|
|
909
|
+
// it. Asked against the UNNARROWED list, because "is it adopted" is a question
|
|
910
|
+
// about the fleet and not about the view.
|
|
911
|
+
const unadopted = opts.bound !== undefined && !all.some((s) => s.id === opts.bound) ? opts.bound : null;
|
|
842
912
|
const graph = await analyzeDependencies(docsDir, undefined, context);
|
|
843
913
|
const entries = await listFeatures(docsDir, {}, context);
|
|
844
914
|
const inScope = narrowed === undefined ? entries : entries.filter((f) => f.services.includes(narrowed));
|
|
845
|
-
|
|
915
|
+
// Once for the fleet, not once per feature: which features hold a contract
|
|
916
|
+
// for which service is one fact about the repository, and every feature below
|
|
917
|
+
// reads its own row out of it. Built over the UNNARROWED list on purpose — a
|
|
918
|
+
// `--service` view still has to know that a feature outside it discharges the
|
|
919
|
+
// contract, or the narrowing would invent an obligation.
|
|
920
|
+
const owners = await contractOwners(docsDir, context);
|
|
921
|
+
const features = await Promise.all(inScope.map((f) => fleetFeature(docsDir, f, graph, contractsHeldElsewhere(owners, f.id), context, interrupted)));
|
|
846
922
|
features.sort((a, b) => compareIds(a.id, b.id));
|
|
847
923
|
return {
|
|
848
924
|
interrupted,
|
|
@@ -855,7 +931,11 @@ export async function fleetStatus(docsDir, opts = {}) {
|
|
|
855
931
|
features,
|
|
856
932
|
order: graph.order.filter((id) => features.some((f) => f.id === id)),
|
|
857
933
|
service: narrowed ?? null,
|
|
858
|
-
|
|
934
|
+
// The binding is passed only when this run was NOT narrowed. `--service X`
|
|
935
|
+
// is an explicit question about X, and answering it with a step about
|
|
936
|
+
// whichever service loam.json happens to name would be a different
|
|
937
|
+
// question's answer at the top of the list.
|
|
938
|
+
next: fleetNext(services, features, graph, interrupted, narrowed === undefined ? unadopted : null),
|
|
859
939
|
};
|
|
860
940
|
}
|
|
861
941
|
/** Nothing is written down: no living spec at all. Every other absence is partial adoption. */
|
|
@@ -866,7 +946,7 @@ function undocumented(s) {
|
|
|
866
946
|
function vouched(s) {
|
|
867
947
|
return s.status === "verified" && s.sources.stamped;
|
|
868
948
|
}
|
|
869
|
-
async function fleetFeature(docsDir, feature, graph, contracted, context) {
|
|
949
|
+
async function fleetFeature(docsDir, feature, graph, contracted, context, interrupted) {
|
|
870
950
|
const paths = featurePaths(feature.dir);
|
|
871
951
|
const missing = [];
|
|
872
952
|
if (!existsSync(paths.intent))
|
|
@@ -893,13 +973,23 @@ async function fleetFeature(docsDir, feature, graph, contracted, context) {
|
|
|
893
973
|
// No `draft` here, ever: that verdict needs the coherence run this form
|
|
894
974
|
// refuses to pay for, and inferring it from presence alone would be a second
|
|
895
975
|
// opinion about validity — the one thing status must never invent.
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
976
|
+
//
|
|
977
|
+
// The interrupted commit leads, exactly as it does in the feature form: every
|
|
978
|
+
// presence test below is a question about files a killed `archive` may have
|
|
979
|
+
// half-written, so answering `done` from them is a claim about bytes nobody
|
|
980
|
+
// has established. Without this the two forms contradicted each other over
|
|
981
|
+
// one repository — `loam status --json` said `done` and offered "ship it"
|
|
982
|
+
// while `loam status <FEAT>` said `blocked` — and the module header's rule is
|
|
983
|
+
// that this projection is never greener than the gates.
|
|
984
|
+
const stage = interrupted !== null
|
|
985
|
+
? "blocked"
|
|
986
|
+
: missing.length > 0
|
|
987
|
+
? "missing"
|
|
988
|
+
: blockedBy.length > 0
|
|
989
|
+
? "blocked"
|
|
990
|
+
: fullyVerified(verification)
|
|
991
|
+
? "done"
|
|
992
|
+
: "ready";
|
|
903
993
|
return {
|
|
904
994
|
id: feature.id,
|
|
905
995
|
dirName: feature.dirName,
|
|
@@ -945,8 +1035,28 @@ export const FLEET_NEXT_LIMIT = 10;
|
|
|
945
1035
|
* once named the command CI runs. This form grades nothing; the least it can do
|
|
946
1036
|
* is say where grading happens.
|
|
947
1037
|
*/
|
|
948
|
-
function fleetNext(services, features, graph, interrupted) {
|
|
1038
|
+
function fleetNext(services, features, graph, interrupted, unadoptedBinding) {
|
|
949
1039
|
const steps = [];
|
|
1040
|
+
// The repository this command is standing in names a service the fleet has no
|
|
1041
|
+
// directory for. First, ahead of every other service's partial adoption,
|
|
1042
|
+
// because it is the only step that is about HERE.
|
|
1043
|
+
//
|
|
1044
|
+
// This form used to count the fleet and ignore the binding entirely, so in a
|
|
1045
|
+
// service repo bound to an unadopted service — a freshly wired one, the most
|
|
1046
|
+
// common repo there is — an empty fleet made "every service is written down"
|
|
1047
|
+
// vacuously true and `next.fleet-clean` came out top. `loam doctor` had the
|
|
1048
|
+
// right answer (`doctor.service-unknown`) at the same moment, and the
|
|
1049
|
+
// documented agent loop reads `status`: an agent following it was sent to
|
|
1050
|
+
// start a feature instead of adopting the service under its feet.
|
|
1051
|
+
if (unadoptedBinding !== null) {
|
|
1052
|
+
steps.push({
|
|
1053
|
+
code: "next.adopt-bound",
|
|
1054
|
+
statement: `This repository's loam.json says it is '${unadoptedBinding}', and the fleet has no ` +
|
|
1055
|
+
`services/${unadoptedBinding}/ — nothing about the service you are standing in is written down yet.`,
|
|
1056
|
+
command: `loam adopt --service ${unadoptedBinding} --json`,
|
|
1057
|
+
service: unadoptedBinding,
|
|
1058
|
+
});
|
|
1059
|
+
}
|
|
950
1060
|
for (const s of services.filter(undocumented)) {
|
|
951
1061
|
steps.push({
|
|
952
1062
|
code: "next.adopt",
|
|
@@ -981,7 +1091,14 @@ function fleetNext(services, features, graph, interrupted) {
|
|
|
981
1091
|
}
|
|
982
1092
|
steps.push({
|
|
983
1093
|
code: "next.feature",
|
|
984
|
-
statement:
|
|
1094
|
+
statement:
|
|
1095
|
+
// The list, not the stage, is what this sentence names. `blocked` has
|
|
1096
|
+
// two causes now — another feature in flight, and an interrupted commit
|
|
1097
|
+
// that stalls the whole repository — and the second one names no
|
|
1098
|
+
// feature, so keying off the stage alone printed "FEAT-1 is waiting
|
|
1099
|
+
// on ." The recover step above already says what the repository is
|
|
1100
|
+
// waiting on.
|
|
1101
|
+
f.stage === "blocked" && f.blockedBy.length > 0
|
|
985
1102
|
? `${f.id} is waiting on ${f.blockedBy.join(", ")}.`
|
|
986
1103
|
: `${f.id} is at '${f.stage}'${f.missing.length > 0 ? ` — missing ${f.missing.join(", ")}` : ""}.`,
|
|
987
1104
|
command: `loam status ${f.id} --json`,
|