@ybotok/loam 0.1.0-beta.1 → 0.1.0-beta.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 +49 -0
- package/README.md +6 -1
- 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 +70 -0
- package/dist/commands/instructions.js.map +1 -0
- package/dist/commands/list.js +6 -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 +8 -24
- 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 +115 -133
- 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 +77 -59
- package/dist/commands/vouch.js.map +1 -1
- package/dist/core/agent.js +273 -31
- package/dist/core/agent.js.map +1 -1
- package/dist/core/arch.js +28 -2
- package/dist/core/arch.js.map +1 -1
- package/dist/core/brief.js +3 -3
- 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 +2 -1
- package/dist/core/docs.js.map +1 -1
- package/dist/core/doctor.js +41 -26
- 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 +16 -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 +6 -1
- 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 +35 -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 +75 -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 +3 -3
- 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 +122 -33
- 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);
|
|
@@ -842,7 +908,13 @@ export async function fleetStatus(docsDir, opts = {}) {
|
|
|
842
908
|
const graph = await analyzeDependencies(docsDir, undefined, context);
|
|
843
909
|
const entries = await listFeatures(docsDir, {}, context);
|
|
844
910
|
const inScope = narrowed === undefined ? entries : entries.filter((f) => f.services.includes(narrowed));
|
|
845
|
-
|
|
911
|
+
// Once for the fleet, not once per feature: which features hold a contract
|
|
912
|
+
// for which service is one fact about the repository, and every feature below
|
|
913
|
+
// reads its own row out of it. Built over the UNNARROWED list on purpose — a
|
|
914
|
+
// `--service` view still has to know that a feature outside it discharges the
|
|
915
|
+
// contract, or the narrowing would invent an obligation.
|
|
916
|
+
const owners = await contractOwners(docsDir, context);
|
|
917
|
+
const features = await Promise.all(inScope.map((f) => fleetFeature(docsDir, f, graph, contractsHeldElsewhere(owners, f.id), context, interrupted)));
|
|
846
918
|
features.sort((a, b) => compareIds(a.id, b.id));
|
|
847
919
|
return {
|
|
848
920
|
interrupted,
|
|
@@ -866,7 +938,7 @@ function undocumented(s) {
|
|
|
866
938
|
function vouched(s) {
|
|
867
939
|
return s.status === "verified" && s.sources.stamped;
|
|
868
940
|
}
|
|
869
|
-
async function fleetFeature(docsDir, feature, graph, contracted, context) {
|
|
941
|
+
async function fleetFeature(docsDir, feature, graph, contracted, context, interrupted) {
|
|
870
942
|
const paths = featurePaths(feature.dir);
|
|
871
943
|
const missing = [];
|
|
872
944
|
if (!existsSync(paths.intent))
|
|
@@ -893,13 +965,23 @@ async function fleetFeature(docsDir, feature, graph, contracted, context) {
|
|
|
893
965
|
// No `draft` here, ever: that verdict needs the coherence run this form
|
|
894
966
|
// refuses to pay for, and inferring it from presence alone would be a second
|
|
895
967
|
// opinion about validity — the one thing status must never invent.
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
968
|
+
//
|
|
969
|
+
// The interrupted commit leads, exactly as it does in the feature form: every
|
|
970
|
+
// presence test below is a question about files a killed `archive` may have
|
|
971
|
+
// half-written, so answering `done` from them is a claim about bytes nobody
|
|
972
|
+
// has established. Without this the two forms contradicted each other over
|
|
973
|
+
// one repository — `loam status --json` said `done` and offered "ship it"
|
|
974
|
+
// while `loam status <FEAT>` said `blocked` — and the module header's rule is
|
|
975
|
+
// that this projection is never greener than the gates.
|
|
976
|
+
const stage = interrupted !== null
|
|
977
|
+
? "blocked"
|
|
978
|
+
: missing.length > 0
|
|
979
|
+
? "missing"
|
|
980
|
+
: blockedBy.length > 0
|
|
981
|
+
? "blocked"
|
|
982
|
+
: fullyVerified(verification)
|
|
983
|
+
? "done"
|
|
984
|
+
: "ready";
|
|
903
985
|
return {
|
|
904
986
|
id: feature.id,
|
|
905
987
|
dirName: feature.dirName,
|
|
@@ -981,7 +1063,14 @@ function fleetNext(services, features, graph, interrupted) {
|
|
|
981
1063
|
}
|
|
982
1064
|
steps.push({
|
|
983
1065
|
code: "next.feature",
|
|
984
|
-
statement:
|
|
1066
|
+
statement:
|
|
1067
|
+
// The list, not the stage, is what this sentence names. `blocked` has
|
|
1068
|
+
// two causes now — another feature in flight, and an interrupted commit
|
|
1069
|
+
// that stalls the whole repository — and the second one names no
|
|
1070
|
+
// feature, so keying off the stage alone printed "FEAT-1 is waiting
|
|
1071
|
+
// on ." The recover step above already says what the repository is
|
|
1072
|
+
// waiting on.
|
|
1073
|
+
f.stage === "blocked" && f.blockedBy.length > 0
|
|
985
1074
|
? `${f.id} is waiting on ${f.blockedBy.join(", ")}.`
|
|
986
1075
|
: `${f.id} is at '${f.stage}'${f.missing.length > 0 ? ` — missing ${f.missing.join(", ")}` : ""}.`,
|
|
987
1076
|
command: `loam status ${f.id} --json`,
|