@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
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"init.js","sourceRoot":"","sources":["../../src/commands/init.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Command } from \"commander\";\nimport { existsSync, statSync } from \"node:fs\";\nimport { join, resolve } from \"node:path\";\nimport {\n CONFIG_FILENAME,\n findConfigPath,\n loadConfig,\n localConfigPath,\n saveConfig,\n type LoamConfig,\n} from \"../core/config.js\";\nimport { emitJson, fail } from \"../core/json.js\";\nimport { plannedDocsFiles, scaffoldDocs } from \"../core/docs.js\";\nimport { docsRepoState, listServices } from \"../core/repo.js\";\nimport { InvalidIdError, assertServiceId } from \"../core/ids.js\";\nimport {\n AGENT_TOOLS,\n detectAgentTools,\n plannedCommandFiles,\n scaffoldAgentCommands,\n type Delivery,\n} from \"../core/agent.js\";\n\ninterface InitOptions {\n docs: string;\n service?: string;\n /** --create: make a new docs repo at --docs instead of joining an existing one. */\n create?: boolean;\n /** --force: write loam.json here even though one already governs a parent directory. */\n force?: boolean;\n /** commander's --no-commands: true unless the flag is passed. */\n commands: boolean;\n /** commander's --no-skills: true unless the flag is passed. */\n skills: boolean;\n /** --tools: comma-separated AGENT_TOOLS ids, or \"all\". Absent = autodetect. */\n tools?: string;\n json?: boolean;\n}\n\n/**\n * Resolve --tools to registry ids, or null after reporting the refusal. The\n * default (the cwd scan) lives at the call site, not in commander, so an\n * explicit `--tools` is distinguishable from none — which is what lets both the\n * autodetection fallback and the `--no-*` contradiction be decided instead of\n * silently arbitrated.\n */\nfunction resolveTools(raw: string, json: boolean): string[] | null {\n const supported = Object.keys(AGENT_TOOLS);\n if (raw === \"all\") return supported;\n const ids = [...new Set(raw.split(\",\").map((t) => t.trim()).filter((t) => t !== \"\"))];\n const unknown = ids.filter((t) => !(t in AGENT_TOOLS));\n if (ids.length === 0 || unknown.length > 0) {\n fail(\n json,\n \"invalid-option\",\n (ids.length === 0\n ? \"--tools names no tool.\"\n : `--tools does not recognize: ${unknown.join(\", \")}.`) +\n ` Supported: ${supported.join(\", \")} — or \"all\".`,\n );\n return null;\n }\n return ids;\n}\n\n/**\n * `--docs` exactly as the caller wrote it, with separators normalised.\n *\n * Stored verbatim on purpose. `init` used to resolve it and write the absolute\n * result, which meant a committed loam.json named a directory that existed on\n * one laptop: every teammate who cloned the service repo got a docsDir under\n * someone else's home directory, and `loam list` reported an empty fleet\n * instead of saying so. `loadConfig` resolves relative paths against the config\n * file's own directory, so `../docs` keeps meaning \"next to this repo\" wherever\n * the pair is checked out. An absolute `--docs` is still stored absolute — that\n * is the caller's explicit choice, and `loam doctor` warns about it.\n *\n * Backslashes become forward slashes so a config written on Windows resolves on\n * POSIX; a trailing separator is dropped so the stored spelling is stable.\n */\nfunction storedDocsDir(raw: string): string {\n const slashed = raw.split(\"\\\\\").join(\"/\");\n if (slashed.length <= 1) return slashed;\n const trimmed = slashed.replace(/\\/+$/, \"\");\n return trimmed === \"\" ? \"/\" : trimmed;\n}\n\n/**\n * Does this directory already hold a docs repo? `services/` plus `AGENTS.md` —\n * the two things every docs repo has and no service repo does. The pair is the\n * whole point: a single marker would make `init --docs ../srv` (a typo for\n * `../docs`) look like a join and adopt the service repo as the fleet.\n */\nfunction isDocsRepo(dir: string): boolean {\n return docsRepoState(dir).kind === \"ok\" && existsSync(join(dir, \"AGENTS.md\"));\n}\n\nexport function registerInit(program: Command): void {\n program\n .command(\"init\")\n .description(\"Initialize loam: create/point at the shared docs repo and write local config\")\n .option(\"--docs <dir>\", \"path to the shared docs repo (the source of truth)\", \".loam-docs\")\n .option(\"--service <id>\", \"canonical id of the service in this repo\")\n .option(\"--create\", \"create a new docs repo at --docs instead of joining an existing one\")\n .option(\"--force\", `write ${CONFIG_FILENAME} here even though one governs a parent directory`)\n .option(\n \"--tools <ids>\",\n \"agent tools to generate for, overriding the scan of this directory: \" +\n `comma-separated (${Object.keys(AGENT_TOOLS).join(\", \")}) or \"all\"`,\n )\n .option(\"--no-commands\", \"skip the slash commands for this repo entirely\")\n .option(\"--no-skills\", \"skip the agent skills for this repo entirely\")\n .option(\"--json\", \"emit the machine contract instead of the human view\")\n .action(async (opts: InitOptions, command: Command) => {\n const json = opts.json === true;\n const cwd = process.cwd();\n\n // The id becomes services/<id>/ in the shared repo, so it is validated\n // before anything is written — by the same rule adopt, vouch and gherkin\n // use, never by a guard invented here.\n if (opts.service !== undefined) {\n try {\n assertServiceId(opts.service);\n } catch (err) {\n if (!(err instanceof InvalidIdError)) throw err;\n fail(json, \"invalid-option\", err.message);\n return;\n }\n }\n\n // A loam.json in an ancestor already governs this directory: every command\n // run from here resolves to it. Writing a second one below it would shadow\n // the first for this subtree only — the kind of split-brain nobody\n // discovers until two commands disagree about which docs repo is the fleet.\n const governing = findConfigPath(cwd);\n if (governing !== null && governing !== localConfigPath(cwd) && opts.force !== true) {\n fail(\n json,\n \"already-exists\",\n `${governing} already governs this directory. ` +\n `Run loam from ${resolve(governing, \"..\")}, or pass --force to give this ` +\n `subdirectory its own ${CONFIG_FILENAME}.`,\n );\n return;\n }\n\n // Only THIS directory's config is spread forward. Under --force there is\n // a config in an ancestor too, and inheriting its `service` would bind\n // this repo to a service somebody else's repo declared.\n const existing = existsSync(localConfigPath(cwd)) ? await loadConfig(cwd) : null;\n\n // A re-run must not move a committed pointer. `--docs` has a default\n // (`.loam-docs`), so an init invoked for any OTHER reason — `--service`,\n // a new `--tools`, the fix `doctor.agent-files-missing` prints — used to\n // arrive here carrying a docsDir nobody typed: the default then named no\n // docs repo, the refusal steered the caller to `--create`, and `--create`\n // rewrote `docsDir` in the committed loam.json to a freshly scaffolded\n // empty one. `validate --all` went green over an empty fleet, which is\n // the exact outcome `--create` exists to prevent. So the flag wins only\n // when it was actually passed; otherwise the file this repo already\n // committed does.\n const committedDocs = existing?.docsDirAsWritten;\n const docsTyped = command.getOptionValueSource(\"docs\") !== \"default\";\n const docsSource: \"flag\" | \"config\" | \"default\" =\n docsTyped ? \"flag\" : committedDocs === undefined ? \"default\" : \"config\";\n const docsOption = !docsTyped && committedDocs !== undefined ? committedDocs : opts.docs;\n const docsDir = resolve(cwd, docsOption);\n\n // The one predictable failure: --docs naming a file. Refused here so the\n // caller gets a clean envelope/message instead of mkdir's ENOTDIR throw.\n if (existsSync(docsDir) && !statSync(docsDir).isDirectory()) {\n fail(json, \"invalid-option\", `--docs points at a file, not a directory: ${docsDir}`);\n return;\n }\n\n // Joining an existing docs repo and creating a new one are different\n // intentions, and `init` used to perform the second one whenever the first\n // was misspelled: `--docs ../dcos` scaffolded a second, empty docs repo\n // beside the real one, and the fleet quietly split in two. Creation is now\n // something the caller asks for.\n const joining = isDocsRepo(docsDir);\n if (!joining && opts.create !== true) {\n fail(\n json,\n \"invalid-option\",\n docsRepoState(docsDir).kind === \"missing\"\n ? `--docs points at a directory that does not exist: ${docsDir}. ` +\n \"If that is a typo, fix it; to create a new docs repo there, pass --create.\"\n : `--docs points at ${docsDir}, which is not a docs repo ` +\n \"(a docs repo has services/ and AGENTS.md). \" +\n \"Point --docs at the shared docs repo, or pass --create to make a new one here.\",\n );\n return;\n }\n\n // --tools selects the files to write; --no-commands and --no-skills each\n // say write none of that kind. Together they contradict — refused, not\n // arbitrated, in either combination.\n const suppressed = [\n ...(opts.commands ? [] : [\"--no-commands\"]),\n ...(opts.skills ? [] : [\"--no-skills\"]),\n ];\n if (opts.tools !== undefined && suppressed.length > 0) {\n fail(\n json,\n \"invalid-option\",\n `--tools contradicts ${suppressed.join(\" and \")}: ` +\n \"one selects the files to generate, the other suppresses them.\",\n );\n return;\n }\n const delivery: Delivery[] = [\n ...(opts.commands ? ([\"commands\"] as const) : []),\n ...(opts.skills ? ([\"skills\"] as const) : []),\n ];\n\n // Without --tools the repo decides: whichever tools have left their own\n // dot-directories here. Falling back to claude keeps a bare repo behaving\n // exactly as it did before the scan existed. `detected` is reported either\n // way — what the scan saw is a fact about the repo, not about the flags.\n const detected = detectAgentTools(cwd);\n const scanned = detected.length > 0 ? detected : [\"claude\"];\n const tools = opts.tools === undefined ? scanned : resolveTools(opts.tools, json);\n if (tools === null) return;\n\n // `skipped` is the other half of the never-overwrite contract: the\n // scaffolds return only what they created, so what they left alone is\n // probed here, before they run — in the order they would create it, so\n // created + skipped is the same list on every repo. Both halves come from\n // the scaffolds' own planned-file lists, so the probe cannot drift from\n // what is actually written.\n const agentFiles = delivery.length > 0 ? plannedCommandFiles(cwd, tools, delivery) : [];\n const candidates = [...plannedDocsFiles(docsDir), ...agentFiles.map((f) => f.path)];\n const skipped = candidates.filter((p) => existsSync(p));\n\n // Joining touches the docs repo not at all: it is somebody's committed\n // repository, and \"point at it\" must never mean \"write to it\".\n const created = joining && opts.create !== true ? [] : (await scaffoldDocs(docsDir)).created;\n const generatedFor = delivery.length > 0 ? tools : [];\n if (delivery.length > 0) {\n created.push(...(await scaffoldAgentCommands(cwd, tools, delivery)));\n }\n\n const stored = storedDocsDir(docsOption);\n // Union, not replacement: this run's files join the ones earlier runs left\n // on disk, and `agentTools` is a record of what the repo HOLDS. A run that\n // generated nothing adds nothing and erases nothing.\n const recordedTools = [...new Set([...(existing?.agentTools ?? []), ...generatedFor])];\n const config: LoamConfig = {\n ...existing,\n docsDir: stored,\n ...(opts.service ? { service: opts.service } : {}),\n ...(recordedTools.length > 0 ? { agentTools: recordedTools } : {}),\n };\n const configFile = await saveConfig(config, cwd);\n\n const serviceCount = (await listServices(docsDir)).length;\n\n if (json) {\n // `docsDir` stays the resolved absolute path — it is what a consumer\n // needs to find the repo. `docsDirStored` is what was written to\n // loam.json, which is the fact this command is now careful about.\n // `tools` names what files were generated FOR — empty when every\n // delivery was suppressed and none were. `detected` is the separate\n // question of what the scan SAW, which `--tools` overrides without\n // changing: an agent comparing the two learns whether its selection\n // matches the repo.\n emitJson({\n docsDir,\n docsDirStored: stored,\n // Where the pointer came from: `flag` (a --docs somebody typed),\n // `config` (the one this repo already committed — a re-run cannot\n // move it), or `default` (nothing said, nothing committed).\n docsDirSource: docsSource,\n docsRepo: joining ? \"existing\" : \"created\",\n services: serviceCount,\n created,\n skipped,\n tools: generatedFor,\n detected,\n });\n return;\n }\n\n console.log(\"loam initialized.\");\n if (joining) {\n console.log(` docs repo: ${docsDir}`);\n console.log(` pointing at existing docs repo (${serviceCount} services)`);\n } else {\n console.log(` docs repo: ${docsDir} (created)`);\n }\n console.log(\n ` docsDir: ${stored} (as stored in ${CONFIG_FILENAME}`\n + (docsSource === \"config\" ? \" — kept; no --docs was passed\" : \"\") + \")\",\n );\n if (config.service) console.log(` service: ${config.service}`);\n console.log(` config: ${configFile}`);\n console.log(` commit ${CONFIG_FILENAME} — docsDir is resolved relative to it`);\n if (opts.tools === undefined && detected.length > 0) {\n console.log(` detected: ${detected.join(\", \")}`);\n }\n if (opts.commands) console.log(` commands: ${tools.join(\", \")}`);\n if (opts.skills) console.log(` skills: ${tools.join(\", \")}`);\n if (created.length > 0) {\n console.log(\" scaffolded:\");\n for (const c of created) console.log(` + ${c}`);\n }\n });\n}\n"]}
|
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { loadConfig } from "../core/config.js";
|
|
3
|
+
import { emitJson, fail, repoPath, reportNoConfig } from "../core/json.js";
|
|
4
|
+
import { loadFile, serviceResolver } from "../core/likec4.js";
|
|
5
|
+
import { DocsRepoUnavailableError, compareIds, landscapePath, listFeatures, listServices, servicePaths, } from "../core/repo.js";
|
|
6
|
+
import { featureChecklist, readVerification, tallyRecord, verificationVerdict, } from "../core/verify.js";
|
|
7
|
+
import { docsRepoReady, reportDocsRepoError } from "./validate.js";
|
|
8
|
+
const SECTIONS = ["services", "features"];
|
|
9
|
+
export function registerList(program) {
|
|
10
|
+
program
|
|
11
|
+
.command("list")
|
|
12
|
+
.argument("[section]", "services | features (default: both)")
|
|
13
|
+
.description("List the services and features in the docs repo")
|
|
14
|
+
.option("--json", "emit the machine contract instead of the human view")
|
|
15
|
+
.option("--archived", "include archived features")
|
|
16
|
+
.option("--needs-work", "services only: the adoption worklist — every service below `vouched`, with what it is missing")
|
|
17
|
+
.action(async (section, opts) => {
|
|
18
|
+
const json = opts.json === true;
|
|
19
|
+
const wanted = opts.needsWork
|
|
20
|
+
? SECTIONS.filter((s) => s === "services")
|
|
21
|
+
: section
|
|
22
|
+
? SECTIONS.filter((s) => s === section)
|
|
23
|
+
: SECTIONS;
|
|
24
|
+
if (section && !SECTIONS.includes(section)) {
|
|
25
|
+
// `invalid-option`, same as show's bad --type: one mistake class, one code.
|
|
26
|
+
fail(json, "invalid-option", `Unknown section '${section}'. Expected: ${SECTIONS.join(" | ")}.`);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
if (opts.needsWork && section === "features") {
|
|
30
|
+
fail(json, "invalid-option", "--needs-work is the service adoption worklist; drop the 'features' section.");
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
const config = await loadConfig();
|
|
34
|
+
if (!config) {
|
|
35
|
+
reportNoConfig(json);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
const { docsDir } = config;
|
|
39
|
+
// A docsDir that is not a docs repo is refused, never rendered as an empty
|
|
40
|
+
// fleet — the whole point of the gate (see validate.ts's docsRepoReady).
|
|
41
|
+
// `list features` alone does not need services/, so it asks for less.
|
|
42
|
+
if (!docsRepoReady(json, docsDir, wanted.includes("services") ? "services" : "docs"))
|
|
43
|
+
return;
|
|
44
|
+
try {
|
|
45
|
+
const services = wanted.includes("services") ? await listServices(docsDir) : undefined;
|
|
46
|
+
const views = services ? await serviceViews(docsDir, services, config.service) : undefined;
|
|
47
|
+
const features = wanted.includes("features")
|
|
48
|
+
? await listFeatures(docsDir, { includeArchived: opts.archived })
|
|
49
|
+
: undefined;
|
|
50
|
+
const verification = features
|
|
51
|
+
? await Promise.all(features.map((f) => featureVerification(docsDir, f)))
|
|
52
|
+
: undefined;
|
|
53
|
+
const worklist = views?.filter((v) => v.maturity !== "vouched");
|
|
54
|
+
if (json) {
|
|
55
|
+
emitJson({
|
|
56
|
+
docsDir,
|
|
57
|
+
...(views
|
|
58
|
+
? {
|
|
59
|
+
services: (opts.needsWork ? worklist : views).map((v) => serviceJson(docsDir, v)),
|
|
60
|
+
maturity: maturityRollup(views),
|
|
61
|
+
}
|
|
62
|
+
: {}),
|
|
63
|
+
...(features
|
|
64
|
+
? { features: features.map((f, i) => featureJson(docsDir, f, verification[i] ?? null)) }
|
|
65
|
+
: {}),
|
|
66
|
+
});
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
if (opts.needsWork) {
|
|
70
|
+
printWorklist(worklist, views.length);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
if (views)
|
|
74
|
+
printServices(views);
|
|
75
|
+
if (views && features)
|
|
76
|
+
console.log("");
|
|
77
|
+
if (features)
|
|
78
|
+
printFeatures(features, verification);
|
|
79
|
+
}
|
|
80
|
+
catch (err) {
|
|
81
|
+
if (err instanceof DocsRepoUnavailableError) {
|
|
82
|
+
reportDocsRepoError(json, err);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
// One unreadable file used to escape as a stack trace (`internal` in
|
|
86
|
+
// --json), naming nothing. The enumeration is all-or-nothing — it reads
|
|
87
|
+
// every service's frontmatter to build one table — so the honest answer
|
|
88
|
+
// is a refusal that says WHICH path could not be read.
|
|
89
|
+
const path = err.path;
|
|
90
|
+
if (path === undefined)
|
|
91
|
+
throw err;
|
|
92
|
+
fail(json, "repository-unavailable", `${path} could not be read, so the listing would be missing a service. ${err instanceof Error ? err.message : String(err)}`);
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Read the living landscape ONCE and decide, per service, whether an API is
|
|
98
|
+
* expected of it. `serviceResolver` is handed the real service ids so an edge
|
|
99
|
+
* drawn into a modelled container (`paymentService.api`) counts for the service
|
|
100
|
+
* that owns it — the same resolution `validate` uses, so the two commands
|
|
101
|
+
* cannot disagree about who is called.
|
|
102
|
+
*/
|
|
103
|
+
async function serviceViews(docsDir, services, boundService) {
|
|
104
|
+
const known = new Set(services.map((s) => s.id));
|
|
105
|
+
const lp = landscapePath(docsDir);
|
|
106
|
+
const land = existsSync(lp) ? await loadFile(lp) : null;
|
|
107
|
+
const called = new Set();
|
|
108
|
+
// Positive evidence only: with no landscape, or one that does not parse, we
|
|
109
|
+
// know nothing about who is called and every service is assumed to have an
|
|
110
|
+
// API — the conservative direction, because claiming a service needs no
|
|
111
|
+
// contract is how a real one goes undocumented.
|
|
112
|
+
const proven = land !== null && land.errors.length === 0;
|
|
113
|
+
if (proven) {
|
|
114
|
+
const svcOf = serviceResolver(land.elements, known);
|
|
115
|
+
for (const r of land.relationships) {
|
|
116
|
+
if (r.op !== undefined)
|
|
117
|
+
called.add(svcOf(r.target));
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return services.map((entry) => {
|
|
121
|
+
const archSpec = existsSync(servicePaths(docsDir, entry.id).archSpec);
|
|
122
|
+
const apiExpected = !proven || called.has(entry.id);
|
|
123
|
+
const view = {
|
|
124
|
+
entry,
|
|
125
|
+
archSpec,
|
|
126
|
+
apiExpected,
|
|
127
|
+
unverifiableFromHere: entry.sources.declared && boundService !== entry.id,
|
|
128
|
+
};
|
|
129
|
+
const missing = gaps(view);
|
|
130
|
+
return { ...view, maturity: maturity(view), missing };
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
/* ------------------------------------------------------------------ */
|
|
134
|
+
/* JSON */
|
|
135
|
+
/* ------------------------------------------------------------------ */
|
|
136
|
+
function serviceJson(docsDir, v) {
|
|
137
|
+
const s = v.entry;
|
|
138
|
+
return {
|
|
139
|
+
id: s.id,
|
|
140
|
+
path: repoPath(docsDir, s.dir),
|
|
141
|
+
has: { ...s.has, archSpec: v.archSpec },
|
|
142
|
+
adrs: s.adrs,
|
|
143
|
+
status: s.status,
|
|
144
|
+
maturity: v.maturity,
|
|
145
|
+
missing: v.missing,
|
|
146
|
+
apiExpected: v.apiExpected,
|
|
147
|
+
// Never omitted when true: a consumer that filters this table into a
|
|
148
|
+
// worklist has to be able to tell "checked and fine" from "not checkable
|
|
149
|
+
// from here", and absence reads as the first one.
|
|
150
|
+
...(v.unverifiableFromHere ? { provenance: "unverifiable-from-here" } : {}),
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
function featureJson(docsDir, f, verification) {
|
|
154
|
+
return {
|
|
155
|
+
id: f.id,
|
|
156
|
+
dirName: f.dirName,
|
|
157
|
+
path: repoPath(docsDir, f.dir),
|
|
158
|
+
archived: f.archived,
|
|
159
|
+
services: f.services,
|
|
160
|
+
has: f.has,
|
|
161
|
+
verification,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
/* ------------------------------------------------------------------ */
|
|
165
|
+
/* Maturity — the adoption campaign's dial */
|
|
166
|
+
/* ------------------------------------------------------------------ */
|
|
167
|
+
/** The adoption-maturity ladder, in order. Each rung stands on every rung below it. */
|
|
168
|
+
const MATURITY_LADDER = ["empty", "partial", "documented", "sourced", "vouched"];
|
|
169
|
+
/**
|
|
170
|
+
* One monotone word for how far a service's documentation has got. Derived
|
|
171
|
+
* from artifact PRESENCE and provenance state only — the data list already
|
|
172
|
+
* holds — never from what the artifacts say. COMPLETENESS of adopted docs is
|
|
173
|
+
* explicitly on the unchecked list (brief.ts): a service with one endpoint
|
|
174
|
+
* documented out of thirty climbs this ladder exactly as fast as a thorough
|
|
175
|
+
* one, which is why no rung is called "adopted".
|
|
176
|
+
*
|
|
177
|
+
* empty services/<id>/ exists, no artifact is in it
|
|
178
|
+
* partial some artifacts, but not the required set
|
|
179
|
+
* documented the artifacts the adopt brief marks required are present
|
|
180
|
+
* sourced the living spec declares `sources` — something ties it to code
|
|
181
|
+
* vouched status: verified with a sources_digest behind it — a person
|
|
182
|
+
* stamped it. `verified` with no digest is a claim with nothing
|
|
183
|
+
* behind it and stays below this rung.
|
|
184
|
+
*
|
|
185
|
+
* `openapi.yaml` is required ONLY where an API is expected. The rung used to
|
|
186
|
+
* demand it of everything, which permanently pinned every worker, cron and
|
|
187
|
+
* consumer in the fleet at `partial` — services that are fully documented and
|
|
188
|
+
* vouched for, reported as unfinished forever, and a rollup that therefore said
|
|
189
|
+
* a correctly-adopted fleet was half-done. The evidence is the same one
|
|
190
|
+
* `validate` uses to keep `service.no-openapi` quiet, so a service cannot be
|
|
191
|
+
* green in one command and unfinished in the other.
|
|
192
|
+
*/
|
|
193
|
+
function maturity(v) {
|
|
194
|
+
const s = v.entry;
|
|
195
|
+
if (!Object.values(s.has).some(Boolean) && !v.archSpec && s.adrs === 0)
|
|
196
|
+
return "empty";
|
|
197
|
+
if (!(s.has.model && s.has.spec && (!v.apiExpected || s.has.openapi)))
|
|
198
|
+
return "partial";
|
|
199
|
+
if (!s.sources.declared)
|
|
200
|
+
return "documented";
|
|
201
|
+
if (!(s.status === "verified" && s.sources.stamped))
|
|
202
|
+
return "sourced";
|
|
203
|
+
return "vouched";
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* What stands between this service and `vouched`, named as the thing to go and
|
|
207
|
+
* do. One rung's worth at a time: telling somebody at `empty` that they also
|
|
208
|
+
* need a vouch is noise, and the vouch is not even possible yet.
|
|
209
|
+
*/
|
|
210
|
+
function gaps(v) {
|
|
211
|
+
const s = v.entry;
|
|
212
|
+
const files = [
|
|
213
|
+
...(s.has.model ? [] : ["model.likec4"]),
|
|
214
|
+
...(s.has.spec ? [] : ["spec.md"]),
|
|
215
|
+
...(v.apiExpected && !s.has.openapi ? ["openapi.yaml"] : []),
|
|
216
|
+
];
|
|
217
|
+
if (files.length > 0)
|
|
218
|
+
return files;
|
|
219
|
+
if (!s.sources.declared)
|
|
220
|
+
return ["sources: in the spec.md frontmatter"];
|
|
221
|
+
if (!(s.status === "verified" && s.sources.stamped))
|
|
222
|
+
return [`\`loam vouch --service ${s.id}\``];
|
|
223
|
+
return [];
|
|
224
|
+
}
|
|
225
|
+
/** Counts per rung, every rung present — a stable shape a fleet dashboard can diff. */
|
|
226
|
+
function maturityRollup(views) {
|
|
227
|
+
const out = Object.fromEntries(MATURITY_LADDER.map((m) => [m, 0]));
|
|
228
|
+
for (const v of views)
|
|
229
|
+
out[v.maturity] += 1;
|
|
230
|
+
return out;
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* The verification column. `readVerification` is one file read; the checklist
|
|
234
|
+
* is derived only when there is a record to judge — on a fleet-sized repo most
|
|
235
|
+
* features have none, and deriving N checklists to print N dashes is the cost
|
|
236
|
+
* that would get this column dropped.
|
|
237
|
+
*
|
|
238
|
+
* An archived feature's record is frozen history (see verify.ts): archive
|
|
239
|
+
* merged the feature's operations into the living openapi, so a re-derived
|
|
240
|
+
* checklist can only mismatch — the record is reported as recorded, never
|
|
241
|
+
* judged stale.
|
|
242
|
+
*/
|
|
243
|
+
async function featureVerification(docsDir, f) {
|
|
244
|
+
const v = await readVerification(f.dir);
|
|
245
|
+
if (v === null)
|
|
246
|
+
return null;
|
|
247
|
+
const stale = f.archived
|
|
248
|
+
? false
|
|
249
|
+
: (await featureChecklist(docsDir, f.dir, f.id)).digest !== v.checklist;
|
|
250
|
+
const tally = tallyRecord(v);
|
|
251
|
+
return {
|
|
252
|
+
state: stale ? "stale" : "recorded",
|
|
253
|
+
recorded: v.recorded,
|
|
254
|
+
verdict: verificationVerdict(tally, stale),
|
|
255
|
+
confirmed: tally.confirmed,
|
|
256
|
+
claims: tally.claims,
|
|
257
|
+
attested: tally.attested,
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
/* ------------------------------------------------------------------ */
|
|
261
|
+
/* Text */
|
|
262
|
+
/* ------------------------------------------------------------------ */
|
|
263
|
+
/**
|
|
264
|
+
* Fixed-width presence flags. `a` and `A` are deliberately the same letter in
|
|
265
|
+
* two cases: the arch spec and the API are the two things a reader most often
|
|
266
|
+
* mistakes for each other, and one is lowercase because it is optional.
|
|
267
|
+
*/
|
|
268
|
+
function serviceFlags(v) {
|
|
269
|
+
const s = v.entry;
|
|
270
|
+
return [
|
|
271
|
+
s.has.model ? "M" : "-",
|
|
272
|
+
s.has.spec ? "S" : "-",
|
|
273
|
+
v.archSpec ? "a" : "-",
|
|
274
|
+
s.has.openapi ? "A" : "-",
|
|
275
|
+
s.has.runbook ? "R" : "-",
|
|
276
|
+
s.has.health ? "H" : "-",
|
|
277
|
+
].join(" ");
|
|
278
|
+
}
|
|
279
|
+
function printServices(views) {
|
|
280
|
+
console.log(`services (${views.length}) [M]odel [S]pec [a]rch-spec [A]pi [R]unbook [H]ealth`);
|
|
281
|
+
const width = Math.max(0, ...views.map((v) => v.entry.id.length));
|
|
282
|
+
const rungWidth = Math.max(0, ...views.map((v) => v.maturity.length));
|
|
283
|
+
for (const v of views) {
|
|
284
|
+
const s = v.entry;
|
|
285
|
+
const adrs = s.adrs > 0 ? ` (${s.adrs} adr${s.adrs === 1 ? "" : "s"})` : "";
|
|
286
|
+
// The rung per service, not only in the rollup: "12 partial" tells a reader
|
|
287
|
+
// the fleet is unfinished and nothing about which twelve, which is the one
|
|
288
|
+
// question the line is read to answer.
|
|
289
|
+
console.log(` ${serviceFlags(v)} ${s.id.padEnd(width)} ${v.maturity.padEnd(rungWidth)}${adrs}`.trimEnd());
|
|
290
|
+
}
|
|
291
|
+
// How much of the fleet anyone has actually vouched for. On 100+ services this
|
|
292
|
+
// is the number that says whether the docs can be trusted at all.
|
|
293
|
+
if (views.length > 0) {
|
|
294
|
+
const counted = new Map();
|
|
295
|
+
for (const v of views) {
|
|
296
|
+
const key = v.entry.status ?? "unmarked";
|
|
297
|
+
counted.set(key, (counted.get(key) ?? 0) + 1);
|
|
298
|
+
}
|
|
299
|
+
const parts = [...counted.entries()]
|
|
300
|
+
.sort((a, b) => compareIds(a[0], b[0]))
|
|
301
|
+
.map(([status, n]) => `${n} ${status}`);
|
|
302
|
+
console.log(` status: ${parts.join(" · ")}`);
|
|
303
|
+
// The campaign dial next to the trust dial: rungs in ladder order, so the
|
|
304
|
+
// line reads as progress left to right. Presence and provenance state only
|
|
305
|
+
// — completeness is unchecked, so this line never says "adopted".
|
|
306
|
+
const rollup = maturityRollup(views);
|
|
307
|
+
const rungs = MATURITY_LADDER.filter((m) => rollup[m] > 0).map((m) => `${rollup[m]} ${m}`);
|
|
308
|
+
console.log(` maturity: ${rungs.join(" · ")}`);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* The one-shot adoption worklist. Onboarding ten legacy services means asking
|
|
313
|
+
* "what is left" once a day for a month, and the answer used to require reading
|
|
314
|
+
* a presence table and a rollup and joining them by eye. A service whose
|
|
315
|
+
* provenance cannot be judged from here is LISTED, tagged, never dropped:
|
|
316
|
+
* omitting it would read as done.
|
|
317
|
+
*/
|
|
318
|
+
function printWorklist(views, total) {
|
|
319
|
+
if (views.length === 0) {
|
|
320
|
+
console.log(`nothing to do — all ${total} service(s) are vouched`);
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
console.log(`${views.length} of ${total} service(s) need work`);
|
|
324
|
+
const width = Math.max(0, ...views.map((v) => v.entry.id.length));
|
|
325
|
+
const rungWidth = Math.max(0, ...views.map((v) => v.maturity.length));
|
|
326
|
+
for (const v of views) {
|
|
327
|
+
const note = v.unverifiableFromHere ? " (provenance: unverifiable-from-here)" : "";
|
|
328
|
+
console.log(` ${v.entry.id.padEnd(width)} ${v.maturity.padEnd(rungWidth)} missing: ${v.missing.join(", ")}${note}`);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
/** Narrow verification cell: confirmed/claims when a record answers, one word when it does not. */
|
|
332
|
+
function verificationMark(v) {
|
|
333
|
+
if (v === null)
|
|
334
|
+
return "-";
|
|
335
|
+
if (v.state === "stale")
|
|
336
|
+
return "stale";
|
|
337
|
+
// `11/11` and `11/11 attested` are different facts — the first is a
|
|
338
|
+
// digest-matched green run, the second is somebody's word about one. Without
|
|
339
|
+
// the suffix a complete count is the same glyph either way, which is exactly
|
|
340
|
+
// the reading `verify`, `status` and the JSON envelope stopped giving.
|
|
341
|
+
return `${v.confirmed}/${v.claims}${v.verdict === "attested" ? " attested" : ""}`;
|
|
342
|
+
}
|
|
343
|
+
function printFeatures(features, verification) {
|
|
344
|
+
const active = features.filter((f) => !f.archived).length;
|
|
345
|
+
const archived = features.length - active;
|
|
346
|
+
const counts = `${active} active${archived > 0 ? `, ${archived} archived` : ""}`;
|
|
347
|
+
// Not `verified`: the column reports three verdicts and only one of them is
|
|
348
|
+
// that one. A heading that names the strong verdict makes every other cell
|
|
349
|
+
// read as it.
|
|
350
|
+
console.log(`features (${counts}) [I]ntent [D]elta verification`);
|
|
351
|
+
const width = Math.max(0, ...features.map((f) => f.id.length));
|
|
352
|
+
const cells = verification.map(verificationMark);
|
|
353
|
+
const cellWidth = Math.max(0, ...cells.map((c) => c.length));
|
|
354
|
+
for (const [i, f] of features.entries()) {
|
|
355
|
+
const flags = `${f.has.intent ? "I" : "-"} ${f.has.delta ? "D" : "-"}`;
|
|
356
|
+
const svcs = f.services.length > 0 ? f.services.join(", ") : "—";
|
|
357
|
+
const tag = f.archived ? " (archived)" : "";
|
|
358
|
+
console.log(` ${flags} ${f.id.padEnd(width)} ${cells[i].padEnd(cellWidth)} ${svcs}${tag}`);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
//# sourceMappingURL=list.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"list.js","sourceRoot":"","sources":["../../src/commands/list.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAC/C,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAC3E,OAAO,EAAE,QAAQ,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAC9D,OAAO,EACL,wBAAwB,EACxB,UAAU,EACV,aAAa,EACb,YAAY,EACZ,YAAY,EACZ,YAAY,GAGb,MAAM,iBAAiB,CAAC;AACzB,OAAO,EACL,gBAAgB,EAChB,gBAAgB,EAChB,WAAW,EACX,mBAAmB,GAEpB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,aAAa,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AAGnE,MAAM,QAAQ,GAAc,CAAC,UAAU,EAAE,UAAU,CAAC,CAAC;AAQrD,MAAM,UAAU,YAAY,CAAC,OAAgB;IAC3C,OAAO;SACJ,OAAO,CAAC,MAAM,CAAC;SACf,QAAQ,CAAC,WAAW,EAAE,qCAAqC,CAAC;SAC5D,WAAW,CAAC,iDAAiD,CAAC;SAC9D,MAAM,CAAC,QAAQ,EAAE,qDAAqD,CAAC;SACvE,MAAM,CAAC,YAAY,EAAE,2BAA2B,CAAC;SACjD,MAAM,CACL,cAAc,EACd,+FAA+F,CAChG;SACA,MAAM,CAAC,KAAK,EAAE,OAA2B,EAAE,IAAiB,EAAE,EAAE;QAC/D,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,KAAK,IAAI,CAAC;QAChC,MAAM,MAAM,GAAG,IAAI,CAAC,SAAS;YAC3B,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,UAAU,CAAC;YAC1C,CAAC,CAAC,OAAO;gBACP,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,OAAO,CAAC;gBACvC,CAAC,CAAC,QAAQ,CAAC;QACf,IAAI,OAAO,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,OAAkB,CAAC,EAAE,CAAC;YACtD,4EAA4E;YAC5E,IAAI,CAAC,IAAI,EAAE,gBAAgB,EAAE,oBAAoB,OAAO,gBAAgB,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;YACjG,OAAO;QACT,CAAC;QACD,IAAI,IAAI,CAAC,SAAS,IAAI,OAAO,KAAK,UAAU,EAAE,CAAC;YAC7C,IAAI,CAAC,IAAI,EAAE,gBAAgB,EAAE,6EAA6E,CAAC,CAAC;YAC5G,OAAO;QACT,CAAC;QAED,MAAM,MAAM,GAAG,MAAM,UAAU,EAAE,CAAC;QAClC,IAAI,CAAC,MAAM,EAAE,CAAC;YACZ,cAAc,CAAC,IAAI,CAAC,CAAC;YACrB,OAAO;QACT,CAAC;QACD,MAAM,EAAE,OAAO,EAAE,GAAG,MAAM,CAAC;QAC3B,2EAA2E;QAC3E,yEAAyE;QACzE,sEAAsE;QACtE,IAAI,CAAC,aAAa,CAAC,IAAI,EAAE,OAAO,EAAE,MAAM,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,MAAM,CAAC;YAAE,OAAO;QAE7F,IAAI,CAAC;YACH,MAAM,QAAQ,GAAG,MAAM,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,MAAM,YAAY,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;YACvF,MAAM,KAAK,GAAG,QAAQ,CAAC,CAAC,CAAC,MAAM,YAAY,CAAC,OAAO,EAAE,QAAQ,EAAE,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;YAC3F,MAAM,QAAQ,GAAG,MAAM,CAAC,QAAQ,CAAC,UAAU,CAAC;gBAC1C,CAAC,CAAC,MAAM,YAAY,CAAC,OAAO,EAAE,EAAE,eAAe,EAAE,IAAI,CAAC,QAAQ,EAAE,CAAC;gBACjE,CAAC,CAAC,SAAS,CAAC;YACd,MAAM,YAAY,GAAG,QAAQ;gBAC3B,CAAC,CAAC,MAAM,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,mBAAmB,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,CAAC;gBACzE,CAAC,CAAC,SAAS,CAAC;YAEd,MAAM,QAAQ,GAAG,KAAK,EAAE,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,KAAK,SAAS,CAAC,CAAC;YAEhE,IAAI,IAAI,EAAE,CAAC;gBACT,QAAQ,CAAC;oBACP,OAAO;oBACP,GAAG,CAAC,KAAK;wBACP,CAAC,CAAC;4BACE,QAAQ,EAAE,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,QAAS,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,WAAW,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC;4BAClF,QAAQ,EAAE,cAAc,CAAC,KAAK,CAAC;yBAChC;wBACH,CAAC,CAAC,EAAE,CAAC;oBACP,GAAG,CAAC,QAAQ;wBACV,CAAC,CAAC,EAAE,QAAQ,EAAE,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,WAAW,CAAC,OAAO,EAAE,CAAC,EAAE,YAAa,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,EAAE;wBACzF,CAAC,CAAC,EAAE,CAAC;iBACR,CAAC,CAAC;gBACH,OAAO;YACT,CAAC;YAED,IAAI,IAAI,CAAC,SAAS,EAAE,CAAC;gBACnB,aAAa,CAAC,QAAS,EAAE,KAAM,CAAC,MAAM,CAAC,CAAC;gBACxC,OAAO;YACT,CAAC;YACD,IAAI,KAAK;gBAAE,aAAa,CAAC,KAAK,CAAC,CAAC;YAChC,IAAI,KAAK,IAAI,QAAQ;gBAAE,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;YACvC,IAAI,QAAQ;gBAAE,aAAa,CAAC,QAAQ,EAAE,YAAa,CAAC,CAAC;QACvD,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,IAAI,GAAG,YAAY,wBAAwB,EAAE,CAAC;gBAC5C,mBAAmB,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;gBAC/B,OAAO;YACT,CAAC;YACD,qEAAqE;YACrE,wEAAwE;YACxE,wEAAwE;YACxE,uDAAuD;YACvD,MAAM,IAAI,GAAI,GAA6B,CAAC,IAAI,CAAC;YACjD,IAAI,IAAI,KAAK,SAAS;gBAAE,MAAM,GAAG,CAAC;YAClC,IAAI,CACF,IAAI,EACJ,wBAAwB,EACxB,GAAG,IAAI,kEACL,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CACjD,EAAE,CACH,CAAC;QACJ,CAAC;IACH,CAAC,CAAC,CAAC;AACP,CAAC;AA6BD;;;;;;GAMG;AACH,KAAK,UAAU,YAAY,CACzB,OAAe,EACf,QAAwB,EACxB,YAAgC;IAEhC,MAAM,KAAK,GAAG,IAAI,GAAG,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IACjD,MAAM,EAAE,GAAG,aAAa,CAAC,OAAO,CAAC,CAAC;IAClC,MAAM,IAAI,GAAG,UAAU,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,MAAM,QAAQ,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IACxD,MAAM,MAAM,GAAG,IAAI,GAAG,EAAU,CAAC;IACjC,4EAA4E;IAC5E,2EAA2E;IAC3E,wEAAwE;IACxE,gDAAgD;IAChD,MAAM,MAAM,GAAG,IAAI,KAAK,IAAI,IAAI,IAAI,CAAC,MAAM,CAAC,MAAM,KAAK,CAAC,CAAC;IACzD,IAAI,MAAM,EAAE,CAAC;QACX,MAAM,KAAK,GAAG,eAAe,CAAC,IAAI,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC;QACpD,KAAK,MAAM,CAAC,IAAI,IAAI,CAAC,aAAa,EAAE,CAAC;YACnC,IAAI,CAAC,CAAC,EAAE,KAAK,SAAS;gBAAE,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC;QACtD,CAAC;IACH,CAAC;IAED,OAAO,QAAQ,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE;QAC5B,MAAM,QAAQ,GAAG,UAAU,CAAC,YAAY,CAAC,OAAO,EAAE,KAAK,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC;QACtE,MAAM,WAAW,GAAG,CAAC,MAAM,IAAI,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;QACpD,MAAM,IAAI,GAAG;YACX,KAAK;YACL,QAAQ;YACR,WAAW;YACX,oBAAoB,EAAE,KAAK,CAAC,OAAO,CAAC,QAAQ,IAAI,YAAY,KAAK,KAAK,CAAC,EAAE;SAC1E,CAAC;QACF,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC;QAC3B,OAAO,EAAE,GAAG,IAAI,EAAE,QAAQ,EAAE,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,CAAC;IACxD,CAAC,CAAC,CAAC;AACL,CAAC;AAED,wEAAwE;AACxE,yEAAyE;AACzE,wEAAwE;AAExE,SAAS,WAAW,CAAC,OAAe,EAAE,CAAc;IAClD,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC;IAClB,OAAO;QACL,EAAE,EAAE,CAAC,CAAC,EAAE;QACR,IAAI,EAAE,QAAQ,CAAC,OAAO,EAAE,CAAC,CAAC,GAAG,CAAC;QAC9B,GAAG,EAAE,EAAE,GAAG,CAAC,CAAC,GAAG,EAAE,QAAQ,EAAE,CAAC,CAAC,QAAQ,EAAE;QACvC,IAAI,EAAE,CAAC,CAAC,IAAI;QACZ,MAAM,EAAE,CAAC,CAAC,MAAM;QAChB,QAAQ,EAAE,CAAC,CAAC,QAAQ;QACpB,OAAO,EAAE,CAAC,CAAC,OAAO;QAClB,WAAW,EAAE,CAAC,CAAC,WAAW;QAC1B,qEAAqE;QACrE,yEAAyE;QACzE,kDAAkD;QAClD,GAAG,CAAC,CAAC,CAAC,oBAAoB,CAAC,CAAC,CAAC,EAAE,UAAU,EAAE,wBAAwB,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KAC5E,CAAC;AACJ,CAAC;AAED,SAAS,WAAW,CAClB,OAAe,EACf,CAAe,EACf,YAAqC;IAErC,OAAO;QACL,EAAE,EAAE,CAAC,CAAC,EAAE;QACR,OAAO,EAAE,CAAC,CAAC,OAAO;QAClB,IAAI,EAAE,QAAQ,CAAC,OAAO,EAAE,CAAC,CAAC,GAAG,CAAC;QAC9B,QAAQ,EAAE,CAAC,CAAC,QAAQ;QACpB,QAAQ,EAAE,CAAC,CAAC,QAAQ;QACpB,GAAG,EAAE,CAAC,CAAC,GAAG;QACV,YAAY;KACb,CAAC;AACJ,CAAC;AAED,wEAAwE;AACxE,yEAAyE;AACzE,wEAAwE;AAExE,uFAAuF;AACvF,MAAM,eAAe,GAAG,CAAC,OAAO,EAAE,SAAS,EAAE,YAAY,EAAE,SAAS,EAAE,SAAS,CAAU,CAAC;AAG1F;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,SAAS,QAAQ,CAAC,CAA4C;IAC5D,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC;IAClB,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,QAAQ,IAAI,CAAC,CAAC,IAAI,KAAK,CAAC;QAAE,OAAO,OAAO,CAAC;IACvF,IAAI,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,IAAI,CAAC,CAAC,GAAG,CAAC,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC,WAAW,IAAI,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;QAAE,OAAO,SAAS,CAAC;IACxF,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,QAAQ;QAAE,OAAO,YAAY,CAAC;IAC7C,IAAI,CAAC,CAAC,CAAC,CAAC,MAAM,KAAK,UAAU,IAAI,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC;QAAE,OAAO,SAAS,CAAC;IACtE,OAAO,SAAS,CAAC;AACnB,CAAC;AAED;;;;GAIG;AACH,SAAS,IAAI,CAAC,CAA4C;IACxD,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC;IAClB,MAAM,KAAK,GAAG;QACZ,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,cAAc,CAAC,CAAC;QACxC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC;QAClC,GAAG,CAAC,CAAC,CAAC,WAAW,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;KAC7D,CAAC;IACF,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,KAAK,CAAC;IACnC,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,QAAQ;QAAE,OAAO,CAAC,qCAAqC,CAAC,CAAC;IACxE,IAAI,CAAC,CAAC,CAAC,CAAC,MAAM,KAAK,UAAU,IAAI,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC;QAAE,OAAO,CAAC,0BAA0B,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC;IACjG,OAAO,EAAE,CAAC;AACZ,CAAC;AAED,uFAAuF;AACvF,SAAS,cAAc,CAAC,KAAoB;IAC1C,MAAM,GAAG,GAAG,MAAM,CAAC,WAAW,CAAC,eAAe,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAA6B,CAAC;IAC/F,KAAK,MAAM,CAAC,IAAI,KAAK;QAAE,GAAG,CAAC,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;IAC5C,OAAO,GAAG,CAAC;AACb,CAAC;AAyBD;;;;;;;;;;GAUG;AACH,KAAK,UAAU,mBAAmB,CAChC,OAAe,EACf,CAAe;IAEf,MAAM,CAAC,GAAG,MAAM,gBAAgB,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;IACxC,IAAI,CAAC,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IAC5B,MAAM,KAAK,GAAG,CAAC,CAAC,QAAQ;QACtB,CAAC,CAAC,KAAK;QACP,CAAC,CAAC,CAAC,MAAM,gBAAgB,CAAC,OAAO,EAAE,CAAC,CAAC,GAAG,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,KAAK,CAAC,CAAC,SAAS,CAAC;IAC1E,MAAM,KAAK,GAAG,WAAW,CAAC,CAAC,CAAC,CAAC;IAC7B,OAAO;QACL,KAAK,EAAE,KAAK,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,UAAU;QACnC,QAAQ,EAAE,CAAC,CAAC,QAAQ;QACpB,OAAO,EAAE,mBAAmB,CAAC,KAAK,EAAE,KAAK,CAAC;QAC1C,SAAS,EAAE,KAAK,CAAC,SAAS;QAC1B,MAAM,EAAE,KAAK,CAAC,MAAM;QACpB,QAAQ,EAAE,KAAK,CAAC,QAAQ;KACzB,CAAC;AACJ,CAAC;AAED,wEAAwE;AACxE,yEAAyE;AACzE,wEAAwE;AAExE;;;;GAIG;AACH,SAAS,YAAY,CAAC,CAAc;IAClC,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC;IAClB,OAAO;QACL,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG;QACvB,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG;QACtB,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG;QACtB,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG;QACzB,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG;QACzB,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG;KACzB,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AACd,CAAC;AAED,SAAS,aAAa,CAAC,KAAoB;IACzC,OAAO,CAAC,GAAG,CACT,aAAa,KAAK,CAAC,MAAM,wDAAwD,CAClF,CAAC;IACF,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC;IAClE,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC;IACtE,KAAK,MAAM,CAAC,IAAI,KAAK,EAAE,CAAC;QACtB,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC;QAClB,MAAM,IAAI,GAAG,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,IAAI,OAAO,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;QAC7E,4EAA4E;QAC5E,2EAA2E;QAC3E,uCAAuC;QACvC,OAAO,CAAC,GAAG,CACT,KAAK,YAAY,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,SAAS,CAAC,GAAG,IAAI,EAAE,CAAC,OAAO,EAAE,CAChG,CAAC;IACJ,CAAC;IACD,+EAA+E;IAC/E,kEAAkE;IAClE,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACrB,MAAM,OAAO,GAAG,IAAI,GAAG,EAAkB,CAAC;QAC1C,KAAK,MAAM,CAAC,IAAI,KAAK,EAAE,CAAC;YACtB,MAAM,GAAG,GAAG,CAAC,CAAC,KAAK,CAAC,MAAM,IAAI,UAAU,CAAC;YACzC,OAAO,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;QAChD,CAAC;QACD,MAAM,KAAK,GAAG,CAAC,GAAG,OAAO,CAAC,OAAO,EAAE,CAAC;aACjC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;aACtC,GAAG,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,IAAI,MAAM,EAAE,CAAC,CAAC;QAC1C,OAAO,CAAC,GAAG,CAAC,aAAa,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;QAC9C,0EAA0E;QAC1E,2EAA2E;QAC3E,kEAAkE;QAClE,MAAM,MAAM,GAAG,cAAc,CAAC,KAAK,CAAC,CAAC;QACrC,MAAM,KAAK,GAAG,eAAe,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QAC3F,OAAO,CAAC,GAAG,CAAC,eAAe,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;IAClD,CAAC;AACH,CAAC;AAED;;;;;;GAMG;AACH,SAAS,aAAa,CAAC,KAAoB,EAAE,KAAa;IACxD,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACvB,OAAO,CAAC,GAAG,CAAC,uBAAuB,KAAK,yBAAyB,CAAC,CAAC;QACnE,OAAO;IACT,CAAC;IACD,OAAO,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC,MAAM,OAAO,KAAK,uBAAuB,CAAC,CAAC;IAChE,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC;IAClE,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC;IACtE,KAAK,MAAM,CAAC,IAAI,KAAK,EAAE,CAAC;QACtB,MAAM,IAAI,GAAG,CAAC,CAAC,oBAAoB,CAAC,CAAC,CAAC,wCAAwC,CAAC,CAAC,CAAC,EAAE,CAAC;QACpF,OAAO,CAAC,GAAG,CACT,KAAK,CAAC,CAAC,KAAK,CAAC,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,SAAS,CAAC,cAAc,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,IAAI,EAAE,CAC1G,CAAC;IACJ,CAAC;AACH,CAAC;AAED,mGAAmG;AACnG,SAAS,gBAAgB,CAAC,CAA0B;IAClD,IAAI,CAAC,KAAK,IAAI;QAAE,OAAO,GAAG,CAAC;IAC3B,IAAI,CAAC,CAAC,KAAK,KAAK,OAAO;QAAE,OAAO,OAAO,CAAC;IACxC,oEAAoE;IACpE,6EAA6E;IAC7E,6EAA6E;IAC7E,uEAAuE;IACvE,OAAO,GAAG,CAAC,CAAC,SAAS,IAAI,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,OAAO,KAAK,UAAU,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC;AACpF,CAAC;AAED,SAAS,aAAa,CAAC,QAAwB,EAAE,YAAyC;IACxF,MAAM,MAAM,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC;IAC1D,MAAM,QAAQ,GAAG,QAAQ,CAAC,MAAM,GAAG,MAAM,CAAC;IAC1C,MAAM,MAAM,GAAG,GAAG,MAAM,UAAU,QAAQ,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,QAAQ,WAAW,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC;IACjF,4EAA4E;IAC5E,2EAA2E;IAC3E,cAAc;IACd,OAAO,CAAC,GAAG,CAAC,aAAa,MAAM,mCAAmC,CAAC,CAAC;IACpE,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC;IAC/D,MAAM,KAAK,GAAG,YAAY,CAAC,GAAG,CAAC,gBAAgB,CAAC,CAAC;IACjD,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC;IAC7D,KAAK,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,QAAQ,CAAC,OAAO,EAAE,EAAE,CAAC;QACxC,MAAM,KAAK,GAAG,GAAG,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC;QACvE,MAAM,IAAI,GAAG,CAAC,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC;QACjE,MAAM,GAAG,GAAG,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,cAAc,CAAC,CAAC,CAAC,EAAE,CAAC;QAC7C,OAAO,CAAC,GAAG,CAAC,KAAK,KAAK,KAAK,CAAC,CAAC,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,KAAK,CAAC,CAAC,CAAE,CAAC,MAAM,CAAC,SAAS,CAAC,KAAK,IAAI,GAAG,GAAG,EAAE,CAAC,CAAC;IAClG,CAAC;AACH,CAAC","sourcesContent":["import type { Command } from \"commander\";\nimport { existsSync } from \"node:fs\";\nimport { loadConfig } from \"../core/config.js\";\nimport { emitJson, fail, repoPath, reportNoConfig } from \"../core/json.js\";\nimport { loadFile, serviceResolver } from \"../core/likec4.js\";\nimport {\n DocsRepoUnavailableError,\n compareIds,\n landscapePath,\n listFeatures,\n listServices,\n servicePaths,\n type FeatureEntry,\n type ServiceEntry,\n} from \"../core/repo.js\";\nimport {\n featureChecklist,\n readVerification,\n tallyRecord,\n verificationVerdict,\n type VerificationVerdict,\n} from \"../core/verify.js\";\nimport { docsRepoReady, reportDocsRepoError } from \"./validate.js\";\n\ntype Section = \"services\" | \"features\";\nconst SECTIONS: Section[] = [\"services\", \"features\"];\n\ninterface ListOptions {\n json?: boolean;\n archived?: boolean;\n needsWork?: boolean;\n}\n\nexport function registerList(program: Command): void {\n program\n .command(\"list\")\n .argument(\"[section]\", \"services | features (default: both)\")\n .description(\"List the services and features in the docs repo\")\n .option(\"--json\", \"emit the machine contract instead of the human view\")\n .option(\"--archived\", \"include archived features\")\n .option(\n \"--needs-work\",\n \"services only: the adoption worklist — every service below `vouched`, with what it is missing\",\n )\n .action(async (section: string | undefined, opts: ListOptions) => {\n const json = opts.json === true;\n const wanted = opts.needsWork\n ? SECTIONS.filter((s) => s === \"services\")\n : section\n ? SECTIONS.filter((s) => s === section)\n : SECTIONS;\n if (section && !SECTIONS.includes(section as Section)) {\n // `invalid-option`, same as show's bad --type: one mistake class, one code.\n fail(json, \"invalid-option\", `Unknown section '${section}'. Expected: ${SECTIONS.join(\" | \")}.`);\n return;\n }\n if (opts.needsWork && section === \"features\") {\n fail(json, \"invalid-option\", \"--needs-work is the service adoption worklist; drop the 'features' section.\");\n return;\n }\n\n const config = await loadConfig();\n if (!config) {\n reportNoConfig(json);\n return;\n }\n const { docsDir } = config;\n // A docsDir that is not a docs repo is refused, never rendered as an empty\n // fleet — the whole point of the gate (see validate.ts's docsRepoReady).\n // `list features` alone does not need services/, so it asks for less.\n if (!docsRepoReady(json, docsDir, wanted.includes(\"services\") ? \"services\" : \"docs\")) return;\n\n try {\n const services = wanted.includes(\"services\") ? await listServices(docsDir) : undefined;\n const views = services ? await serviceViews(docsDir, services, config.service) : undefined;\n const features = wanted.includes(\"features\")\n ? await listFeatures(docsDir, { includeArchived: opts.archived })\n : undefined;\n const verification = features\n ? await Promise.all(features.map((f) => featureVerification(docsDir, f)))\n : undefined;\n\n const worklist = views?.filter((v) => v.maturity !== \"vouched\");\n\n if (json) {\n emitJson({\n docsDir,\n ...(views\n ? {\n services: (opts.needsWork ? worklist! : views).map((v) => serviceJson(docsDir, v)),\n maturity: maturityRollup(views),\n }\n : {}),\n ...(features\n ? { features: features.map((f, i) => featureJson(docsDir, f, verification![i] ?? null)) }\n : {}),\n });\n return;\n }\n\n if (opts.needsWork) {\n printWorklist(worklist!, views!.length);\n return;\n }\n if (views) printServices(views);\n if (views && features) console.log(\"\");\n if (features) printFeatures(features, verification!);\n } catch (err) {\n if (err instanceof DocsRepoUnavailableError) {\n reportDocsRepoError(json, err);\n return;\n }\n // One unreadable file used to escape as a stack trace (`internal` in\n // --json), naming nothing. The enumeration is all-or-nothing — it reads\n // every service's frontmatter to build one table — so the honest answer\n // is a refusal that says WHICH path could not be read.\n const path = (err as NodeJS.ErrnoException).path;\n if (path === undefined) throw err;\n fail(\n json,\n \"repository-unavailable\",\n `${path} could not be read, so the listing would be missing a service. ${\n err instanceof Error ? err.message : String(err)\n }`,\n );\n }\n });\n}\n\n/* ------------------------------------------------------------------ */\n/* The per-service view */\n/* ------------------------------------------------------------------ */\n\n/**\n * A service entry plus the two things the enumeration cannot know on its own:\n * whether an API is EXPECTED of it, and whether anyone standing here could\n * check its `sources`. Both change what the maturity rung means, and both are\n * fleet-level facts — they need the landscape and loam.json, not the directory.\n */\ninterface ServiceView {\n entry: ServiceEntry;\n /** `arch.spec.md` — the architecture-obligations axis, absent from ServiceEntry.has. */\n archSpec: boolean;\n /**\n * Does anything in the fleet call an operation on this service? True unless\n * the landscape PROVES otherwise, exactly as `validate`'s no-openapi grace\n * reads it: a missing or unparseable landscape proves nothing.\n */\n apiExpected: boolean;\n /** True when this service's `sources` can only be resolved from its own repo. */\n unverifiableFromHere: boolean;\n maturity: Maturity;\n /** What stands between this service and the next rung, in fix order. */\n missing: string[];\n}\n\n/**\n * Read the living landscape ONCE and decide, per service, whether an API is\n * expected of it. `serviceResolver` is handed the real service ids so an edge\n * drawn into a modelled container (`paymentService.api`) counts for the service\n * that owns it — the same resolution `validate` uses, so the two commands\n * cannot disagree about who is called.\n */\nasync function serviceViews(\n docsDir: string,\n services: ServiceEntry[],\n boundService: string | undefined,\n): Promise<ServiceView[]> {\n const known = new Set(services.map((s) => s.id));\n const lp = landscapePath(docsDir);\n const land = existsSync(lp) ? await loadFile(lp) : null;\n const called = new Set<string>();\n // Positive evidence only: with no landscape, or one that does not parse, we\n // know nothing about who is called and every service is assumed to have an\n // API — the conservative direction, because claiming a service needs no\n // contract is how a real one goes undocumented.\n const proven = land !== null && land.errors.length === 0;\n if (proven) {\n const svcOf = serviceResolver(land.elements, known);\n for (const r of land.relationships) {\n if (r.op !== undefined) called.add(svcOf(r.target));\n }\n }\n\n return services.map((entry) => {\n const archSpec = existsSync(servicePaths(docsDir, entry.id).archSpec);\n const apiExpected = !proven || called.has(entry.id);\n const view = {\n entry,\n archSpec,\n apiExpected,\n unverifiableFromHere: entry.sources.declared && boundService !== entry.id,\n };\n const missing = gaps(view);\n return { ...view, maturity: maturity(view), missing };\n });\n}\n\n/* ------------------------------------------------------------------ */\n/* JSON */\n/* ------------------------------------------------------------------ */\n\nfunction serviceJson(docsDir: string, v: ServiceView): Record<string, unknown> {\n const s = v.entry;\n return {\n id: s.id,\n path: repoPath(docsDir, s.dir),\n has: { ...s.has, archSpec: v.archSpec },\n adrs: s.adrs,\n status: s.status,\n maturity: v.maturity,\n missing: v.missing,\n apiExpected: v.apiExpected,\n // Never omitted when true: a consumer that filters this table into a\n // worklist has to be able to tell \"checked and fine\" from \"not checkable\n // from here\", and absence reads as the first one.\n ...(v.unverifiableFromHere ? { provenance: \"unverifiable-from-here\" } : {}),\n };\n}\n\nfunction featureJson(\n docsDir: string,\n f: FeatureEntry,\n verification: VerificationCell | null,\n): Record<string, unknown> {\n return {\n id: f.id,\n dirName: f.dirName,\n path: repoPath(docsDir, f.dir),\n archived: f.archived,\n services: f.services,\n has: f.has,\n verification,\n };\n}\n\n/* ------------------------------------------------------------------ */\n/* Maturity — the adoption campaign's dial */\n/* ------------------------------------------------------------------ */\n\n/** The adoption-maturity ladder, in order. Each rung stands on every rung below it. */\nconst MATURITY_LADDER = [\"empty\", \"partial\", \"documented\", \"sourced\", \"vouched\"] as const;\ntype Maturity = (typeof MATURITY_LADDER)[number];\n\n/**\n * One monotone word for how far a service's documentation has got. Derived\n * from artifact PRESENCE and provenance state only — the data list already\n * holds — never from what the artifacts say. COMPLETENESS of adopted docs is\n * explicitly on the unchecked list (brief.ts): a service with one endpoint\n * documented out of thirty climbs this ladder exactly as fast as a thorough\n * one, which is why no rung is called \"adopted\".\n *\n * empty services/<id>/ exists, no artifact is in it\n * partial some artifacts, but not the required set\n * documented the artifacts the adopt brief marks required are present\n * sourced the living spec declares `sources` — something ties it to code\n * vouched status: verified with a sources_digest behind it — a person\n * stamped it. `verified` with no digest is a claim with nothing\n * behind it and stays below this rung.\n *\n * `openapi.yaml` is required ONLY where an API is expected. The rung used to\n * demand it of everything, which permanently pinned every worker, cron and\n * consumer in the fleet at `partial` — services that are fully documented and\n * vouched for, reported as unfinished forever, and a rollup that therefore said\n * a correctly-adopted fleet was half-done. The evidence is the same one\n * `validate` uses to keep `service.no-openapi` quiet, so a service cannot be\n * green in one command and unfinished in the other.\n */\nfunction maturity(v: Omit<ServiceView, \"maturity\" | \"missing\">): Maturity {\n const s = v.entry;\n if (!Object.values(s.has).some(Boolean) && !v.archSpec && s.adrs === 0) return \"empty\";\n if (!(s.has.model && s.has.spec && (!v.apiExpected || s.has.openapi))) return \"partial\";\n if (!s.sources.declared) return \"documented\";\n if (!(s.status === \"verified\" && s.sources.stamped)) return \"sourced\";\n return \"vouched\";\n}\n\n/**\n * What stands between this service and `vouched`, named as the thing to go and\n * do. One rung's worth at a time: telling somebody at `empty` that they also\n * need a vouch is noise, and the vouch is not even possible yet.\n */\nfunction gaps(v: Omit<ServiceView, \"maturity\" | \"missing\">): string[] {\n const s = v.entry;\n const files = [\n ...(s.has.model ? [] : [\"model.likec4\"]),\n ...(s.has.spec ? [] : [\"spec.md\"]),\n ...(v.apiExpected && !s.has.openapi ? [\"openapi.yaml\"] : []),\n ];\n if (files.length > 0) return files;\n if (!s.sources.declared) return [\"sources: in the spec.md frontmatter\"];\n if (!(s.status === \"verified\" && s.sources.stamped)) return [`\\`loam vouch --service ${s.id}\\``];\n return [];\n}\n\n/** Counts per rung, every rung present — a stable shape a fleet dashboard can diff. */\nfunction maturityRollup(views: ServiceView[]): Record<Maturity, number> {\n const out = Object.fromEntries(MATURITY_LADDER.map((m) => [m, 0])) as Record<Maturity, number>;\n for (const v of views) out[v.maturity] += 1;\n return out;\n}\n\n/* ------------------------------------------------------------------ */\n/* Verification */\n/* ------------------------------------------------------------------ */\n\n/** What the features table says about verification without N `loam verify` runs. */\ninterface VerificationCell {\n state: \"recorded\" | \"stale\";\n /** The day the record was written. */\n recorded: string;\n /**\n * `verify --json`'s own three-valued verdict, through `verificationVerdict` —\n * not a fourth opinion computed here. `attested` is the one this column got\n * wrong: a scenario confirmed on an agent's word printed as `11/11` under a\n * heading that said `verified`, and `list --json` is how the fleet is graded.\n */\n verdict: VerificationVerdict;\n /** Both counts are `tallyRecord`'s — the record's `claims[]`, never its `summary:` block. */\n confirmed: number;\n claims: number;\n /** Of the confirmed, the scenario claims on an agent's word (`attestedClaims`). */\n attested: number;\n}\n\n/**\n * The verification column. `readVerification` is one file read; the checklist\n * is derived only when there is a record to judge — on a fleet-sized repo most\n * features have none, and deriving N checklists to print N dashes is the cost\n * that would get this column dropped.\n *\n * An archived feature's record is frozen history (see verify.ts): archive\n * merged the feature's operations into the living openapi, so a re-derived\n * checklist can only mismatch — the record is reported as recorded, never\n * judged stale.\n */\nasync function featureVerification(\n docsDir: string,\n f: FeatureEntry,\n): Promise<VerificationCell | null> {\n const v = await readVerification(f.dir);\n if (v === null) return null;\n const stale = f.archived\n ? false\n : (await featureChecklist(docsDir, f.dir, f.id)).digest !== v.checklist;\n const tally = tallyRecord(v);\n return {\n state: stale ? \"stale\" : \"recorded\",\n recorded: v.recorded,\n verdict: verificationVerdict(tally, stale),\n confirmed: tally.confirmed,\n claims: tally.claims,\n attested: tally.attested,\n };\n}\n\n/* ------------------------------------------------------------------ */\n/* Text */\n/* ------------------------------------------------------------------ */\n\n/**\n * Fixed-width presence flags. `a` and `A` are deliberately the same letter in\n * two cases: the arch spec and the API are the two things a reader most often\n * mistakes for each other, and one is lowercase because it is optional.\n */\nfunction serviceFlags(v: ServiceView): string {\n const s = v.entry;\n return [\n s.has.model ? \"M\" : \"-\",\n s.has.spec ? \"S\" : \"-\",\n v.archSpec ? \"a\" : \"-\",\n s.has.openapi ? \"A\" : \"-\",\n s.has.runbook ? \"R\" : \"-\",\n s.has.health ? \"H\" : \"-\",\n ].join(\" \");\n}\n\nfunction printServices(views: ServiceView[]): void {\n console.log(\n `services (${views.length}) [M]odel [S]pec [a]rch-spec [A]pi [R]unbook [H]ealth`,\n );\n const width = Math.max(0, ...views.map((v) => v.entry.id.length));\n const rungWidth = Math.max(0, ...views.map((v) => v.maturity.length));\n for (const v of views) {\n const s = v.entry;\n const adrs = s.adrs > 0 ? ` (${s.adrs} adr${s.adrs === 1 ? \"\" : \"s\"})` : \"\";\n // The rung per service, not only in the rollup: \"12 partial\" tells a reader\n // the fleet is unfinished and nothing about which twelve, which is the one\n // question the line is read to answer.\n console.log(\n ` ${serviceFlags(v)} ${s.id.padEnd(width)} ${v.maturity.padEnd(rungWidth)}${adrs}`.trimEnd(),\n );\n }\n // How much of the fleet anyone has actually vouched for. On 100+ services this\n // is the number that says whether the docs can be trusted at all.\n if (views.length > 0) {\n const counted = new Map<string, number>();\n for (const v of views) {\n const key = v.entry.status ?? \"unmarked\";\n counted.set(key, (counted.get(key) ?? 0) + 1);\n }\n const parts = [...counted.entries()]\n .sort((a, b) => compareIds(a[0], b[0]))\n .map(([status, n]) => `${n} ${status}`);\n console.log(` status: ${parts.join(\" · \")}`);\n // The campaign dial next to the trust dial: rungs in ladder order, so the\n // line reads as progress left to right. Presence and provenance state only\n // — completeness is unchecked, so this line never says \"adopted\".\n const rollup = maturityRollup(views);\n const rungs = MATURITY_LADDER.filter((m) => rollup[m] > 0).map((m) => `${rollup[m]} ${m}`);\n console.log(` maturity: ${rungs.join(\" · \")}`);\n }\n}\n\n/**\n * The one-shot adoption worklist. Onboarding ten legacy services means asking\n * \"what is left\" once a day for a month, and the answer used to require reading\n * a presence table and a rollup and joining them by eye. A service whose\n * provenance cannot be judged from here is LISTED, tagged, never dropped:\n * omitting it would read as done.\n */\nfunction printWorklist(views: ServiceView[], total: number): void {\n if (views.length === 0) {\n console.log(`nothing to do — all ${total} service(s) are vouched`);\n return;\n }\n console.log(`${views.length} of ${total} service(s) need work`);\n const width = Math.max(0, ...views.map((v) => v.entry.id.length));\n const rungWidth = Math.max(0, ...views.map((v) => v.maturity.length));\n for (const v of views) {\n const note = v.unverifiableFromHere ? \" (provenance: unverifiable-from-here)\" : \"\";\n console.log(\n ` ${v.entry.id.padEnd(width)} ${v.maturity.padEnd(rungWidth)} missing: ${v.missing.join(\", \")}${note}`,\n );\n }\n}\n\n/** Narrow verification cell: confirmed/claims when a record answers, one word when it does not. */\nfunction verificationMark(v: VerificationCell | null): string {\n if (v === null) return \"-\";\n if (v.state === \"stale\") return \"stale\";\n // `11/11` and `11/11 attested` are different facts — the first is a\n // digest-matched green run, the second is somebody's word about one. Without\n // the suffix a complete count is the same glyph either way, which is exactly\n // the reading `verify`, `status` and the JSON envelope stopped giving.\n return `${v.confirmed}/${v.claims}${v.verdict === \"attested\" ? \" attested\" : \"\"}`;\n}\n\nfunction printFeatures(features: FeatureEntry[], verification: (VerificationCell | null)[]): void {\n const active = features.filter((f) => !f.archived).length;\n const archived = features.length - active;\n const counts = `${active} active${archived > 0 ? `, ${archived} archived` : \"\"}`;\n // Not `verified`: the column reports three verdicts and only one of them is\n // that one. A heading that names the strong verdict makes every other cell\n // read as it.\n console.log(`features (${counts}) [I]ntent [D]elta verification`);\n const width = Math.max(0, ...features.map((f) => f.id.length));\n const cells = verification.map(verificationMark);\n const cellWidth = Math.max(0, ...cells.map((c) => c.length));\n for (const [i, f] of features.entries()) {\n const flags = `${f.has.intent ? \"I\" : \"-\"} ${f.has.delta ? \"D\" : \"-\"}`;\n const svcs = f.services.length > 0 ? f.services.join(\", \") : \"—\";\n const tag = f.archived ? \" (archived)\" : \"\";\n console.log(` ${flags} ${f.id.padEnd(width)} ${cells[i]!.padEnd(cellWidth)} ${svcs}${tag}`);\n }\n}\n"]}
|