@ybotok/loam 0.0.0 → 0.1.0-beta.1
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 +89 -0
- package/COMPARISON.md +63 -0
- package/LICENSE +21 -0
- package/MIGRATING-from-OpenSpec.md +202 -0
- package/README.md +241 -9
- package/SCHEMA.md +425 -0
- package/SECURITY.md +9 -0
- package/dist/cli.js +138 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/adopt.js +226 -0
- package/dist/commands/adopt.js.map +1 -0
- package/dist/commands/archive.js +1297 -0
- package/dist/commands/archive.js.map +1 -0
- package/dist/commands/delta.js +361 -0
- package/dist/commands/delta.js.map +1 -0
- package/dist/commands/dependencies.js +78 -0
- package/dist/commands/dependencies.js.map +1 -0
- package/dist/commands/doctor.js +85 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/gherkin.js +270 -0
- package/dist/commands/gherkin.js.map +1 -0
- package/dist/commands/init.js +250 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/list.js +361 -0
- package/dist/commands/list.js.map +1 -0
- package/dist/commands/migrate-openspec.js +933 -0
- package/dist/commands/migrate-openspec.js.map +1 -0
- package/dist/commands/new.js +412 -0
- package/dist/commands/new.js.map +1 -0
- package/dist/commands/rebase.js +358 -0
- package/dist/commands/rebase.js.map +1 -0
- package/dist/commands/show.js +325 -0
- package/dist/commands/show.js.map +1 -0
- package/dist/commands/status.js +270 -0
- package/dist/commands/status.js.map +1 -0
- package/dist/commands/unarchive.js +310 -0
- package/dist/commands/unarchive.js.map +1 -0
- package/dist/commands/validate.js +1609 -0
- package/dist/commands/validate.js.map +1 -0
- package/dist/commands/verify.js +759 -0
- package/dist/commands/verify.js.map +1 -0
- package/dist/commands/vouch.js +325 -0
- package/dist/commands/vouch.js.map +1 -0
- package/dist/core/agent.js +1823 -0
- package/dist/core/agent.js.map +1 -0
- package/dist/core/agents-stamp.js +68 -0
- package/dist/core/agents-stamp.js.map +1 -0
- package/dist/core/arch.js +104 -0
- package/dist/core/arch.js.map +1 -0
- package/dist/core/brief.js +733 -0
- package/dist/core/brief.js.map +1 -0
- package/dist/core/coherence.js +553 -0
- package/dist/core/coherence.js.map +1 -0
- package/dist/core/config.js +172 -0
- package/dist/core/config.js.map +1 -0
- package/dist/core/delta.js +457 -0
- package/dist/core/delta.js.map +1 -0
- package/dist/core/dependencies.js +352 -0
- package/dist/core/dependencies.js.map +1 -0
- package/dist/core/docs.js +143 -0
- package/dist/core/docs.js.map +1 -0
- package/dist/core/doctor.js +544 -0
- package/dist/core/doctor.js.map +1 -0
- package/dist/core/fleet-context.js +272 -0
- package/dist/core/fleet-context.js.map +1 -0
- package/dist/core/frontmatter.js +203 -0
- package/dist/core/frontmatter.js.map +1 -0
- package/dist/core/gherkin.js +521 -0
- package/dist/core/gherkin.js.map +1 -0
- package/dist/core/health.js +71 -0
- package/dist/core/health.js.map +1 -0
- package/dist/core/ids.js +93 -0
- package/dist/core/ids.js.map +1 -0
- package/dist/core/issue.js +9 -0
- package/dist/core/issue.js.map +1 -0
- package/dist/core/json.js +67 -0
- package/dist/core/json.js.map +1 -0
- package/dist/core/likec4.js +474 -0
- package/dist/core/likec4.js.map +1 -0
- package/dist/core/openapi-merge.js +519 -0
- package/dist/core/openapi-merge.js.map +1 -0
- package/dist/core/openapi.js +243 -0
- package/dist/core/openapi.js.map +1 -0
- package/dist/core/openspec-inventory.js +1403 -0
- package/dist/core/openspec-inventory.js.map +1 -0
- package/dist/core/path-safety.js +109 -0
- package/dist/core/path-safety.js.map +1 -0
- package/dist/core/provenance.js +695 -0
- package/dist/core/provenance.js.map +1 -0
- package/dist/core/repo.js +362 -0
- package/dist/core/repo.js.map +1 -0
- package/dist/core/report.js +38 -0
- package/dist/core/report.js.map +1 -0
- package/dist/core/results.js +201 -0
- package/dist/core/results.js.map +1 -0
- package/dist/core/spec.js +481 -0
- package/dist/core/spec.js.map +1 -0
- package/dist/core/staging.js +783 -0
- package/dist/core/staging.js.map +1 -0
- package/dist/core/status.js +1015 -0
- package/dist/core/status.js.map +1 -0
- package/dist/core/verify.js +793 -0
- package/dist/core/verify.js.map +1 -0
- package/dist/core/version.js +8 -0
- package/dist/core/version.js.map +1 -0
- package/package.json +63 -4
package/dist/cli.js
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command, CommanderError } from "commander";
|
|
3
|
+
import { realpathSync } from "node:fs";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { registerInit } from "./commands/init.js";
|
|
6
|
+
import { registerAdopt } from "./commands/adopt.js";
|
|
7
|
+
import { registerList } from "./commands/list.js";
|
|
8
|
+
import { registerNew } from "./commands/new.js";
|
|
9
|
+
import { registerShow } from "./commands/show.js";
|
|
10
|
+
import { registerStatus } from "./commands/status.js";
|
|
11
|
+
import { registerDelta } from "./commands/delta.js";
|
|
12
|
+
import { registerGherkin } from "./commands/gherkin.js";
|
|
13
|
+
import { registerRebase } from "./commands/rebase.js";
|
|
14
|
+
import { registerArchive } from "./commands/archive.js";
|
|
15
|
+
import { registerUnarchive } from "./commands/unarchive.js";
|
|
16
|
+
import { registerValidate } from "./commands/validate.js";
|
|
17
|
+
import { registerVerify } from "./commands/verify.js";
|
|
18
|
+
import { registerVouch } from "./commands/vouch.js";
|
|
19
|
+
import { registerDoctor } from "./commands/doctor.js";
|
|
20
|
+
import { registerDependencies } from "./commands/dependencies.js";
|
|
21
|
+
import { registerMigrateOpenSpec } from "./commands/migrate-openspec.js";
|
|
22
|
+
import { emitJsonError } from "./core/json.js";
|
|
23
|
+
import { LOAM_VERSION } from "./core/version.js";
|
|
24
|
+
/**
|
|
25
|
+
* The whole CLI surface, as a program nobody has parsed yet.
|
|
26
|
+
*
|
|
27
|
+
* Exported because loam PRINTS commands — every `loam …` in AGENTS.md, in the
|
|
28
|
+
* generated command and skill bodies, in a `doctor` finding's `fix` and in a
|
|
29
|
+
* `status` next[] step is an instruction somebody (usually an agent) will type
|
|
30
|
+
* back. The only way to prove one of those parses is to hand it to the program
|
|
31
|
+
* that would receive it; a second copy of the registrations in a test agrees
|
|
32
|
+
* with itself and with nothing else, which is exactly how
|
|
33
|
+
* `Run \`loam adopt <id>\`` shipped as the first instruction a bound service
|
|
34
|
+
* repo received. `test/agent-commands-runnable.test.ts` is the consumer.
|
|
35
|
+
*/
|
|
36
|
+
export function buildProgram() {
|
|
37
|
+
const program = new Command();
|
|
38
|
+
program
|
|
39
|
+
.name("loam")
|
|
40
|
+
.description("Architecture-first spec framework for microservice fleets")
|
|
41
|
+
.version(LOAM_VERSION);
|
|
42
|
+
// Make commander throw a CommanderError instead of process.exit()ing, so a
|
|
43
|
+
// usage error (mistyped flag, unknown command) can still honour the envelope
|
|
44
|
+
// invariant in the catch below instead of dying with an empty stdout.
|
|
45
|
+
program.exitOverride();
|
|
46
|
+
registerInit(program);
|
|
47
|
+
registerAdopt(program);
|
|
48
|
+
registerList(program);
|
|
49
|
+
registerNew(program);
|
|
50
|
+
registerShow(program);
|
|
51
|
+
registerStatus(program);
|
|
52
|
+
registerDelta(program);
|
|
53
|
+
registerGherkin(program);
|
|
54
|
+
registerRebase(program);
|
|
55
|
+
registerArchive(program);
|
|
56
|
+
registerUnarchive(program);
|
|
57
|
+
registerValidate(program);
|
|
58
|
+
registerVerify(program);
|
|
59
|
+
registerVouch(program);
|
|
60
|
+
registerDoctor(program);
|
|
61
|
+
registerDependencies(program);
|
|
62
|
+
registerMigrateOpenSpec(program);
|
|
63
|
+
return program;
|
|
64
|
+
}
|
|
65
|
+
/** CommanderError codes whose output (already printed) is the point, not a failure to wrap. */
|
|
66
|
+
const PASS_THROUGH = new Set(["commander.help", "commander.helpDisplayed", "commander.version"]);
|
|
67
|
+
function main() {
|
|
68
|
+
const program = buildProgram();
|
|
69
|
+
// Set once a command's action actually runs: every subcommand declares its own
|
|
70
|
+
// `--json`, so past that point the parsed option is the authoritative answer.
|
|
71
|
+
let parsedJson;
|
|
72
|
+
program.hook("preAction", (_thisCommand, actionCommand) => {
|
|
73
|
+
parsedJson = Boolean(actionCommand.optsWithGlobals().json);
|
|
74
|
+
});
|
|
75
|
+
/**
|
|
76
|
+
* Did this invocation ask for --json? Prefers the parsed option once an action
|
|
77
|
+
* has run; before that (commander usage errors abort mid-parse) falls back to
|
|
78
|
+
* scanning raw argv. The scan is a heuristic and cannot be made exact: a token
|
|
79
|
+
* spelled `--json` that was meant as an option VALUE (e.g. --title "--json")
|
|
80
|
+
* is indistinguishable from the flag in raw argv. It gets the common cases
|
|
81
|
+
* right: a `--json` token anywhere before a `--` terminator counts, and tokens
|
|
82
|
+
* after `--` never do.
|
|
83
|
+
*/
|
|
84
|
+
const jsonRequested = () => {
|
|
85
|
+
if (parsedJson !== undefined)
|
|
86
|
+
return parsedJson;
|
|
87
|
+
const argv = process.argv.slice(2);
|
|
88
|
+
const terminator = argv.indexOf("--");
|
|
89
|
+
return (terminator === -1 ? argv : argv.slice(0, terminator)).includes("--json");
|
|
90
|
+
};
|
|
91
|
+
program.parseAsync(process.argv).catch((err) => {
|
|
92
|
+
if (err instanceof CommanderError) {
|
|
93
|
+
// Help/version output was already written by commander — pass it through
|
|
94
|
+
// untouched with the exit code it always had.
|
|
95
|
+
//
|
|
96
|
+
// Anything else is a usage error (unknown option/command, missing or
|
|
97
|
+
// invalid argument). Commander has already printed its diagnostic to
|
|
98
|
+
// stderr, and that line stays: the invariant below is about stdout only.
|
|
99
|
+
if (!PASS_THROUGH.has(err.code) && jsonRequested()) {
|
|
100
|
+
emitJsonError("invalid-option", err.message.replace(/^error: /, ""));
|
|
101
|
+
}
|
|
102
|
+
process.exitCode = err.exitCode;
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
106
|
+
// The envelope's one hard invariant is that stdout is JSON whenever --json
|
|
107
|
+
// was asked for — an unexpected throw must not be the exception to it.
|
|
108
|
+
if (jsonRequested()) {
|
|
109
|
+
emitJsonError("internal", message);
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
console.error(message);
|
|
113
|
+
process.exitCode = 1;
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Was this file run, or merely imported? Realpath on both sides because the
|
|
119
|
+
* installed entry point is reached through a symlink (`node_modules/.bin/loam`)
|
|
120
|
+
* on every platform that has them, and a string compare against the link would
|
|
121
|
+
* make the binary a no-op. A failure to resolve reads as "not the entry" only
|
|
122
|
+
* for the import side; `process.argv[1]` that cannot be realpath'd is not a
|
|
123
|
+
* loam invocation either.
|
|
124
|
+
*/
|
|
125
|
+
function invokedDirectly() {
|
|
126
|
+
const entry = process.argv[1];
|
|
127
|
+
if (entry === undefined)
|
|
128
|
+
return false;
|
|
129
|
+
try {
|
|
130
|
+
return realpathSync(entry) === realpathSync(fileURLToPath(import.meta.url));
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
if (invokedDirectly())
|
|
137
|
+
main();
|
|
138
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,OAAO,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AACpD,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACxD,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACxD,OAAO,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAC5D,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC1D,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EAAE,oBAAoB,EAAE,MAAM,4BAA4B,CAAC;AAClE,OAAO,EAAE,uBAAuB,EAAE,MAAM,gCAAgC,CAAC;AACzE,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAC/C,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAEjD;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,YAAY;IAC1B,MAAM,OAAO,GAAG,IAAI,OAAO,EAAE,CAAC;IAE9B,OAAO;SACJ,IAAI,CAAC,MAAM,CAAC;SACZ,WAAW,CAAC,2DAA2D,CAAC;SACxE,OAAO,CAAC,YAAY,CAAC,CAAC;IAEzB,2EAA2E;IAC3E,6EAA6E;IAC7E,sEAAsE;IACtE,OAAO,CAAC,YAAY,EAAE,CAAC;IAEvB,YAAY,CAAC,OAAO,CAAC,CAAC;IACtB,aAAa,CAAC,OAAO,CAAC,CAAC;IACvB,YAAY,CAAC,OAAO,CAAC,CAAC;IACtB,WAAW,CAAC,OAAO,CAAC,CAAC;IACrB,YAAY,CAAC,OAAO,CAAC,CAAC;IACtB,cAAc,CAAC,OAAO,CAAC,CAAC;IACxB,aAAa,CAAC,OAAO,CAAC,CAAC;IACvB,eAAe,CAAC,OAAO,CAAC,CAAC;IACzB,cAAc,CAAC,OAAO,CAAC,CAAC;IACxB,eAAe,CAAC,OAAO,CAAC,CAAC;IACzB,iBAAiB,CAAC,OAAO,CAAC,CAAC;IAC3B,gBAAgB,CAAC,OAAO,CAAC,CAAC;IAC1B,cAAc,CAAC,OAAO,CAAC,CAAC;IACxB,aAAa,CAAC,OAAO,CAAC,CAAC;IACvB,cAAc,CAAC,OAAO,CAAC,CAAC;IACxB,oBAAoB,CAAC,OAAO,CAAC,CAAC;IAC9B,uBAAuB,CAAC,OAAO,CAAC,CAAC;IAEjC,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,+FAA+F;AAC/F,MAAM,YAAY,GAAG,IAAI,GAAG,CAAC,CAAC,gBAAgB,EAAE,yBAAyB,EAAE,mBAAmB,CAAC,CAAC,CAAC;AAEjG,SAAS,IAAI;IACX,MAAM,OAAO,GAAG,YAAY,EAAE,CAAC;IAE/B,+EAA+E;IAC/E,8EAA8E;IAC9E,IAAI,UAA+B,CAAC;IACpC,OAAO,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC,YAAY,EAAE,aAAa,EAAE,EAAE;QACxD,UAAU,GAAG,OAAO,CAAE,aAAa,CAAC,eAAe,EAAyB,CAAC,IAAI,CAAC,CAAC;IACrF,CAAC,CAAC,CAAC;IAEH;;;;;;;;OAQG;IACH,MAAM,aAAa,GAAG,GAAY,EAAE;QAClC,IAAI,UAAU,KAAK,SAAS;YAAE,OAAO,UAAU,CAAC;QAChD,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;QACnC,MAAM,UAAU,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;QACtC,OAAO,CAAC,UAAU,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,UAAU,CAAC,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;IACnF,CAAC,CAAC;IAEF,OAAO,CAAC,UAAU,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,CAAC,GAAY,EAAE,EAAE;QACtD,IAAI,GAAG,YAAY,cAAc,EAAE,CAAC;YAClC,yEAAyE;YACzE,8CAA8C;YAC9C,EAAE;YACF,qEAAqE;YACrE,qEAAqE;YACrE,yEAAyE;YACzE,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,aAAa,EAAE,EAAE,CAAC;gBACnD,aAAa,CAAC,gBAAgB,EAAE,GAAG,CAAC,OAAO,CAAC,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC,CAAC;YACvE,CAAC;YACD,OAAO,CAAC,QAAQ,GAAG,GAAG,CAAC,QAAQ,CAAC;YAChC,OAAO;QACT,CAAC;QACD,MAAM,OAAO,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QACjE,2EAA2E;QAC3E,uEAAuE;QACvE,IAAI,aAAa,EAAE,EAAE,CAAC;YACpB,aAAa,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC;QACrC,CAAC;aAAM,CAAC;YACN,OAAO,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;YACvB,OAAO,CAAC,QAAQ,GAAG,CAAC,CAAC;QACvB,CAAC;IACH,CAAC,CAAC,CAAC;AACL,CAAC;AAED;;;;;;;GAOG;AACH,SAAS,eAAe;IACtB,MAAM,KAAK,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAC9B,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO,KAAK,CAAC;IACtC,IAAI,CAAC;QACH,OAAO,YAAY,CAAC,KAAK,CAAC,KAAK,YAAY,CAAC,aAAa,CAAC,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;IAC9E,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED,IAAI,eAAe,EAAE;IAAE,IAAI,EAAE,CAAC","sourcesContent":["#!/usr/bin/env node\nimport { Command, CommanderError } from \"commander\";\nimport { realpathSync } from \"node:fs\";\nimport { fileURLToPath } from \"node:url\";\nimport { registerInit } from \"./commands/init.js\";\nimport { registerAdopt } from \"./commands/adopt.js\";\nimport { registerList } from \"./commands/list.js\";\nimport { registerNew } from \"./commands/new.js\";\nimport { registerShow } from \"./commands/show.js\";\nimport { registerStatus } from \"./commands/status.js\";\nimport { registerDelta } from \"./commands/delta.js\";\nimport { registerGherkin } from \"./commands/gherkin.js\";\nimport { registerRebase } from \"./commands/rebase.js\";\nimport { registerArchive } from \"./commands/archive.js\";\nimport { registerUnarchive } from \"./commands/unarchive.js\";\nimport { registerValidate } from \"./commands/validate.js\";\nimport { registerVerify } from \"./commands/verify.js\";\nimport { registerVouch } from \"./commands/vouch.js\";\nimport { registerDoctor } from \"./commands/doctor.js\";\nimport { registerDependencies } from \"./commands/dependencies.js\";\nimport { registerMigrateOpenSpec } from \"./commands/migrate-openspec.js\";\nimport { emitJsonError } from \"./core/json.js\";\nimport { LOAM_VERSION } from \"./core/version.js\";\n\n/**\n * The whole CLI surface, as a program nobody has parsed yet.\n *\n * Exported because loam PRINTS commands — every `loam …` in AGENTS.md, in the\n * generated command and skill bodies, in a `doctor` finding's `fix` and in a\n * `status` next[] step is an instruction somebody (usually an agent) will type\n * back. The only way to prove one of those parses is to hand it to the program\n * that would receive it; a second copy of the registrations in a test agrees\n * with itself and with nothing else, which is exactly how\n * `Run \\`loam adopt <id>\\`` shipped as the first instruction a bound service\n * repo received. `test/agent-commands-runnable.test.ts` is the consumer.\n */\nexport function buildProgram(): Command {\n const program = new Command();\n\n program\n .name(\"loam\")\n .description(\"Architecture-first spec framework for microservice fleets\")\n .version(LOAM_VERSION);\n\n // Make commander throw a CommanderError instead of process.exit()ing, so a\n // usage error (mistyped flag, unknown command) can still honour the envelope\n // invariant in the catch below instead of dying with an empty stdout.\n program.exitOverride();\n\n registerInit(program);\n registerAdopt(program);\n registerList(program);\n registerNew(program);\n registerShow(program);\n registerStatus(program);\n registerDelta(program);\n registerGherkin(program);\n registerRebase(program);\n registerArchive(program);\n registerUnarchive(program);\n registerValidate(program);\n registerVerify(program);\n registerVouch(program);\n registerDoctor(program);\n registerDependencies(program);\n registerMigrateOpenSpec(program);\n\n return program;\n}\n\n/** CommanderError codes whose output (already printed) is the point, not a failure to wrap. */\nconst PASS_THROUGH = new Set([\"commander.help\", \"commander.helpDisplayed\", \"commander.version\"]);\n\nfunction main(): void {\n const program = buildProgram();\n\n // Set once a command's action actually runs: every subcommand declares its own\n // `--json`, so past that point the parsed option is the authoritative answer.\n let parsedJson: boolean | undefined;\n program.hook(\"preAction\", (_thisCommand, actionCommand) => {\n parsedJson = Boolean((actionCommand.optsWithGlobals() as { json?: boolean }).json);\n });\n\n /**\n * Did this invocation ask for --json? Prefers the parsed option once an action\n * has run; before that (commander usage errors abort mid-parse) falls back to\n * scanning raw argv. The scan is a heuristic and cannot be made exact: a token\n * spelled `--json` that was meant as an option VALUE (e.g. --title \"--json\")\n * is indistinguishable from the flag in raw argv. It gets the common cases\n * right: a `--json` token anywhere before a `--` terminator counts, and tokens\n * after `--` never do.\n */\n const jsonRequested = (): boolean => {\n if (parsedJson !== undefined) return parsedJson;\n const argv = process.argv.slice(2);\n const terminator = argv.indexOf(\"--\");\n return (terminator === -1 ? argv : argv.slice(0, terminator)).includes(\"--json\");\n };\n\n program.parseAsync(process.argv).catch((err: unknown) => {\n if (err instanceof CommanderError) {\n // Help/version output was already written by commander — pass it through\n // untouched with the exit code it always had.\n //\n // Anything else is a usage error (unknown option/command, missing or\n // invalid argument). Commander has already printed its diagnostic to\n // stderr, and that line stays: the invariant below is about stdout only.\n if (!PASS_THROUGH.has(err.code) && jsonRequested()) {\n emitJsonError(\"invalid-option\", err.message.replace(/^error: /, \"\"));\n }\n process.exitCode = err.exitCode;\n return;\n }\n const message = err instanceof Error ? err.message : String(err);\n // The envelope's one hard invariant is that stdout is JSON whenever --json\n // was asked for — an unexpected throw must not be the exception to it.\n if (jsonRequested()) {\n emitJsonError(\"internal\", message);\n } else {\n console.error(message);\n process.exitCode = 1;\n }\n });\n}\n\n/**\n * Was this file run, or merely imported? Realpath on both sides because the\n * installed entry point is reached through a symlink (`node_modules/.bin/loam`)\n * on every platform that has them, and a string compare against the link would\n * make the binary a no-op. A failure to resolve reads as \"not the entry\" only\n * for the import side; `process.argv[1]` that cannot be realpath'd is not a\n * loam invocation either.\n */\nfunction invokedDirectly(): boolean {\n const entry = process.argv[1];\n if (entry === undefined) return false;\n try {\n return realpathSync(entry) === realpathSync(fileURLToPath(import.meta.url));\n } catch {\n return false;\n }\n}\n\nif (invokedDirectly()) main();\n"]}
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { closeIds } from "../core/arch.js";
|
|
3
|
+
import { loadConfig } from "../core/config.js";
|
|
4
|
+
import { InvalidIdError, assertServiceId } from "../core/ids.js";
|
|
5
|
+
import { emitJson, fail, reportNoConfig } from "../core/json.js";
|
|
6
|
+
import { DocsRepoUnavailableError, listServices, servicePaths } from "../core/repo.js";
|
|
7
|
+
import { serviceBrief, VIA_ALL } from "../core/brief.js";
|
|
8
|
+
/**
|
|
9
|
+
* What is suspicious about this invocation without being wrong.
|
|
10
|
+
*
|
|
11
|
+
* Adopting a service that has no `services/<id>/` yet is the NORMAL case — it
|
|
12
|
+
* is the whole point of the command — so neither of these can be a refusal.
|
|
13
|
+
* But both used to be silent, and both produce the same outcome: a complete,
|
|
14
|
+
* validating baseline written under a directory name nobody meant. `adopt
|
|
15
|
+
* --service biling-service` briefed a phantom service beside the real
|
|
16
|
+
* `billing-service`, and an agent following the brief created it.
|
|
17
|
+
*/
|
|
18
|
+
async function invocationWarnings(docsDir, service, bound) {
|
|
19
|
+
const warnings = [];
|
|
20
|
+
if (bound !== undefined && bound !== service) {
|
|
21
|
+
warnings.push(`This repository's loam.json declares service '${bound}', but you asked to adopt '${service}'. ` +
|
|
22
|
+
`That is legal — one repo can author another service's baseline — but \`loam vouch\`, ` +
|
|
23
|
+
`\`loam gherkin\` and \`loam verify --service\` all bind to '${bound}' here, so the baseline ` +
|
|
24
|
+
`you write cannot be vouched from this repo. If '${service}' is a typo, fix it; otherwise run ` +
|
|
25
|
+
`adopt from '${service}''s own repository.`);
|
|
26
|
+
}
|
|
27
|
+
if (existsSync(servicePaths(docsDir, service).dir))
|
|
28
|
+
return warnings;
|
|
29
|
+
// A near-miss is only computable when the docs repo can be enumerated at all;
|
|
30
|
+
// a repo that refuses enumeration is a different problem, reported by every
|
|
31
|
+
// other command, and adopt has nothing useful to add to it.
|
|
32
|
+
let known = [];
|
|
33
|
+
try {
|
|
34
|
+
known = (await listServices(docsDir)).map((s) => s.id);
|
|
35
|
+
}
|
|
36
|
+
catch (err) {
|
|
37
|
+
if (!(err instanceof DocsRepoUnavailableError))
|
|
38
|
+
throw err;
|
|
39
|
+
return warnings;
|
|
40
|
+
}
|
|
41
|
+
const close = closeIds(service, known);
|
|
42
|
+
if (close.length > 0) {
|
|
43
|
+
warnings.push(`There is no services/${service}/ yet, but the docs repo already has ${close.join(", ")}. ` +
|
|
44
|
+
`If '${service}' is a misspelling of one of those, stop now: an agent following this brief ` +
|
|
45
|
+
`writes a complete, validating baseline for a service that does not exist, and nothing ` +
|
|
46
|
+
`downstream can tell it from a real one. If it really is a new service, this is expected.`);
|
|
47
|
+
}
|
|
48
|
+
return warnings;
|
|
49
|
+
}
|
|
50
|
+
export function registerAdopt(program) {
|
|
51
|
+
program
|
|
52
|
+
.command("adopt")
|
|
53
|
+
.description("Brief an agent to write one service's baseline docs from its code, and say what will be checked")
|
|
54
|
+
.option("--service <id>", "service to adopt (defaults to the configured service)")
|
|
55
|
+
.option("--json", "emit the machine contract instead of the human view")
|
|
56
|
+
.action(async (opts) => {
|
|
57
|
+
const json = opts.json === true;
|
|
58
|
+
const config = await loadConfig();
|
|
59
|
+
if (!config) {
|
|
60
|
+
reportNoConfig(json);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
const service = opts.service ?? config.service;
|
|
64
|
+
if (service === undefined) {
|
|
65
|
+
fail(json, "invalid-option", "No service. Pass --service <id> or set it in loam.json.");
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
// The id becomes `services/<id>/` in a shared repo, so it goes through the
|
|
69
|
+
// one grammar before anything interpolates it into a path.
|
|
70
|
+
try {
|
|
71
|
+
assertServiceId(service, opts.service === undefined ? "service (loam.json)" : "--service");
|
|
72
|
+
}
|
|
73
|
+
catch (err) {
|
|
74
|
+
if (!(err instanceof InvalidIdError))
|
|
75
|
+
throw err;
|
|
76
|
+
fail(json, "invalid-option", err.message);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
const warnings = await invocationWarnings(config.docsDir, service, config.service);
|
|
80
|
+
const brief = await serviceBrief(config.docsDir, service);
|
|
81
|
+
if (json) {
|
|
82
|
+
emitJson({ ...brief, warnings });
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
render(brief, warnings);
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
/* ------------------------------------------------------------------ */
|
|
89
|
+
/* Text */
|
|
90
|
+
/* ------------------------------------------------------------------ */
|
|
91
|
+
function render(b, warnings) {
|
|
92
|
+
console.log(`adopt ${b.service} — write the baseline into ${b.path}/\n`);
|
|
93
|
+
// In the header, before the work: a warning printed after eight artifact
|
|
94
|
+
// shapes is a warning read after the agent has started writing.
|
|
95
|
+
for (const w of warnings) {
|
|
96
|
+
console.log(wrap(`! ${w}`, " "));
|
|
97
|
+
console.log("");
|
|
98
|
+
}
|
|
99
|
+
console.log(wrap(b.rule, " "));
|
|
100
|
+
console.log("\n artifacts\n");
|
|
101
|
+
const width = Math.max(...b.targets.map((t) => t.artifact.length));
|
|
102
|
+
for (const t of b.targets) {
|
|
103
|
+
// `edit` is the shared fleet map: the file exists, but "present" would read
|
|
104
|
+
// as "nothing to do here", which is the opposite of why it is on the list.
|
|
105
|
+
const flag = t.action === "edit" ? "UNDRAWN" : t.exists ? "present" : t.required ? "MISSING" : "missing";
|
|
106
|
+
console.log(` ${t.artifact.padEnd(width)} ${t.action.padEnd(6)} ${flag.padEnd(7)} ${t.purpose}`);
|
|
107
|
+
}
|
|
108
|
+
for (const t of b.targets)
|
|
109
|
+
printShape(t);
|
|
110
|
+
printLandscape(b);
|
|
111
|
+
console.log("\n frontmatter — on every markdown artifact\n");
|
|
112
|
+
for (const [field, what] of Object.entries(b.frontmatter.fields)) {
|
|
113
|
+
console.log(` ${field}`);
|
|
114
|
+
console.log(wrap(what, " "));
|
|
115
|
+
}
|
|
116
|
+
console.log(`\n never write by hand: ${b.frontmatter.never.join(", ")}`);
|
|
117
|
+
console.log(`\n${wrap(b.frontmatter.why, " ")}`);
|
|
118
|
+
// Attribution matters here: the fleet cross-check runs under `--all`, and a
|
|
119
|
+
// header promising it to `--service` sent agents chasing a finding that
|
|
120
|
+
// invocation never reports.
|
|
121
|
+
console.log(`\n what \`loam validate --service ${b.service}\` then checks\n`);
|
|
122
|
+
for (const c of b.checks) {
|
|
123
|
+
if (c.via !== VIA_ALL)
|
|
124
|
+
printCheck(c);
|
|
125
|
+
}
|
|
126
|
+
console.log(`\n and what only \`loam validate --all\` surfaces\n`);
|
|
127
|
+
for (const c of b.checks) {
|
|
128
|
+
if (c.via === VIA_ALL)
|
|
129
|
+
printCheck(c);
|
|
130
|
+
}
|
|
131
|
+
console.log("\n what nothing checks\n");
|
|
132
|
+
for (const u of b.unchecked)
|
|
133
|
+
console.log(bullet(u, " "));
|
|
134
|
+
console.log("\n when you are done\n");
|
|
135
|
+
console.log(` loam validate --service ${b.service} --json # fix every error`);
|
|
136
|
+
// The fleet run is not a nicety: --service never reports the landscape
|
|
137
|
+
// cross-check, so a baseline can pass the line above with the fleet map
|
|
138
|
+
// never edited at all.
|
|
139
|
+
console.log(` loam validate --all --json # in the docs repo: the fleet map`);
|
|
140
|
+
console.log(` loam vouch --service ${b.service} # a HUMAN, in the service's own repo`);
|
|
141
|
+
console.log("");
|
|
142
|
+
console.log(wrap("Leave everything `status: draft`. Vouching is somebody reading the code and saying the document matches it — it is not yours to do, and a status with no digest behind it is a claim with nothing behind it.", " "));
|
|
143
|
+
}
|
|
144
|
+
function printCheck(c) {
|
|
145
|
+
console.log(` ${c.severity === "error" ? "✗" : "⚠"} ${c.code}`);
|
|
146
|
+
console.log(wrap(c.what, " "));
|
|
147
|
+
}
|
|
148
|
+
const ACTION_NOTE = {
|
|
149
|
+
create: "",
|
|
150
|
+
diff: " (exists — diff, do not replace)",
|
|
151
|
+
edit: " (the whole fleet's file — ADD to it, never rewrite it)",
|
|
152
|
+
};
|
|
153
|
+
function printShape(t) {
|
|
154
|
+
console.log(`\n ${t.path}${ACTION_NOTE[t.action]}\n`);
|
|
155
|
+
for (const rule of t.shape)
|
|
156
|
+
console.log(bullet(rule, " "));
|
|
157
|
+
if (t.example === undefined)
|
|
158
|
+
return;
|
|
159
|
+
console.log("");
|
|
160
|
+
for (const line of t.example.trimEnd().split("\n"))
|
|
161
|
+
console.log(` ${line}`.trimEnd());
|
|
162
|
+
}
|
|
163
|
+
function printLandscape(b) {
|
|
164
|
+
const l = b.landscape;
|
|
165
|
+
console.log("\n what the fleet already says about this service\n");
|
|
166
|
+
// The instruction is the same sentence the JSON carries, printed from the
|
|
167
|
+
// same field — the text view and `--json` cannot drift into briefing two
|
|
168
|
+
// different writes.
|
|
169
|
+
if (!l.present) {
|
|
170
|
+
console.log(" architecture/landscape.likec4 does not exist yet — nothing to bind to.");
|
|
171
|
+
console.log("");
|
|
172
|
+
console.log(wrap(l.instruction ?? "", " "));
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
if (!l.parses) {
|
|
176
|
+
console.log(" architecture/landscape.likec4 does not parse — bind by hand, and fix it first.");
|
|
177
|
+
console.log("");
|
|
178
|
+
console.log(wrap(l.instruction ?? "", " "));
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
if (l.elements.length === 0) {
|
|
182
|
+
console.log(` Nothing in the landscape models '${b.service}'.`);
|
|
183
|
+
console.log("");
|
|
184
|
+
console.log(wrap(l.instruction ?? "", " "));
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
for (const e of l.elements) {
|
|
188
|
+
const bound = e.bound ? "bound" : "matched by title — bind it";
|
|
189
|
+
console.log(` ${e.id} = ${e.kind} '${e.title}' (${bound})`);
|
|
190
|
+
}
|
|
191
|
+
for (const e of l.inbound)
|
|
192
|
+
console.log(` ← ${e.from} ${e.op ?? `"${e.title ?? ""}"`}`);
|
|
193
|
+
for (const e of l.outbound)
|
|
194
|
+
console.log(` → ${e.to} ${e.op ?? `"${e.title ?? ""}"`}`);
|
|
195
|
+
if (l.expects.length > 0) {
|
|
196
|
+
console.log("");
|
|
197
|
+
console.log(wrap(`openapi.yaml must define ${l.expects.map((o) => `'${o}'`).join(", ")} — the fleet already calls ${l.expects.length === 1 ? "it" : "them"}, and a contract that omits ${l.expects.length === 1 ? "it" : "them"} fails spine.op-undefined the moment it lands.`, " "));
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Wrap prose so the brief is readable in a terminal. Continuation lines are
|
|
202
|
+
* indented past the marker, so a wrapped bullet cannot be misread as two.
|
|
203
|
+
*/
|
|
204
|
+
function wrap(text, indent, width = 88) {
|
|
205
|
+
const hang = /^\s*-\s/.test(text) ? `${indent} ` : indent;
|
|
206
|
+
const out = [];
|
|
207
|
+
let line = indent;
|
|
208
|
+
let pad = indent;
|
|
209
|
+
for (const word of text.trim().split(/\s+/)) {
|
|
210
|
+
if (line.length > pad.length && line.length + 1 + word.length > width) {
|
|
211
|
+
out.push(line);
|
|
212
|
+
pad = hang;
|
|
213
|
+
line = hang + word;
|
|
214
|
+
}
|
|
215
|
+
else {
|
|
216
|
+
line = line.length > pad.length ? `${line} ${word}` : line + word;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
out.push(line);
|
|
220
|
+
return out.join("\n");
|
|
221
|
+
}
|
|
222
|
+
/** A bullet, wrapped with its continuation lines hanging under the text. */
|
|
223
|
+
function bullet(text, indent) {
|
|
224
|
+
return wrap(`- ${text}`, indent);
|
|
225
|
+
}
|
|
226
|
+
//# sourceMappingURL=adopt.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"adopt.js","sourceRoot":"","sources":["../../src/commands/adopt.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `loam adopt` — the bootstrap flow, as a contract with an agent.\n *\n * It emits a BRIEF, not an extraction: the artifacts one service's baseline is\n * made of, the grammar each has to be written in, what the fleet already says\n * about the service, the checks that follow, and the checks that do not exist.\n * The agent reads the code; loam states the work and then judges the result.\n *\n * It writes nothing. A command that produced the documents it is asking for\n * would be doing the half of the job it exists to hand over.\n */\nimport type { Command } from \"commander\";\nimport { existsSync } from \"node:fs\";\nimport { closeIds } from \"../core/arch.js\";\nimport { loadConfig } from \"../core/config.js\";\nimport { InvalidIdError, assertServiceId } from \"../core/ids.js\";\nimport { emitJson, fail, reportNoConfig } from \"../core/json.js\";\nimport { DocsRepoUnavailableError, listServices, servicePaths } from \"../core/repo.js\";\nimport { serviceBrief, VIA_ALL, type Brief, type BriefCheck, type BriefTarget } from \"../core/brief.js\";\n\ninterface AdoptOptions {\n service?: string;\n json?: boolean;\n}\n\n/**\n * What is suspicious about this invocation without being wrong.\n *\n * Adopting a service that has no `services/<id>/` yet is the NORMAL case — it\n * is the whole point of the command — so neither of these can be a refusal.\n * But both used to be silent, and both produce the same outcome: a complete,\n * validating baseline written under a directory name nobody meant. `adopt\n * --service biling-service` briefed a phantom service beside the real\n * `billing-service`, and an agent following the brief created it.\n */\nasync function invocationWarnings(\n docsDir: string,\n service: string,\n bound: string | undefined,\n): Promise<string[]> {\n const warnings: string[] = [];\n\n if (bound !== undefined && bound !== service) {\n warnings.push(\n `This repository's loam.json declares service '${bound}', but you asked to adopt '${service}'. ` +\n `That is legal — one repo can author another service's baseline — but \\`loam vouch\\`, ` +\n `\\`loam gherkin\\` and \\`loam verify --service\\` all bind to '${bound}' here, so the baseline ` +\n `you write cannot be vouched from this repo. If '${service}' is a typo, fix it; otherwise run ` +\n `adopt from '${service}''s own repository.`,\n );\n }\n\n if (existsSync(servicePaths(docsDir, service).dir)) return warnings;\n\n // A near-miss is only computable when the docs repo can be enumerated at all;\n // a repo that refuses enumeration is a different problem, reported by every\n // other command, and adopt has nothing useful to add to it.\n let known: string[] = [];\n try {\n known = (await listServices(docsDir)).map((s) => s.id);\n } catch (err) {\n if (!(err instanceof DocsRepoUnavailableError)) throw err;\n return warnings;\n }\n const close = closeIds(service, known);\n if (close.length > 0) {\n warnings.push(\n `There is no services/${service}/ yet, but the docs repo already has ${close.join(\", \")}. ` +\n `If '${service}' is a misspelling of one of those, stop now: an agent following this brief ` +\n `writes a complete, validating baseline for a service that does not exist, and nothing ` +\n `downstream can tell it from a real one. If it really is a new service, this is expected.`,\n );\n }\n return warnings;\n}\n\nexport function registerAdopt(program: Command): void {\n program\n .command(\"adopt\")\n .description(\"Brief an agent to write one service's baseline docs from its code, and say what will be checked\")\n .option(\"--service <id>\", \"service to adopt (defaults to the configured service)\")\n .option(\"--json\", \"emit the machine contract instead of the human view\")\n .action(async (opts: AdoptOptions) => {\n const json = opts.json === true;\n\n const config = await loadConfig();\n if (!config) {\n reportNoConfig(json);\n return;\n }\n\n const service = opts.service ?? config.service;\n if (service === undefined) {\n fail(json, \"invalid-option\", \"No service. Pass --service <id> or set it in loam.json.\");\n return;\n }\n // The id becomes `services/<id>/` in a shared repo, so it goes through the\n // one grammar before anything interpolates it into a path.\n try {\n assertServiceId(service, opts.service === undefined ? \"service (loam.json)\" : \"--service\");\n } catch (err) {\n if (!(err instanceof InvalidIdError)) throw err;\n fail(json, \"invalid-option\", err.message);\n return;\n }\n\n const warnings = await invocationWarnings(config.docsDir, service, config.service);\n const brief = await serviceBrief(config.docsDir, service);\n if (json) {\n emitJson({ ...brief, warnings });\n return;\n }\n render(brief, warnings);\n });\n}\n\n/* ------------------------------------------------------------------ */\n/* Text */\n/* ------------------------------------------------------------------ */\n\nfunction render(b: Brief, warnings: string[]): void {\n console.log(`adopt ${b.service} — write the baseline into ${b.path}/\\n`);\n // In the header, before the work: a warning printed after eight artifact\n // shapes is a warning read after the agent has started writing.\n for (const w of warnings) {\n console.log(wrap(`! ${w}`, \" \"));\n console.log(\"\");\n }\n console.log(wrap(b.rule, \" \"));\n\n console.log(\"\\n artifacts\\n\");\n const width = Math.max(...b.targets.map((t) => t.artifact.length));\n for (const t of b.targets) {\n // `edit` is the shared fleet map: the file exists, but \"present\" would read\n // as \"nothing to do here\", which is the opposite of why it is on the list.\n const flag = t.action === \"edit\" ? \"UNDRAWN\" : t.exists ? \"present\" : t.required ? \"MISSING\" : \"missing\";\n console.log(` ${t.artifact.padEnd(width)} ${t.action.padEnd(6)} ${flag.padEnd(7)} ${t.purpose}`);\n }\n\n for (const t of b.targets) printShape(t);\n\n printLandscape(b);\n\n console.log(\"\\n frontmatter — on every markdown artifact\\n\");\n for (const [field, what] of Object.entries(b.frontmatter.fields)) {\n console.log(` ${field}`);\n console.log(wrap(what, \" \"));\n }\n console.log(`\\n never write by hand: ${b.frontmatter.never.join(\", \")}`);\n console.log(`\\n${wrap(b.frontmatter.why, \" \")}`);\n\n // Attribution matters here: the fleet cross-check runs under `--all`, and a\n // header promising it to `--service` sent agents chasing a finding that\n // invocation never reports.\n console.log(`\\n what \\`loam validate --service ${b.service}\\` then checks\\n`);\n for (const c of b.checks) {\n if (c.via !== VIA_ALL) printCheck(c);\n }\n console.log(`\\n and what only \\`loam validate --all\\` surfaces\\n`);\n for (const c of b.checks) {\n if (c.via === VIA_ALL) printCheck(c);\n }\n\n console.log(\"\\n what nothing checks\\n\");\n for (const u of b.unchecked) console.log(bullet(u, \" \"));\n\n console.log(\"\\n when you are done\\n\");\n console.log(` loam validate --service ${b.service} --json # fix every error`);\n // The fleet run is not a nicety: --service never reports the landscape\n // cross-check, so a baseline can pass the line above with the fleet map\n // never edited at all.\n console.log(` loam validate --all --json # in the docs repo: the fleet map`);\n console.log(` loam vouch --service ${b.service} # a HUMAN, in the service's own repo`);\n console.log(\"\");\n console.log(\n wrap(\n \"Leave everything `status: draft`. Vouching is somebody reading the code and saying the document matches it — it is not yours to do, and a status with no digest behind it is a claim with nothing behind it.\",\n \" \",\n ),\n );\n}\n\nfunction printCheck(c: BriefCheck): void {\n console.log(` ${c.severity === \"error\" ? \"✗\" : \"⚠\"} ${c.code}`);\n console.log(wrap(c.what, \" \"));\n}\n\nconst ACTION_NOTE: Record<BriefTarget[\"action\"], string> = {\n create: \"\",\n diff: \" (exists — diff, do not replace)\",\n edit: \" (the whole fleet's file — ADD to it, never rewrite it)\",\n};\n\nfunction printShape(t: BriefTarget): void {\n console.log(`\\n ${t.path}${ACTION_NOTE[t.action]}\\n`);\n for (const rule of t.shape) console.log(bullet(rule, \" \"));\n if (t.example === undefined) return;\n console.log(\"\");\n for (const line of t.example.trimEnd().split(\"\\n\")) console.log(` ${line}`.trimEnd());\n}\n\nfunction printLandscape(b: Brief): void {\n const l = b.landscape;\n console.log(\"\\n what the fleet already says about this service\\n\");\n // The instruction is the same sentence the JSON carries, printed from the\n // same field — the text view and `--json` cannot drift into briefing two\n // different writes.\n if (!l.present) {\n console.log(\" architecture/landscape.likec4 does not exist yet — nothing to bind to.\");\n console.log(\"\");\n console.log(wrap(l.instruction ?? \"\", \" \"));\n return;\n }\n if (!l.parses) {\n console.log(\" architecture/landscape.likec4 does not parse — bind by hand, and fix it first.\");\n console.log(\"\");\n console.log(wrap(l.instruction ?? \"\", \" \"));\n return;\n }\n if (l.elements.length === 0) {\n console.log(` Nothing in the landscape models '${b.service}'.`);\n console.log(\"\");\n console.log(wrap(l.instruction ?? \"\", \" \"));\n return;\n }\n for (const e of l.elements) {\n const bound = e.bound ? \"bound\" : \"matched by title — bind it\";\n console.log(` ${e.id} = ${e.kind} '${e.title}' (${bound})`);\n }\n for (const e of l.inbound) console.log(` ← ${e.from} ${e.op ?? `\"${e.title ?? \"\"}\"`}`);\n for (const e of l.outbound) console.log(` → ${e.to} ${e.op ?? `\"${e.title ?? \"\"}\"`}`);\n if (l.expects.length > 0) {\n console.log(\"\");\n console.log(\n wrap(\n `openapi.yaml must define ${l.expects.map((o) => `'${o}'`).join(\", \")} — the fleet already calls ${l.expects.length === 1 ? \"it\" : \"them\"}, and a contract that omits ${l.expects.length === 1 ? \"it\" : \"them\"} fails spine.op-undefined the moment it lands.`,\n \" \",\n ),\n );\n }\n}\n\n/**\n * Wrap prose so the brief is readable in a terminal. Continuation lines are\n * indented past the marker, so a wrapped bullet cannot be misread as two.\n */\nfunction wrap(text: string, indent: string, width = 88): string {\n const hang = /^\\s*-\\s/.test(text) ? `${indent} ` : indent;\n const out: string[] = [];\n let line = indent;\n let pad = indent;\n for (const word of text.trim().split(/\\s+/)) {\n if (line.length > pad.length && line.length + 1 + word.length > width) {\n out.push(line);\n pad = hang;\n line = hang + word;\n } else {\n line = line.length > pad.length ? `${line} ${word}` : line + word;\n }\n }\n out.push(line);\n return out.join(\"\\n\");\n}\n\n/** A bullet, wrapped with its continuation lines hanging under the text. */\nfunction bullet(text: string, indent: string): string {\n return wrap(`- ${text}`, indent);\n}\n"]}
|