@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.
Files changed (106) hide show
  1. package/CHANGELOG.md +89 -0
  2. package/COMPARISON.md +63 -0
  3. package/LICENSE +21 -0
  4. package/MIGRATING-from-OpenSpec.md +202 -0
  5. package/README.md +241 -9
  6. package/SCHEMA.md +425 -0
  7. package/SECURITY.md +9 -0
  8. package/dist/cli.js +138 -0
  9. package/dist/cli.js.map +1 -0
  10. package/dist/commands/adopt.js +226 -0
  11. package/dist/commands/adopt.js.map +1 -0
  12. package/dist/commands/archive.js +1297 -0
  13. package/dist/commands/archive.js.map +1 -0
  14. package/dist/commands/delta.js +361 -0
  15. package/dist/commands/delta.js.map +1 -0
  16. package/dist/commands/dependencies.js +78 -0
  17. package/dist/commands/dependencies.js.map +1 -0
  18. package/dist/commands/doctor.js +85 -0
  19. package/dist/commands/doctor.js.map +1 -0
  20. package/dist/commands/gherkin.js +270 -0
  21. package/dist/commands/gherkin.js.map +1 -0
  22. package/dist/commands/init.js +250 -0
  23. package/dist/commands/init.js.map +1 -0
  24. package/dist/commands/list.js +361 -0
  25. package/dist/commands/list.js.map +1 -0
  26. package/dist/commands/migrate-openspec.js +933 -0
  27. package/dist/commands/migrate-openspec.js.map +1 -0
  28. package/dist/commands/new.js +412 -0
  29. package/dist/commands/new.js.map +1 -0
  30. package/dist/commands/rebase.js +358 -0
  31. package/dist/commands/rebase.js.map +1 -0
  32. package/dist/commands/show.js +325 -0
  33. package/dist/commands/show.js.map +1 -0
  34. package/dist/commands/status.js +270 -0
  35. package/dist/commands/status.js.map +1 -0
  36. package/dist/commands/unarchive.js +310 -0
  37. package/dist/commands/unarchive.js.map +1 -0
  38. package/dist/commands/validate.js +1609 -0
  39. package/dist/commands/validate.js.map +1 -0
  40. package/dist/commands/verify.js +759 -0
  41. package/dist/commands/verify.js.map +1 -0
  42. package/dist/commands/vouch.js +325 -0
  43. package/dist/commands/vouch.js.map +1 -0
  44. package/dist/core/agent.js +1823 -0
  45. package/dist/core/agent.js.map +1 -0
  46. package/dist/core/agents-stamp.js +68 -0
  47. package/dist/core/agents-stamp.js.map +1 -0
  48. package/dist/core/arch.js +104 -0
  49. package/dist/core/arch.js.map +1 -0
  50. package/dist/core/brief.js +733 -0
  51. package/dist/core/brief.js.map +1 -0
  52. package/dist/core/coherence.js +553 -0
  53. package/dist/core/coherence.js.map +1 -0
  54. package/dist/core/config.js +172 -0
  55. package/dist/core/config.js.map +1 -0
  56. package/dist/core/delta.js +457 -0
  57. package/dist/core/delta.js.map +1 -0
  58. package/dist/core/dependencies.js +352 -0
  59. package/dist/core/dependencies.js.map +1 -0
  60. package/dist/core/docs.js +143 -0
  61. package/dist/core/docs.js.map +1 -0
  62. package/dist/core/doctor.js +544 -0
  63. package/dist/core/doctor.js.map +1 -0
  64. package/dist/core/fleet-context.js +272 -0
  65. package/dist/core/fleet-context.js.map +1 -0
  66. package/dist/core/frontmatter.js +203 -0
  67. package/dist/core/frontmatter.js.map +1 -0
  68. package/dist/core/gherkin.js +521 -0
  69. package/dist/core/gherkin.js.map +1 -0
  70. package/dist/core/health.js +71 -0
  71. package/dist/core/health.js.map +1 -0
  72. package/dist/core/ids.js +93 -0
  73. package/dist/core/ids.js.map +1 -0
  74. package/dist/core/issue.js +9 -0
  75. package/dist/core/issue.js.map +1 -0
  76. package/dist/core/json.js +67 -0
  77. package/dist/core/json.js.map +1 -0
  78. package/dist/core/likec4.js +474 -0
  79. package/dist/core/likec4.js.map +1 -0
  80. package/dist/core/openapi-merge.js +519 -0
  81. package/dist/core/openapi-merge.js.map +1 -0
  82. package/dist/core/openapi.js +243 -0
  83. package/dist/core/openapi.js.map +1 -0
  84. package/dist/core/openspec-inventory.js +1403 -0
  85. package/dist/core/openspec-inventory.js.map +1 -0
  86. package/dist/core/path-safety.js +109 -0
  87. package/dist/core/path-safety.js.map +1 -0
  88. package/dist/core/provenance.js +695 -0
  89. package/dist/core/provenance.js.map +1 -0
  90. package/dist/core/repo.js +362 -0
  91. package/dist/core/repo.js.map +1 -0
  92. package/dist/core/report.js +38 -0
  93. package/dist/core/report.js.map +1 -0
  94. package/dist/core/results.js +201 -0
  95. package/dist/core/results.js.map +1 -0
  96. package/dist/core/spec.js +481 -0
  97. package/dist/core/spec.js.map +1 -0
  98. package/dist/core/staging.js +783 -0
  99. package/dist/core/staging.js.map +1 -0
  100. package/dist/core/status.js +1015 -0
  101. package/dist/core/status.js.map +1 -0
  102. package/dist/core/verify.js +793 -0
  103. package/dist/core/verify.js.map +1 -0
  104. package/dist/core/version.js +8 -0
  105. package/dist/core/version.js.map +1 -0
  106. package/package.json +63 -4
@@ -0,0 +1 @@
1
+ {"version":3,"file":"provenance.js","sourceRoot":"","sources":["../../src/core/provenance.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Provenance checks: is this artifact anchored to anything, and has anyone\n * vouched for it?\n *\n * SCHEMA.md has declared `status` / `owner` / `sources` / `last_verified` since\n * the beginning and nothing read them. They matter more now that the prose is\n * written by an agent: coherence proves the corpus agrees with itself, and only\n * `sources` says it has anything to do with the code.\n *\n * Absence and contradiction are graded differently on purpose. A missing field\n * is incompleteness — a warning, something to work through. A field that says\n * the wrong thing (a spec claiming to be another service, a status nobody\n * defined) is a bug, and gates.\n */\nimport { spawn } from \"node:child_process\";\nimport { createHash } from \"node:crypto\";\nimport { existsSync } from \"node:fs\";\nimport { readdir, readFile, realpath, stat } from \"node:fs/promises\";\nimport { join, relative, resolve, sep } from \"node:path\";\nimport {\n listField,\n parseFrontmatter,\n rawBody,\n readFrontmatter,\n stringField,\n FEATURE_STATUSES,\n SERVICE_STATUSES,\n type Frontmatter,\n} from \"./frontmatter.js\";\nimport type { Finding } from \"./report.js\";\nimport { featurePaths, servicePaths } from \"./repo.js\";\nimport { isPathInside, resolveInside } from \"./path-safety.js\";\n\n/** Fields every artifact is expected to carry, beyond its identity and status. */\nconst EXPECTED = [\"owner\"] as const;\n\nexport interface ProvenanceOptions {\n /**\n * The repo `sources` resolve against — the service repo loam is running in.\n * Undefined when the docs being validated describe some other repository, in\n * which case the paths are not ours to resolve and are left alone.\n */\n repoDir?: string;\n}\n\nexport async function serviceProvenance(\n docsDir: string,\n service: string,\n opts: ProvenanceOptions = {},\n): Promise<Finding[]> {\n const paths = servicePaths(docsDir, service);\n const findings: Finding[] = [];\n // Both requirement-carrying specs get the same pass: arch.spec.md follows\n // spec.md's frontmatter conventions exactly, so the checks are one loop —\n // only the label tells a reader which file moved.\n for (const { path, file } of [\n { path: paths.spec, file: \"spec.md\" },\n { path: paths.archSpec, file: \"arch.spec.md\" },\n ]) {\n if (!existsSync(path)) continue;\n const raw = await readFile(path, \"utf8\");\n const fm = parseFrontmatter(raw);\n const label = `${service}: ${file}`;\n findings.push(\n ...identityFindings(fm, {\n label,\n idField: \"service\",\n idValue: service,\n statuses: [...SERVICE_STATUSES],\n }),\n );\n // With no frontmatter at all, \"it names no sources\" says nothing the missing\n // header did not already say — and a malformed one proves nothing either:\n // `sources.absent` against a header nobody can read is the same false\n // advice as the field cascade, so frontmatter.malformed stands alone.\n if (fm.present && !fm.malformed) {\n findings.push(...(await sourceFindings(fm, service, label, opts.repoDir)));\n findings.push(...contentFindings(fm, raw, service, label));\n }\n }\n return findings;\n}\n\nexport async function featureProvenance(\n featureDir: string,\n featureId: string,\n): Promise<Finding[]> {\n const path = featurePaths(featureDir).intent;\n if (!existsSync(path)) return [];\n const fm = await readFrontmatter(path);\n return identityFindings(fm, {\n label: \"intent.md\",\n idField: \"feature\",\n idValue: featureId,\n statuses: [...FEATURE_STATUSES],\n });\n}\n\ninterface IdentitySpec {\n label: string;\n idField: \"service\" | \"feature\";\n idValue: string;\n statuses: string[];\n}\n\nfunction identityFindings(fm: Frontmatter, spec: IdentitySpec): Finding[] {\n const findings: Finding[] = [];\n if (!fm.present) {\n return [\n {\n severity: \"warn\",\n code: \"frontmatter.missing\",\n subject: spec.idValue,\n message: `${spec.label} has no frontmatter — no owner, no status, no link to the code it describes`,\n },\n ];\n }\n\n // One honest error instead of the cascade: with an unreadable header the\n // field checks below would grade owner/status/service \"missing\" and send the\n // author adding fields to a block YAML refuses to parse. Nothing can be\n // concluded from a header nobody can read — including that it lacks fields.\n if (fm.malformed) {\n return [\n {\n severity: \"error\",\n code: \"frontmatter.malformed\",\n subject: spec.idValue,\n message: `${spec.label} has frontmatter that does not parse as YAML — owner, status and sources are unreadable until the header is fixed`,\n },\n ];\n }\n\n const id = stringField(fm, spec.idField);\n if (id !== undefined && id !== spec.idValue) {\n findings.push({\n severity: \"error\",\n code: \"frontmatter.field-mismatch\",\n subject: spec.idValue,\n message: `${spec.label} declares ${spec.idField}: ${id}, but it lives under ${spec.idValue}`,\n });\n }\n\n const status = stringField(fm, \"status\");\n if (status !== undefined && !spec.statuses.includes(status)) {\n findings.push({\n severity: \"error\",\n code: \"frontmatter.status-unknown\",\n subject: spec.idValue,\n message: `${spec.label} has status '${status}' — expected one of ${spec.statuses.join(\", \")}`,\n });\n }\n\n const missing = [\n ...(id === undefined ? [spec.idField] : []),\n ...(status === undefined ? [\"status\"] : []),\n ...EXPECTED.filter((f) => stringField(fm, f) === undefined),\n ];\n if (missing.length > 0) {\n findings.push({\n severity: \"warn\",\n code: \"frontmatter.field-missing\",\n subject: spec.idValue,\n message: `${spec.label} is missing ${missing.length} frontmatter field(s)`,\n details: missing,\n text: { detailPrefix: \"- \" },\n });\n }\n return findings;\n}\n\nasync function sourceFindings(\n fm: Frontmatter,\n service: string,\n label: string,\n repoDir: string | undefined,\n): Promise<Finding[]> {\n const sources = listField(fm, \"sources\");\n if (sources.length === 0) {\n return [\n {\n severity: \"warn\",\n code: \"sources.absent\",\n message: `${label} names no sources — nothing ties it to the code, so nothing can tell you when it goes out of date`,\n },\n ];\n }\n // Someone else's repository: the paths describe a tree loam is not standing in.\n if (repoDir === undefined) return [];\n\n // Before existence: a pattern entry would \"not exist\" as a literal path, and\n // grading it missing would send the author fixing the wrong thing.\n const patterns = patternSources(sources);\n if (patterns.length > 0) {\n return [\n {\n severity: \"error\",\n code: \"sources.path-missing\",\n message: `${label}: ${patterns.length} source(s) are glob patterns — ${patterns.join(\", \")}. Patterns are no longer supported: name files or directories (a directory already covers everything beneath it).`,\n details: patterns,\n text: { detailPrefix: \"- \" },\n },\n ];\n }\n\n const unsafe = unsafeSources(repoDir, sources);\n if (unsafe.length > 0) {\n return [\n {\n severity: \"error\",\n code: \"sources.path-outside\",\n message: `${label}: ${unsafe.length} source(s) escape the service repo — ${unsafe.join(\", \")}. Sources must be relative paths contained by this repository, including through symlinks.`,\n details: unsafe,\n text: { detailPrefix: \"- \" },\n },\n ];\n }\n\n const missing = missingSources(repoDir, sources);\n if (missing.length > 0) {\n return [\n {\n severity: \"error\",\n code: \"sources.path-missing\",\n message: `${label}: ${missing.length} source(s) do not exist — ${missing.join(\", \")}`,\n details: missing,\n text: { detailPrefix: \"- \" },\n },\n ];\n }\n const resolved: Finding = {\n severity: \"ok\",\n code: \"sources.resolved\",\n message: `${label}: ${sources.length} source(s) resolve`,\n };\n\n // The walk happens here rather than inside the staleness branch below, even\n // though only staleness needs the hashes: what the walk REFUSED to follow is\n // a hole in the tie to the code whether or not anybody has vouched yet, and\n // an unvouched service is exactly where a reader is about to decide the\n // sources list is honest.\n const { digest, files, index, skipped } = await sourcesDigest(repoDir, sources);\n const findings: Finding[] = [resolved, ...skippedFindings(skipped, label)];\n\n // The paths are there; the question staleness answers is whether what is AT\n // them is still what somebody read.\n const stamped = stringField(fm, \"sources_digest\");\n if (stamped === undefined) {\n return [\n ...findings,\n {\n severity: \"warn\",\n code: \"sources.unvouched\",\n message: `${label}: no sources_digest — nobody has vouched for this against the code, so nothing can tell you when it goes stale. Run \\`loam vouch --service ${service}\\`.`,\n },\n ];\n }\n\n const since = stringField(fm, \"last_verified\") ?? \"it was stamped\";\n if (digest === stamped) {\n return [\n ...findings,\n {\n severity: \"ok\",\n code: \"sources.current\",\n message: `${label}: sources unchanged since ${since} (${files.length} file(s), digest ${digest})`,\n },\n ];\n }\n\n // What MOVED, when the stamp recorded enough to say. Repeating the frontmatter's\n // own `sources` entries back at a reader — which is all this finding could do\n // before `sources_files` existed — answers a question nobody asked: they wrote\n // that list, and \"one of these five directories changed somehow\" is where the\n // search starts, not where it ends.\n const moved = movedSources(decodeSourceIndex(stringField(fm, \"sources_files\")), index);\n return [\n ...findings,\n {\n // A warning, not an error: the doc may still be right, and only a person\n // can say. What loam knows is that nobody has looked since the code moved.\n severity: \"warn\",\n code: \"sources.stale\",\n message: `${label}: sources changed since ${since}${moved.summary} — re-read them and \\`loam vouch --service ${service}\\``,\n details: moved.paths ?? sources,\n text: { detailPrefix: \"- \" },\n },\n ];\n}\n\n/**\n * The paths the digest walk would not follow. Never silent: a symlink dropped\n * on the floor is a file whose content nobody is watching, while the document\n * beside it says `verified` — the digest cannot go stale over bytes it never\n * hashed, so the one thing the stamp promises is quietly untrue for them.\n *\n * A warning, not an error, and for the same reason staleness is: the doc may be\n * perfectly right about the code behind that link. What loam can say is that it\n * is not watching it.\n */\nfunction skippedFindings(skipped: SkippedSource[], label: string): Finding[] {\n if (skipped.length === 0) return [];\n return [\n {\n severity: \"warn\",\n code: \"sources.skipped\",\n message: `${label}: ${skipped.length} path(s) under the listed sources were not hashed — the digest says nothing about what is behind them`,\n details: skipped.map((s) => `${s.path} — ${s.reason}`),\n text: { detailPrefix: \"- \" },\n },\n ];\n}\n\n/**\n * The doc-side half of the freshness check. `sources_digest` says whether the\n * CODE moved since a person looked, and only that service's repo can recompute\n * it; `content_digest` says whether the DOCUMENT did, and needs nothing but\n * the document — so this runs wherever the spec is readable, `--service` and\n * `--all` alike. Without it, editing a spec after it was vouched left\n * `status: verified` standing over words nobody read — forged freshness, in\n * the exact agent-written-prose threat model this layer exists for.\n */\nfunction contentFindings(fm: Frontmatter, raw: string, service: string, label: string): Finding[] {\n if (stringField(fm, \"status\") !== \"verified\") return [];\n const stamped = stringField(fm, \"content_digest\");\n // A verified doc with no stamp predates the field. Quiet on purpose: the fix\n // is re-vouching, and grading years of pre-feature vouches as suspect would\n // drown the fleet in warnings nobody chose.\n if (stamped === undefined) return [];\n if (contentDigest(raw) === stamped) return [];\n const since = stringField(fm, \"last_verified\") ?? \"it was vouched\";\n return [\n {\n // A warning, not an error — the sources.stale doctrine: the doc changed\n // since it was vouched, and only a person can say whether verified still\n // holds of the new words.\n severity: \"warn\",\n code: \"content.stale\",\n message: `${label} changed since ${since} — the doc moved under its vouch, and only a person can say whether 'verified' still holds. Re-read it and \\`loam vouch --service ${service}\\`.`,\n },\n ];\n}\n\n/* ------------------------------------------------------------------ */\n/* The content digest */\n/* ------------------------------------------------------------------ */\n\n/** How much of the sha256 is written into the document. */\nconst DIGEST_LENGTH = 16;\n\nexport interface SourcesDigest extends SourcesExpansion {\n /** The stamp that goes in `sources_digest`. */\n digest: string;\n /**\n * Per file, its own short sha — the record `sources_files` carries so a later\n * `sources.stale` can name what moved instead of guessing. Same order as `files`.\n */\n index: SourceIndexEntry[];\n}\n\n/** One file's share of the stamp: what it is called, and what it said. */\nexport interface SourceIndexEntry {\n /** Repo-relative, `/`-separated. */\n path: string;\n /** sha256 of its bytes, truncated the way `sources_digest` is. */\n sha: string;\n}\n\n/** A path the walk found but would not hash, and the reason a reader needs. */\nexport interface SkippedSource {\n /** Repo-relative, `/`-separated. */\n path: string;\n /** Why it was left out — the half of the sentence after the path. */\n reason: string;\n}\n\n/** What a `sources` list covers, and what it does not. */\nexport interface SourcesExpansion {\n /** Repo-relative paths, sorted — exactly what a digest would hash. */\n files: string[];\n /** Everything the walk refused to follow. Empty in the ordinary case. */\n skipped: SkippedSource[];\n /**\n * Set only when `files` is empty: the one sentence that says the list covers\n * nothing. `loam vouch` refuses with it, `loam validate` grades it — see\n * emptySourcesMessage for why they must be the same words.\n */\n empty?: string;\n}\n\n/**\n * Digest the CONTENT of the files `sources` names. The recipe is spelled out\n * because the value is written into documents, and anything reading them has to\n * be able to reproduce it:\n *\n * 1. expand each entry to repo-relative file paths (`/` separators) — a file\n * is itself, a directory everything beneath it, minus what the repository\n * does not consider its own (see collectSources) — sorted and de-duplicated;\n * 2. per file, `sha256(bytes)`;\n * 3. feed `<path>\\0<hex>\\n` for each file, in that order, into an outer\n * sha256;\n * 4. keep the first 16 hex characters.\n *\n * Content, not mtime: git does not preserve modification times, so after a\n * fresh clone every file looks changed and the check would be false positives\n * end to end. Bytes survive the clone. Hashing the path alongside the content\n * means a rename registers, which is what a reader of the doc would want.\n *\n * 64 bits is a change detector, not a seal — it answers \"did this move?\", and\n * an adversary who wants a collision can have one.\n */\nexport async function sourcesDigest(repoDir: string, sources: string[]): Promise<SourcesDigest> {\n const { found, skipped } = await collectSources(repoDir, sources);\n const outer = createHash(\"sha256\");\n const index: SourceIndexEntry[] = [];\n for (const file of found) {\n const content = createHash(\"sha256\").update(await readFile(file.abs)).digest(\"hex\");\n outer.update(`${file.rel}\\0${content}\\n`);\n index.push({ path: file.rel, sha: content.slice(0, DIGEST_LENGTH) });\n }\n // `empty` is deliberately not set here: it is a sentence about a named\n // document, and a digest has no name to put in it. Callers that need it say\n // emptySourcesMessage(label, sources) — the same words, from one definition.\n return { digest: outer.digest(\"hex\").slice(0, DIGEST_LENGTH), files: index.map((e) => e.path), index, skipped };\n}\n\n/**\n * What a `sources` list actually covers — the digest's file set without the\n * hashing.\n *\n * Exported because two commands must give the SAME answer to \"does this list\n * cover anything?\". `loam vouch` refuses to stamp an expansion that covers no\n * files (a digest over nothing never changes, so the stamp would read as\n * current forever), and until `loam validate` could say the same thing in the\n * same words, an author got a green validate followed by a vouch that refused —\n * two commands contradicting each other about one document, with nothing in the\n * green run hinting at it.\n */\nexport async function expandSourceFiles(\n repoDir: string,\n sources: string[],\n label: string,\n): Promise<SourcesExpansion> {\n const { found, skipped } = await collectSources(repoDir, sources);\n const files = found.map((f) => f.rel);\n return { files, skipped, ...(files.length === 0 ? { empty: emptySourcesMessage(label, sources) } : {}) };\n}\n\n/**\n * The one sentence for \"these paths exist and cover no file\". One definition,\n * because `vouch`'s refusal and `validate`'s finding are the same diagnosis\n * about the same document and must not drift into two descriptions of it — the\n * author fixes it once, in the sources list.\n */\nexport function emptySourcesMessage(label: string, sources: string[]): string {\n return `${label}: the sources listed match no files — ${sources.join(\", \")}. A digest over nothing would read as current forever.`;\n}\n\n/**\n * The stamp `loam vouch` writes into `content_digest`: sha256 of the\n * document's own BODY — every byte after the frontmatter block (below the\n * closing `---` line and its newline; `rawBody` is the one definition of that\n * cut) — first 16 hex characters, the sources recipe's length.\n *\n * Byte-exact, no normalization. Body-only is load-bearing: vouch itself\n * rewrites the frontmatter as it stamps, and a later frontmatter-only edit\n * (another vouch, a corrected owner) must not read as the document moving.\n */\nexport function contentDigest(source: string): string {\n return createHash(\"sha256\").update(rawBody(source), \"utf8\").digest(\"hex\").slice(0, DIGEST_LENGTH);\n}\n\ninterface SourceFile {\n /** Repo-relative, `/`-separated — the spelling that goes into the hash. */\n rel: string;\n abs: string;\n}\n\n/**\n * The files a `sources` list names: a file is itself, a directory is everything\n * beneath it. Nothing else — glob patterns used to be matched here, by a\n * hand-rolled dialect that silently differed from the gitignore/minimatch\n * conventions authors assume, so a pattern quietly digested a DIFFERENT file\n * set than its author intended. Pattern-looking entries are now refused loudly\n * upstream (see patternSources); this function only ever sees literal paths.\n *\n * Three exclusions, all part of the digest recipe's contract:\n *\n * - dot-entries are skipped while walking — `.git` is not what the doc was\n * written from — though a path naming one outright is still honoured;\n * - `node_modules` is skipped the same way, unconditionally: it is the one\n * build input every ecosystem agrees is not source, and it is the one that\n * makes the walk take minutes;\n * - anything the repository's own `.gitignore` covers is dropped, when the\n * repo is a git checkout and git is on the PATH (see gitIgnoredPaths).\n *\n * The third is the one worth stating a reason for. `sources_digest` answers\n * \"did the code move since a person read it\", and BUILD OUTPUT moves on every\n * CI run without anybody touching the code — a `sources: [src/]` over a repo\n * that compiles into `src/generated/` went stale on a schedule, so the warning\n * that means \"re-read this\" arrived when nothing had been written. A signal\n * that fires every night is a signal people learn to close. Git's answer is\n * used rather than a list of our own because it is the answer the repository\n * already gives every other tool, and it is the author's to change.\n *\n * The fallback is deliberate: no git, no checkout, a git that errors — hash\n * everything. Missing an exclusion costs noise; inventing one costs a file the\n * stamp silently stops watching, and this walk exists to have nothing in that\n * category.\n */\nasync function collectSources(\n repoDir: string,\n sources: string[],\n): Promise<{ found: SourceFile[]; skipped: SkippedSource[] }> {\n const found = new Map<string, string>();\n const skipped = new Map<string, string>();\n const relOf = (abs: string): string => relative(repoDir, abs).split(sep).join(\"/\");\n\n // realpath once, up front: every containment question below is asked against\n // the resolved root, so a repo reached through a symlink (a worktree under\n // /var -> /private/var on macOS, say) does not make its own files look external.\n const repoReal = await realpath(repoDir).catch(() => resolve(repoDir));\n // The realpaths of the directories currently being walked — the cycle guard.\n // A STACK rather than a set of everything ever entered, because the two rules\n // answer different questions: a directory reachable by two spellings (a\n // `current -> versions/3` link beside the real tree) is content under both\n // names and belongs in the digest twice, while a directory reachable from\n // INSIDE itself is a loop. A global visited-set conflates them, and then which\n // spelling survives depends on the order readdir happened to return — a\n // digest that differs between two machines holding identical bytes.\n const walking: string[] = [];\n\n const walk = async (dir: string): Promise<void> => {\n const entries = await readdir(dir, { withFileTypes: true }).catch(() => []);\n // Sorted, so a tree walks the same way on every filesystem.\n entries.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));\n for (const entry of entries) {\n if (entry.name.startsWith(\".\") || entry.name === \"node_modules\") continue;\n const child = join(dir, entry.name);\n if (entry.isDirectory()) await enterDir(child);\n else if (entry.isFile()) found.set(relOf(child), child);\n // readdir reports symlinks by lstat, so isDirectory() and isFile() are\n // BOTH false for one: before this branch existed a symlinked file, and a\n // whole subtree behind a symlinked directory, fell between the two arms\n // and left the digest without a word said. That is the failure this\n // module is here to prevent, arriving through its own walk.\n else if (entry.isSymbolicLink()) await follow(child);\n }\n };\n\n const enterDir = async (dir: string): Promise<void> => {\n const real = await realpath(dir).catch(() => null);\n if (real === null || walking.includes(real)) return;\n walking.push(real);\n try {\n await walk(dir);\n } finally {\n walking.pop();\n }\n };\n\n const follow = async (link: string): Promise<void> => {\n const real = await realpath(link).catch(() => null);\n if (real === null) {\n skipped.set(relOf(link), \"a symlink that does not resolve\");\n return;\n }\n // Outside the repo the file is not this service's to vouch for, and the\n // same rule already refuses it as a top-level `sources` entry\n // (sources.path-outside). Reported rather than dropped: an author who\n // vendored a sibling repo through a symlink needs to know the stamp stops\n // at the link.\n if (!isPathInside(repoReal, real)) {\n skipped.set(relOf(link), \"a symlink whose target is outside this repository\");\n return;\n }\n const info = await stat(link).catch(() => null);\n if (info === null) {\n skipped.set(relOf(link), \"a symlink that does not resolve\");\n return;\n }\n // A link's own spelling is what the doc's author wrote, so that is the path\n // that goes into the digest — the content comes from the target either way.\n if (info.isFile()) found.set(relOf(link), link);\n // A link that closes a loop is not reported: everything behind it is already\n // in the digest under the spelling it was reached by, so nothing has stopped\n // being watched. Only unhashed CONTENT is worth a warning.\n else if (info.isDirectory()) await enterDir(link);\n else skipped.set(relOf(link), \"a symlink to neither a file nor a directory\");\n };\n\n for (const source of sources) {\n const cleaned = source.trim();\n if (cleaned.length === 0) continue;\n const root = resolveInside(repoDir, cleaned, `source '${source}'`);\n const info = await stat(root).catch(() => null);\n if (info === null) continue;\n if (info.isFile()) found.set(relOf(root), root);\n else if (info.isDirectory()) await enterDir(root);\n }\n\n // One git invocation for the whole expansion, not one per file: the skipped\n // list rides along so an ignored symlink does not warn about a path the\n // repository already says is not its own.\n const ignored = await gitIgnoredPaths(repoDir, [...found.keys(), ...skipped.keys()]);\n for (const rel of ignored) {\n found.delete(rel);\n skipped.delete(rel);\n }\n\n return {\n // Plain codepoint order, not locale order: the digest has to be the same\n // everywhere it is computed.\n found: [...found.entries()]\n .map(([rel, abs]) => ({ rel, abs }))\n .sort((a, b) => (a.rel < b.rel ? -1 : a.rel > b.rel ? 1 : 0)),\n skipped: [...skipped.entries()]\n .map(([path, reason]) => ({ path, reason }))\n .sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0)),\n };\n}\n\n/** How long the digest waits on git before deciding it learned nothing. */\nconst GIT_TIMEOUT_MS = 10_000;\n\n/**\n * Which of these repo-relative paths the repository ignores, asked once for the\n * whole list. `git check-ignore` is the authority on purpose — it reads\n * `.gitignore` at every level, `.git/info/exclude` and the user's global\n * excludes, and it already knows that a TRACKED file is not ignored no matter\n * what a pattern says, which is the distinction a re-implementation always gets\n * wrong.\n *\n * Every failure answers \"nothing is ignored\": exit 1 is git saying so, exit 128\n * is \"not a git repository\", a spawn error is git not being installed, and a\n * timeout is a repository too strange to wait for. None of them justify\n * dropping a file from a digest a person is going to sign.\n */\nasync function gitIgnoredPaths(repoDir: string, rels: string[]): Promise<Set<string>> {\n if (rels.length === 0) return new Set();\n return new Promise<Set<string>>((done) => {\n const child = spawn(\"git\", [\"check-ignore\", \"--stdin\", \"-z\"], {\n cwd: repoDir,\n stdio: [\"pipe\", \"pipe\", \"ignore\"],\n timeout: GIT_TIMEOUT_MS,\n });\n let out = \"\";\n child.stdout.setEncoding(\"utf8\");\n child.stdout.on(\"data\", (chunk: string) => {\n out += chunk;\n });\n child.on(\"error\", () => done(new Set()));\n child.on(\"close\", (code) => {\n // 0: some paths are ignored and are on stdout. 1: none are — an empty\n // answer, not a failure. Anything else: we learned nothing.\n done(code === 0 || code === 1 ? new Set(out.split(\"\\0\").filter((s) => s.length > 0)) : new Set());\n });\n // A git that died before reading its input hands us EPIPE; the close\n // handler above is what decides the outcome either way.\n child.stdin.on(\"error\", () => undefined);\n child.stdin.end(rels.join(\"\\0\") + \"\\0\");\n });\n}\n\n/* ------------------------------------------------------------------ */\n/* The per-file index: what `sources.stale` names */\n/* ------------------------------------------------------------------ */\n\n/**\n * How many files `sources_files` will list one by one before it gives up and\n * records only how many there were.\n *\n * A readability budget, not a correctness one: the index lives in the header of\n * a document a person reads, and a `sources: [src/]` over a legacy service can\n * expand to thousands of files. Past the limit `sources.stale` falls back to\n * repeating the `sources` entries — worse advice, but a spec.md whose\n * frontmatter is longer than its requirements is worse still.\n */\nconst SOURCE_INDEX_LIMIT = 100;\n\n/**\n * The `sources_files` value `loam vouch` stamps beside `sources_digest`: one\n * `<sha> <path>` line per file (sha256sum's layout, and its column order, so\n * the path may contain spaces), or just the file count once the list would run\n * past SOURCE_INDEX_LIMIT.\n *\n * Its whole purpose is the next `sources.stale`. `sources_digest` can say THAT\n * the code moved and never which part of it; with the index a reader gets the\n * added, removed and changed paths, which is the difference between re-reading\n * one file and re-reading a directory. It is also a readable git diff in the\n * docs repo: the vouch commit shows exactly which source files the stamp now\n * stands over.\n */\nexport function encodeSourceIndex(index: SourceIndexEntry[]): string {\n if (index.length > SOURCE_INDEX_LIMIT) return String(index.length);\n return index.map((e) => `${e.sha} ${e.path}`).join(\"\\n\");\n}\n\n/** What a stamped `sources_files` says: the per-file shas, the count, or nothing. */\ninterface StampedIndex {\n /** path -> sha, or null when the stamp did not record them. */\n entries: Map<string, string> | null;\n /** How many files the stamp covered, when that is knowable. */\n count?: number;\n}\n\n/**\n * Read back what `encodeSourceIndex` wrote. Anything unrecognised reads as \"the\n * stamp said nothing\" rather than as an empty index: a header nobody can parse\n * must not turn into a `sources.stale` claiming every file was deleted.\n */\nfunction decodeSourceIndex(stamped: string | undefined): StampedIndex {\n if (stamped === undefined) return { entries: null };\n const text = stamped.trim();\n if (text.length === 0) return { entries: null };\n if (/^\\d+$/.test(text)) return { entries: null, count: Number(text) };\n\n const entries = new Map<string, string>();\n for (const line of text.split(\"\\n\")) {\n const match = /^([0-9a-f]{16})\\s+(.+)$/.exec(line.trim());\n if (match === null) return { entries: null };\n entries.set(match[2]!, match[1]!);\n }\n return { entries, count: entries.size };\n}\n\n/** The paths that moved between a stamp and now, with a phrase for the message. */\ninterface MovedSources {\n /** Annotated paths, or undefined when the stamp did not record enough to say. */\n paths?: string[];\n /** Appended to the `sources.stale` message; empty when there is nothing to add. */\n summary: string;\n}\n\n/**\n * added / removed / changed, from the stamped index against the current one.\n * The annotation travels IN the detail line rather than in three separate\n * lists: a reader scanning a stale report wants the paths in one column, and\n * \"which of the three\" is one word wide.\n */\nfunction movedSources(stamp: StampedIndex, now: SourceIndexEntry[]): MovedSources {\n if (stamp.entries === null) {\n // No index, but the count alone still beats silence: \"12 files then, 14\n // now\" tells a reader to go looking for two new files.\n return {\n summary: stamp.count === undefined ? \"\" : ` (${stamp.count} file(s) then, ${now.length} now)`,\n };\n }\n const paths: string[] = [];\n const current = new Map(now.map((e) => [e.path, e.sha]));\n for (const [path, sha] of current) {\n const before = stamp.entries.get(path);\n if (before === undefined) paths.push(`added ${path}`);\n else if (before !== sha) paths.push(`changed ${path}`);\n }\n for (const path of stamp.entries.keys()) if (!current.has(path)) paths.push(`removed ${path}`);\n paths.sort();\n // An index that agrees with the current tree while the digest does not is a\n // contradiction (a truncation collision, or a hand-edited header). Say\n // nothing rather than print an empty list under a staleness warning.\n if (paths.length === 0) return { summary: \"\" };\n return { paths, summary: ` (${paths.length} path(s) moved)` };\n}\n\n/** The characters that made an entry a pattern under the glob dialect loam no longer ships. */\nconst PATTERN_CHARS = /[*?[]/;\n\n/**\n * The `sources` entries that look like glob patterns — anything containing\n * `*`, `?` or `[`. Patterns are refused loudly everywhere sources are consumed\n * (`loam vouch` refuses the run, `loam validate` grades an error), never\n * resolved: the glob dialect loam used to ship silently differed from the\n * gitignore/minimatch conventions authors assume — bracket classes literal,\n * its own `**` handling — so a pattern quietly digested a different file set\n * than intended, corrupting the staleness signal in both directions. A\n * directory already means \"everything beneath it\", so the fix is to name one.\n *\n * The cost accepted with the rule: a real file with `[` in its name (a Next.js\n * route, say) cannot be listed on its own — its parent directory covers it.\n */\nexport function patternSources(sources: string[]): string[] {\n return sources.filter((s) => PATTERN_CHARS.test(s.trim()));\n}\n\n/** The `sources` entries that resolve to nothing in this repo. */\nexport function missingSources(repoDir: string, sources: string[]): string[] {\n return sources.filter((s) => !sourceExists(repoDir, s));\n}\n\n/** Entries that are absolute, traverse upward, or escape through a symlink. */\nexport function unsafeSources(repoDir: string, sources: string[]): string[] {\n return sources.filter((source) => {\n const cleaned = source.trim();\n if (cleaned.length === 0) return false;\n try {\n resolveInside(repoDir, cleaned, `source '${source}'`);\n return false;\n } catch {\n return true;\n }\n });\n}\n\n/** Does a `sources` entry point at something real? Literal paths only, checked exactly. */\nfunction sourceExists(repoDir: string, source: string): boolean {\n const cleaned = source.trim();\n if (cleaned.length === 0) return false;\n try {\n return existsSync(resolveInside(repoDir, cleaned, `source '${source}'`));\n } catch {\n return false;\n }\n}\n"]}
@@ -0,0 +1,362 @@
1
+ /**
2
+ * Read model of the docs repo — the single place that knows its layout.
3
+ *
4
+ * Enumeration goes by the filesystem, and only by the filesystem: files are the
5
+ * source of truth, so `services/` IS the list of services and there is no
6
+ * manifest that can disagree with it. Every command that needs "which services
7
+ * exist", "which features are in flight" or "where does artifact X live" asks
8
+ * here, so the layout is spelled exactly once.
9
+ */
10
+ import { existsSync, statSync } from "node:fs";
11
+ import { readdir } from "node:fs/promises";
12
+ import { join } from "node:path";
13
+ import { listField, readFrontmatter, stringField } from "./frontmatter.js";
14
+ import { serviceIdProblem } from "./ids.js";
15
+ /** Directory under features/ holding shipped features. Never a feature itself. */
16
+ const ARCHIVE_DIR = "archive";
17
+ /**
18
+ * Compare ids so digit runs sort numerically: FEAT-2 before FEAT-10.
19
+ * Deterministic and locale-independent — ordering is part of the output
20
+ * contract for `list`, and `--json` consumers diff it.
21
+ */
22
+ export function compareIds(a, b) {
23
+ const ta = tokenize(a);
24
+ const tb = tokenize(b);
25
+ for (let i = 0; i < Math.max(ta.length, tb.length); i += 1) {
26
+ const x = ta[i];
27
+ const y = tb[i];
28
+ if (x === undefined)
29
+ return -1;
30
+ if (y === undefined)
31
+ return 1;
32
+ if (typeof x === "number" && typeof y === "number") {
33
+ if (x !== y)
34
+ return x - y;
35
+ }
36
+ else if (String(x) !== String(y)) {
37
+ return String(x) < String(y) ? -1 : 1;
38
+ }
39
+ }
40
+ return 0;
41
+ }
42
+ function tokenize(s) {
43
+ return (s.match(/\d+|\D+/g) ?? []).map((t) => (/^\d/.test(t) ? Number(t) : t));
44
+ }
45
+ /**
46
+ * Feature id from a directory name: everything up to and including the first
47
+ * number run (`FEAT-101-payment-splitting` -> `FEAT-101`). A name with no
48
+ * `<word>-<number>` head is its own id. Quirk: a dated slug keeps only its first
49
+ * segment (`release-2024-01-x` -> `release-2024`) — ids are not meant to be dates.
50
+ */
51
+ export function featureIdFromDirName(dirName) {
52
+ const m = /^(.*?-\d+)(?:-|$)/.exec(dirName);
53
+ return m ? m[1] : dirName;
54
+ }
55
+ export function servicePaths(docsDir, service) {
56
+ const dir = join(docsDir, "services", service);
57
+ return {
58
+ dir,
59
+ model: join(dir, "model.likec4"),
60
+ spec: join(dir, "spec.md"),
61
+ archSpec: join(dir, "arch.spec.md"),
62
+ openapi: join(dir, "openapi.yaml"),
63
+ runbook: join(dir, "runbook.md"),
64
+ health: join(dir, "health.yaml"),
65
+ adrsDir: join(dir, "adrs"),
66
+ };
67
+ }
68
+ /**
69
+ * The pair of requirement-carrying spec files, living and delta alike: the
70
+ * business spec and the architecture spec. Same grammar, same delta algebra,
71
+ * same merge — everything that walks "the spec files" of a service walks this
72
+ * list, so the two axes cannot drift apart in what handles them. `key` indexes
73
+ * ServicePaths/FeatureSpecPaths; `label` is how the axis names itself in prose.
74
+ */
75
+ export const SPEC_AXES = [
76
+ { key: "spec", file: "spec.md", label: "requirements" },
77
+ { key: "archSpec", file: "arch.spec.md", label: "arch requirements" },
78
+ ];
79
+ export function featurePaths(featureDir) {
80
+ return {
81
+ dir: featureDir,
82
+ intent: join(featureDir, "intent.md"),
83
+ delta: join(featureDir, "delta.likec4"),
84
+ specsDir: join(featureDir, "specs"),
85
+ adrsDir: join(featureDir, "adrs"),
86
+ };
87
+ }
88
+ export function featureSpecPaths(featureDir, service) {
89
+ const dir = join(featureDir, "specs", service);
90
+ return {
91
+ dir,
92
+ spec: join(dir, "spec.md"),
93
+ archSpec: join(dir, "arch.spec.md"),
94
+ openapi: join(dir, "openapi.yaml"),
95
+ };
96
+ }
97
+ export function landscapePath(docsDir) {
98
+ return join(docsDir, "architecture", "landscape.likec4");
99
+ }
100
+ export function featuresDir(docsDir) {
101
+ return join(docsDir, "features");
102
+ }
103
+ export function docsRepoState(docsDir) {
104
+ const path = docsDir;
105
+ let isDir = false;
106
+ try {
107
+ isDir = statSync(path).isDirectory();
108
+ }
109
+ catch {
110
+ isDir = false;
111
+ }
112
+ if (!isDir)
113
+ return { kind: "missing", path };
114
+ return { kind: existsSync(join(path, "services")) ? "ok" : "no-services", path };
115
+ }
116
+ /**
117
+ * The docs repo is not there. Thrown rather than returned because every caller
118
+ * of the enumeration wants the same thing — to stop — and a `[]` return is the
119
+ * bug this closes. `message` is written for a human at a terminal: the CLI's
120
+ * top-level handler prints it verbatim, and commands that catch it report
121
+ * `repository-unavailable`.
122
+ */
123
+ export class DocsRepoUnavailableError extends Error {
124
+ state;
125
+ constructor(state, message) {
126
+ super(message);
127
+ this.state = state;
128
+ this.name = "DocsRepoUnavailableError";
129
+ }
130
+ }
131
+ /**
132
+ * Refuse to enumerate a docs repo that is not one. `allow` names the states the
133
+ * caller can honestly survive: features/ may be absent from a real docs repo
134
+ * (nothing is in flight yet), so feature enumeration only insists the docs repo
135
+ * EXISTS, while service enumeration insists it is shaped like a docs repo — a
136
+ * `services/` directory is what makes it one.
137
+ */
138
+ function requireDocsRepo(docsDir, allow) {
139
+ const state = docsRepoState(docsDir);
140
+ if (allow.includes(state.kind))
141
+ return;
142
+ if (state.kind === "missing") {
143
+ throw new DocsRepoUnavailableError(state, `The configured docs repo does not exist: ${state.path}. ` +
144
+ "Fix `docsDir` in loam.json, clone the docs repo there, " +
145
+ "or run `loam init --docs <dir> --create` to make a new one.");
146
+ }
147
+ throw new DocsRepoUnavailableError(state, `${state.path} is not a docs repo — it has no services/ directory. ` +
148
+ "Point `docsDir` in loam.json at the shared docs repo, " +
149
+ "or run `loam init --docs <dir> --create` to make one.");
150
+ }
151
+ /* ------------------------------------------------------------------ */
152
+ /* Enumeration */
153
+ /* ------------------------------------------------------------------ */
154
+ /**
155
+ * What a directory entry IS, following a symlink to find out.
156
+ *
157
+ * `readdir(withFileTypes)` does not follow symlinks: for a symlink `Dirent`
158
+ * every `isDirectory()`/`isFile()` is false, whatever it points at. That made a
159
+ * symlinked `services/<id>/` or `features/<id>/` vanish from every enumeration
160
+ * at once — `list`, `validate --all`, the fleet gate — with no diagnostic
161
+ * anywhere, which is the one outcome a shared docs repo cannot survive: the
162
+ * fleet gate reported a service that is not there as fine because it never saw
163
+ * it. Composing a docs repo out of symlinks (a worktree, a submodule, one
164
+ * service's directory shared between two checkouts) is legitimate, so they are
165
+ * FOLLOWED rather than refused. A dangling link stats as nothing and is skipped
166
+ * exactly as an absent directory is — it is not a service, and saying so would
167
+ * mean inventing an entry to hang the complaint on.
168
+ */
169
+ function entryIs(dir, e, want) {
170
+ if (!e.isSymbolicLink())
171
+ return want === "dir" ? e.isDirectory() : e.isFile();
172
+ try {
173
+ const s = statSync(join(dir, e.name));
174
+ return want === "dir" ? s.isDirectory() : s.isFile();
175
+ }
176
+ catch {
177
+ return false;
178
+ }
179
+ }
180
+ /**
181
+ * Sorted names of the real subdirectories of `dir` (dot-dirs and files skipped).
182
+ * A missing `dir` is an empty list — the swallow is deliberate but narrow: it
183
+ * only ever runs INSIDE a docs repo that `requireDocsRepo` has already
184
+ * confirmed, where an absent `features/` or `specs/` means "none yet" and
185
+ * nothing else.
186
+ */
187
+ async function subdirs(dir) {
188
+ if (!existsSync(dir))
189
+ return [];
190
+ const entries = await readdir(dir, { withFileTypes: true });
191
+ return entries
192
+ .filter((e) => !e.name.startsWith(".") && entryIs(dir, e, "dir"))
193
+ .map((e) => e.name)
194
+ .sort(compareIds);
195
+ }
196
+ async function countMarkdown(dir) {
197
+ if (!existsSync(dir))
198
+ return 0;
199
+ const entries = await readdir(dir, { withFileTypes: true });
200
+ return entries.filter((e) => e.name.endsWith(".md") && entryIs(dir, e, "file")).length;
201
+ }
202
+ /** Services a feature carries a delta for, ordered — the subdirs of its specs/. */
203
+ export async function featureSpecServices(featureDir, context) {
204
+ if (context !== undefined)
205
+ return context.featureSpecServices(featureDir);
206
+ return subdirs(featurePaths(featureDir).specsDir);
207
+ }
208
+ /** Every service in the docs repo, ordered by id. */
209
+ export async function listServices(docsDir, context) {
210
+ if (context !== undefined)
211
+ return context.listServices(docsDir);
212
+ requireDocsRepo(docsDir, ["ok"]);
213
+ const names = await subdirs(join(docsDir, "services"));
214
+ return Promise.all(names.map(async (id) => {
215
+ const p = servicePaths(docsDir, id);
216
+ const fm = await readFrontmatter(p.spec);
217
+ const idProblem = serviceIdProblem(id, "directory name");
218
+ return {
219
+ id,
220
+ dir: p.dir,
221
+ ...(idProblem === null ? {} : { idProblem }),
222
+ has: {
223
+ model: existsSync(p.model),
224
+ spec: existsSync(p.spec),
225
+ openapi: existsSync(p.openapi),
226
+ runbook: existsSync(p.runbook),
227
+ health: existsSync(p.health),
228
+ },
229
+ adrs: await countMarkdown(p.adrsDir),
230
+ status: stringField(fm, "status") ?? null,
231
+ sources: {
232
+ declared: listField(fm, "sources").length > 0,
233
+ stamped: stringField(fm, "sources_digest") !== undefined,
234
+ },
235
+ };
236
+ }));
237
+ }
238
+ /**
239
+ * The directories under `services/` that no loam command can address.
240
+ *
241
+ * A finding rather than a silent skip, and an ERROR rather than a warning,
242
+ * because the state is not survivable in either direction: `loam adopt`,
243
+ * `loam delta`, `loam rebase` and `loam new --touches` all refuse the id with
244
+ * `invalid-option`, so the documents in there can never be updated by loam
245
+ * again, while `validate --all` used to grade the directory as an ordinary
246
+ * service and demand `model.likec4`, a spec and a landscape binding for it. The
247
+ * fix is a rename, and it is a rename loam cannot do for you — the id is
248
+ * spelled in the landscape binding, in the frontmatter and in every feature
249
+ * that touched the service — so the message names all three.
250
+ *
251
+ * Emitted from the fleet target (`validate --all`), where the SET of service
252
+ * directories is the thing being checked; a per-service run is already refused
253
+ * by its own `--service` guard before it gets here.
254
+ */
255
+ export function serviceIdFindings(services) {
256
+ return services
257
+ .filter((s) => s.idProblem !== undefined)
258
+ .map((s) => ({
259
+ severity: "error",
260
+ code: "service.id-invalid",
261
+ subject: s.id,
262
+ message: `services/${s.id}/ — ${s.idProblem} ` +
263
+ `Every authoring command refuses this id (\`loam adopt\`, \`loam delta\`, \`loam new --touches\`), so nothing in that directory can be changed through loam. ` +
264
+ `Rename the directory to a legal id, then update its \`service:\` frontmatter, its \`metadata { service '${s.id}' }\` binding in architecture/landscape.likec4, and any features/<FEAT>/specs/${s.id}/ that names it.`,
265
+ }));
266
+ }
267
+ async function readFeature(dir, dirName, archived) {
268
+ const p = featurePaths(dir);
269
+ return {
270
+ id: featureIdFromDirName(dirName),
271
+ dirName,
272
+ dir,
273
+ archived,
274
+ services: await subdirs(p.specsDir),
275
+ has: { intent: existsSync(p.intent), delta: existsSync(p.delta) },
276
+ };
277
+ }
278
+ /**
279
+ * Features in the docs repo, ordered by id (active and archived interleaved when
280
+ * both are asked for). `features/archive/` is the archive, never a feature.
281
+ */
282
+ export async function listFeatures(docsDir, opts = {}, context) {
283
+ if (context !== undefined)
284
+ return context.listFeatures(docsDir, opts);
285
+ // "no-services" is tolerated here on purpose: a docs repo whose services/ is
286
+ // gone is broken, but it is `listServices` (and `loam doctor`) that says so.
287
+ // Refusing here too would turn one diagnosis into two contradictory ones.
288
+ requireDocsRepo(docsDir, ["ok", "no-services"]);
289
+ const root = featuresDir(docsDir);
290
+ const active = (await subdirs(root)).filter((n) => n !== ARCHIVE_DIR);
291
+ const out = await Promise.all(active.map((n) => readFeature(join(root, n), n, false)));
292
+ if (opts.includeArchived) {
293
+ const archiveRoot = join(root, ARCHIVE_DIR);
294
+ const archived = await subdirs(archiveRoot);
295
+ out.push(...(await Promise.all(archived.map((n) => readFeature(join(archiveRoot, n), n, true)))));
296
+ }
297
+ return out.sort((a, b) => compareIds(a.id, b.id) || compareIds(a.dirName, b.dirName));
298
+ }
299
+ /**
300
+ * Resolve a feature argument — a canonical id (`FEAT-5`) or an exact directory
301
+ * name (`FEAT-5-slug`) — to its entry. The entry's `id` is the canonical
302
+ * spelling, and it is the only name a caller may use from here on: tag filters,
303
+ * coherence, and the cross-feature self-exclusion scans all compare against
304
+ * `feature.id`, never against the raw argument.
305
+ *
306
+ * Matching is EXACT, on one of two spellings: the directory name as it is on
307
+ * disk, or the canonical id derived from it. It used to also match any dirName
308
+ * starting with `arg + "-"`, which made the argument a prefix query in
309
+ * disguise: with FEAT-401 and FEAT-402 in flight, `loam archive FEAT` archived
310
+ * whichever sorted first, and `loam delta billing` reached into
311
+ * `billing-7-rewrite`. A destructive command must never pick a target the
312
+ * caller did not name, so a prefix now resolves to nothing at all.
313
+ *
314
+ * An exact directory name still wins over the id spelling (`FEAT-5` the
315
+ * directory over `FEAT-5-slug`, whose id is also `FEAT-5`), and under "include"
316
+ * an active feature wins over an archived one. Ties among equally-exact
317
+ * candidates go to the first by name — deterministic, unlike the raw readdir
318
+ * order this replaces; `ambiguousFeatureMessage` lists them for the caller.
319
+ */
320
+ export async function resolveFeature(docsDir, arg, archived, context) {
321
+ return (await featureCandidates(docsDir, arg, archived, context))[0] ?? null;
322
+ }
323
+ /**
324
+ * Every feature the argument names exactly, best first. Split out of
325
+ * `resolveFeature` so a caller that wants to REFUSE an ambiguous argument can
326
+ * see the tie instead of inheriting the winner.
327
+ */
328
+ export async function featureCandidates(docsDir, arg, archived, context) {
329
+ const all = await listFeatures(docsDir, { includeArchived: archived !== "exclude" }, context);
330
+ return all
331
+ .filter((f) => archived === "include" || f.archived === (archived === "only"))
332
+ .filter((f) => f.dirName === arg || f.id === arg)
333
+ .sort((a, b) => Number(b.dirName === arg) - Number(a.dirName === arg) ||
334
+ Number(a.archived) - Number(b.archived) ||
335
+ compareIds(a.dirName, b.dirName));
336
+ }
337
+ /**
338
+ * The message for an argument that names more than one feature — two directories
339
+ * carrying the same id (`FEAT-6-aaa` and `FEAT-6-zzz`). `resolveFeature` still
340
+ * picks one deterministically, but a command that is about to WRITE should say
341
+ * which directories collided and make the caller spell one out.
342
+ */
343
+ export function ambiguousFeatureMessage(arg, candidates) {
344
+ return (`'${arg}' names ${candidates.length} features: ${candidates.map((c) => c.dirName).join(", ")}. ` +
345
+ "Pass the directory name you mean.");
346
+ }
347
+ /**
348
+ * The message for an exclude-mode miss. When the feature exists under
349
+ * `features/archive/`, "no feature" is a lie the caller would have to debug —
350
+ * the honest answer is "already archived", plus how to look at it. The `--json`
351
+ * code stays `unknown-target` either way: the target is unknown to the command
352
+ * that asked, and the prose carries the diagnosis.
353
+ */
354
+ export async function missingFeatureMessage(docsDir, arg, context) {
355
+ const shipped = await resolveFeature(docsDir, arg, "only", context);
356
+ if (shipped !== null) {
357
+ return (`Feature '${shipped.id}' is already archived (features/archive/${shipped.dirName}) — ` +
358
+ `\`loam show ${shipped.id}\` reads it, \`loam unarchive ${shipped.id}\` re-opens it.`);
359
+ }
360
+ return `No feature '${arg}' under ${featuresDir(docsDir)}.`;
361
+ }
362
+ //# sourceMappingURL=repo.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"repo.js","sourceRoot":"","sources":["../../src/core/repo.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Read model of the docs repo — the single place that knows its layout.\n *\n * Enumeration goes by the filesystem, and only by the filesystem: files are the\n * source of truth, so `services/` IS the list of services and there is no\n * manifest that can disagree with it. Every command that needs \"which services\n * exist\", \"which features are in flight\" or \"where does artifact X live\" asks\n * here, so the layout is spelled exactly once.\n */\nimport { existsSync, statSync, type Dirent } from \"node:fs\";\nimport { readdir } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport { listField, readFrontmatter, stringField } from \"./frontmatter.js\";\nimport type { FleetContext } from \"./fleet-context.js\";\nimport { serviceIdProblem } from \"./ids.js\";\nimport type { Finding } from \"./report.js\";\n\n/** Directory under features/ holding shipped features. Never a feature itself. */\nconst ARCHIVE_DIR = \"archive\";\n\nexport interface ServiceArtifacts {\n model: boolean;\n spec: boolean;\n openapi: boolean;\n runbook: boolean;\n health: boolean;\n}\n\nexport interface ServiceEntry {\n /** Canonical service id — the directory name under services/. */\n id: string;\n /** Absolute path to services/<id>/. */\n dir: string;\n has: ServiceArtifacts;\n /** Number of ADR files under adrs/. */\n adrs: number;\n /** `status` from the living spec's frontmatter; null when nobody has said. */\n status: string | null;\n /**\n * Provenance signals from the living spec's frontmatter: whether it declares\n * any `sources`, and whether a `sources_digest` stamp exists. Presence only —\n * whether the digest still matches the code is `validate`'s question, and it\n * can only be answered from inside the service's own repo.\n */\n sources: { declared: boolean; stamped: boolean };\n /**\n * Why no loam command can author this directory, or absent when the name is a\n * legal service id — `serviceIdProblem`'s own sentence, so the read model and\n * the refusal say the same thing.\n *\n * The enumeration still LISTS it: the directory is there, it holds somebody's\n * documents, and hiding it would repeat the symlink mistake one field up. But\n * every authoring command (`adopt`, `delta`, `rebase`, `new --touches`,\n * `init`) refuses this id with `invalid-option`, so grading it as an ordinary\n * service left the fleet gate demanding fixes no loam command can make. The\n * NAME is the finding — see `serviceIdFindings`.\n */\n idProblem?: string;\n}\n\nexport interface FeatureEntry {\n /** Feature id, derived from the directory name (FEAT-1-split -> FEAT-1). */\n id: string;\n /** The directory name as it is on disk, slug and all. */\n dirName: string;\n /** Absolute path to the feature directory. */\n dir: string;\n archived: boolean;\n /** Services this feature carries a delta for, from specs/<svc>/. */\n services: string[];\n has: { intent: boolean; delta: boolean };\n}\n\n/**\n * Compare ids so digit runs sort numerically: FEAT-2 before FEAT-10.\n * Deterministic and locale-independent — ordering is part of the output\n * contract for `list`, and `--json` consumers diff it.\n */\nexport function compareIds(a: string, b: string): number {\n const ta = tokenize(a);\n const tb = tokenize(b);\n for (let i = 0; i < Math.max(ta.length, tb.length); i += 1) {\n const x = ta[i];\n const y = tb[i];\n if (x === undefined) return -1;\n if (y === undefined) return 1;\n if (typeof x === \"number\" && typeof y === \"number\") {\n if (x !== y) return x - y;\n } else if (String(x) !== String(y)) {\n return String(x) < String(y) ? -1 : 1;\n }\n }\n return 0;\n}\n\nfunction tokenize(s: string): (string | number)[] {\n return (s.match(/\\d+|\\D+/g) ?? []).map((t) => (/^\\d/.test(t) ? Number(t) : t));\n}\n\n/**\n * Feature id from a directory name: everything up to and including the first\n * number run (`FEAT-101-payment-splitting` -> `FEAT-101`). A name with no\n * `<word>-<number>` head is its own id. Quirk: a dated slug keeps only its first\n * segment (`release-2024-01-x` -> `release-2024`) — ids are not meant to be dates.\n */\nexport function featureIdFromDirName(dirName: string): string {\n const m = /^(.*?-\\d+)(?:-|$)/.exec(dirName);\n return m ? m[1]! : dirName;\n}\n\n/* ------------------------------------------------------------------ */\n/* Paths — artifact filenames are spelled here and nowhere else */\n/* ------------------------------------------------------------------ */\n\nexport interface ServicePaths {\n dir: string;\n model: string;\n spec: string;\n archSpec: string;\n openapi: string;\n runbook: string;\n health: string;\n adrsDir: string;\n}\n\nexport function servicePaths(docsDir: string, service: string): ServicePaths {\n const dir = join(docsDir, \"services\", service);\n return {\n dir,\n model: join(dir, \"model.likec4\"),\n spec: join(dir, \"spec.md\"),\n archSpec: join(dir, \"arch.spec.md\"),\n openapi: join(dir, \"openapi.yaml\"),\n runbook: join(dir, \"runbook.md\"),\n health: join(dir, \"health.yaml\"),\n adrsDir: join(dir, \"adrs\"),\n };\n}\n\n/**\n * The pair of requirement-carrying spec files, living and delta alike: the\n * business spec and the architecture spec. Same grammar, same delta algebra,\n * same merge — everything that walks \"the spec files\" of a service walks this\n * list, so the two axes cannot drift apart in what handles them. `key` indexes\n * ServicePaths/FeatureSpecPaths; `label` is how the axis names itself in prose.\n */\nexport const SPEC_AXES = [\n { key: \"spec\", file: \"spec.md\", label: \"requirements\" },\n { key: \"archSpec\", file: \"arch.spec.md\", label: \"arch requirements\" },\n] as const;\nexport type SpecAxis = (typeof SPEC_AXES)[number];\n\nexport interface FeaturePaths {\n dir: string;\n intent: string;\n delta: string;\n specsDir: string;\n adrsDir: string;\n}\n\nexport function featurePaths(featureDir: string): FeaturePaths {\n return {\n dir: featureDir,\n intent: join(featureDir, \"intent.md\"),\n delta: join(featureDir, \"delta.likec4\"),\n specsDir: join(featureDir, \"specs\"),\n adrsDir: join(featureDir, \"adrs\"),\n };\n}\n\nexport interface FeatureSpecPaths {\n dir: string;\n spec: string;\n archSpec: string;\n openapi: string;\n}\n\nexport function featureSpecPaths(featureDir: string, service: string): FeatureSpecPaths {\n const dir = join(featureDir, \"specs\", service);\n return {\n dir,\n spec: join(dir, \"spec.md\"),\n archSpec: join(dir, \"arch.spec.md\"),\n openapi: join(dir, \"openapi.yaml\"),\n };\n}\n\nexport function landscapePath(docsDir: string): string {\n return join(docsDir, \"architecture\", \"landscape.likec4\");\n}\n\nexport function featuresDir(docsDir: string): string {\n return join(docsDir, \"features\");\n}\n\n/* ------------------------------------------------------------------ */\n/* Is there a docs repo there at all? */\n/* ------------------------------------------------------------------ */\n\n/**\n * What is actually at `docsDir`. Three answers, because they point at three\n * different fixes and the enumeration below used to give all of them the same\n * one — an empty list:\n *\n * - `missing` — nothing there (or not a directory): `docsDir` in loam.json\n * is wrong, or the docs repo was never cloned;\n * - `no-services` — a directory, but with no `services/`: it is some other\n * directory, most often the service repo itself after a typo;\n * - `ok` — a docs repo. `services/` may still be EMPTY, and that is a\n * legitimate state: a docs repo before the first `loam adopt`.\n *\n * \"Empty fleet\" and \"wrong path\" are the same output only if nobody asks this\n * question, and a green `loam list` over a docsDir that does not exist is worse\n * than a red one: it says the fleet is fine.\n */\nexport type DocsRepoKind = \"missing\" | \"no-services\" | \"ok\";\n\nexport interface DocsRepoState {\n kind: DocsRepoKind;\n /** The path examined, so a caller can quote it without re-deriving it. */\n path: string;\n}\n\nexport function docsRepoState(docsDir: string): DocsRepoState {\n const path = docsDir;\n let isDir = false;\n try {\n isDir = statSync(path).isDirectory();\n } catch {\n isDir = false;\n }\n if (!isDir) return { kind: \"missing\", path };\n return { kind: existsSync(join(path, \"services\")) ? \"ok\" : \"no-services\", path };\n}\n\n/**\n * The docs repo is not there. Thrown rather than returned because every caller\n * of the enumeration wants the same thing — to stop — and a `[]` return is the\n * bug this closes. `message` is written for a human at a terminal: the CLI's\n * top-level handler prints it verbatim, and commands that catch it report\n * `repository-unavailable`.\n */\nexport class DocsRepoUnavailableError extends Error {\n constructor(\n readonly state: DocsRepoState,\n message: string,\n ) {\n super(message);\n this.name = \"DocsRepoUnavailableError\";\n }\n}\n\n/**\n * Refuse to enumerate a docs repo that is not one. `allow` names the states the\n * caller can honestly survive: features/ may be absent from a real docs repo\n * (nothing is in flight yet), so feature enumeration only insists the docs repo\n * EXISTS, while service enumeration insists it is shaped like a docs repo — a\n * `services/` directory is what makes it one.\n */\nfunction requireDocsRepo(docsDir: string, allow: DocsRepoKind[]): void {\n const state = docsRepoState(docsDir);\n if (allow.includes(state.kind)) return;\n if (state.kind === \"missing\") {\n throw new DocsRepoUnavailableError(\n state,\n `The configured docs repo does not exist: ${state.path}. ` +\n \"Fix `docsDir` in loam.json, clone the docs repo there, \" +\n \"or run `loam init --docs <dir> --create` to make a new one.\",\n );\n }\n throw new DocsRepoUnavailableError(\n state,\n `${state.path} is not a docs repo — it has no services/ directory. ` +\n \"Point `docsDir` in loam.json at the shared docs repo, \" +\n \"or run `loam init --docs <dir> --create` to make one.\",\n );\n}\n\n/* ------------------------------------------------------------------ */\n/* Enumeration */\n/* ------------------------------------------------------------------ */\n\n/**\n * What a directory entry IS, following a symlink to find out.\n *\n * `readdir(withFileTypes)` does not follow symlinks: for a symlink `Dirent`\n * every `isDirectory()`/`isFile()` is false, whatever it points at. That made a\n * symlinked `services/<id>/` or `features/<id>/` vanish from every enumeration\n * at once — `list`, `validate --all`, the fleet gate — with no diagnostic\n * anywhere, which is the one outcome a shared docs repo cannot survive: the\n * fleet gate reported a service that is not there as fine because it never saw\n * it. Composing a docs repo out of symlinks (a worktree, a submodule, one\n * service's directory shared between two checkouts) is legitimate, so they are\n * FOLLOWED rather than refused. A dangling link stats as nothing and is skipped\n * exactly as an absent directory is — it is not a service, and saying so would\n * mean inventing an entry to hang the complaint on.\n */\nfunction entryIs(dir: string, e: Dirent, want: \"dir\" | \"file\"): boolean {\n if (!e.isSymbolicLink()) return want === \"dir\" ? e.isDirectory() : e.isFile();\n try {\n const s = statSync(join(dir, e.name));\n return want === \"dir\" ? s.isDirectory() : s.isFile();\n } catch {\n return false;\n }\n}\n\n/**\n * Sorted names of the real subdirectories of `dir` (dot-dirs and files skipped).\n * A missing `dir` is an empty list — the swallow is deliberate but narrow: it\n * only ever runs INSIDE a docs repo that `requireDocsRepo` has already\n * confirmed, where an absent `features/` or `specs/` means \"none yet\" and\n * nothing else.\n */\nasync function subdirs(dir: string): Promise<string[]> {\n if (!existsSync(dir)) return [];\n const entries = await readdir(dir, { withFileTypes: true });\n return entries\n .filter((e) => !e.name.startsWith(\".\") && entryIs(dir, e, \"dir\"))\n .map((e) => e.name)\n .sort(compareIds);\n}\n\nasync function countMarkdown(dir: string): Promise<number> {\n if (!existsSync(dir)) return 0;\n const entries = await readdir(dir, { withFileTypes: true });\n return entries.filter((e) => e.name.endsWith(\".md\") && entryIs(dir, e, \"file\")).length;\n}\n\n/** Services a feature carries a delta for, ordered — the subdirs of its specs/. */\nexport async function featureSpecServices(\n featureDir: string,\n context?: FleetContext,\n): Promise<string[]> {\n if (context !== undefined) return context.featureSpecServices(featureDir);\n return subdirs(featurePaths(featureDir).specsDir);\n}\n\n/** Every service in the docs repo, ordered by id. */\nexport async function listServices(docsDir: string, context?: FleetContext): Promise<ServiceEntry[]> {\n if (context !== undefined) return context.listServices(docsDir);\n requireDocsRepo(docsDir, [\"ok\"]);\n const names = await subdirs(join(docsDir, \"services\"));\n return Promise.all(\n names.map(async (id): Promise<ServiceEntry> => {\n const p = servicePaths(docsDir, id);\n const fm = await readFrontmatter(p.spec);\n const idProblem = serviceIdProblem(id, \"directory name\");\n return {\n id,\n dir: p.dir,\n ...(idProblem === null ? {} : { idProblem }),\n has: {\n model: existsSync(p.model),\n spec: existsSync(p.spec),\n openapi: existsSync(p.openapi),\n runbook: existsSync(p.runbook),\n health: existsSync(p.health),\n },\n adrs: await countMarkdown(p.adrsDir),\n status: stringField(fm, \"status\") ?? null,\n sources: {\n declared: listField(fm, \"sources\").length > 0,\n stamped: stringField(fm, \"sources_digest\") !== undefined,\n },\n };\n }),\n );\n}\n\n/**\n * The directories under `services/` that no loam command can address.\n *\n * A finding rather than a silent skip, and an ERROR rather than a warning,\n * because the state is not survivable in either direction: `loam adopt`,\n * `loam delta`, `loam rebase` and `loam new --touches` all refuse the id with\n * `invalid-option`, so the documents in there can never be updated by loam\n * again, while `validate --all` used to grade the directory as an ordinary\n * service and demand `model.likec4`, a spec and a landscape binding for it. The\n * fix is a rename, and it is a rename loam cannot do for you — the id is\n * spelled in the landscape binding, in the frontmatter and in every feature\n * that touched the service — so the message names all three.\n *\n * Emitted from the fleet target (`validate --all`), where the SET of service\n * directories is the thing being checked; a per-service run is already refused\n * by its own `--service` guard before it gets here.\n */\nexport function serviceIdFindings(services: ServiceEntry[]): Finding[] {\n return services\n .filter((s) => s.idProblem !== undefined)\n .map((s) => ({\n severity: \"error\" as const,\n code: \"service.id-invalid\",\n subject: s.id,\n message:\n `services/${s.id}/ — ${s.idProblem} ` +\n `Every authoring command refuses this id (\\`loam adopt\\`, \\`loam delta\\`, \\`loam new --touches\\`), so nothing in that directory can be changed through loam. ` +\n `Rename the directory to a legal id, then update its \\`service:\\` frontmatter, its \\`metadata { service '${s.id}' }\\` binding in architecture/landscape.likec4, and any features/<FEAT>/specs/${s.id}/ that names it.`,\n }));\n}\n\nasync function readFeature(dir: string, dirName: string, archived: boolean): Promise<FeatureEntry> {\n const p = featurePaths(dir);\n return {\n id: featureIdFromDirName(dirName),\n dirName,\n dir,\n archived,\n services: await subdirs(p.specsDir),\n has: { intent: existsSync(p.intent), delta: existsSync(p.delta) },\n };\n}\n\n/**\n * Features in the docs repo, ordered by id (active and archived interleaved when\n * both are asked for). `features/archive/` is the archive, never a feature.\n */\nexport async function listFeatures(\n docsDir: string,\n opts: { includeArchived?: boolean } = {},\n context?: FleetContext,\n): Promise<FeatureEntry[]> {\n if (context !== undefined) return context.listFeatures(docsDir, opts);\n // \"no-services\" is tolerated here on purpose: a docs repo whose services/ is\n // gone is broken, but it is `listServices` (and `loam doctor`) that says so.\n // Refusing here too would turn one diagnosis into two contradictory ones.\n requireDocsRepo(docsDir, [\"ok\", \"no-services\"]);\n const root = featuresDir(docsDir);\n const active = (await subdirs(root)).filter((n) => n !== ARCHIVE_DIR);\n const out = await Promise.all(active.map((n) => readFeature(join(root, n), n, false)));\n\n if (opts.includeArchived) {\n const archiveRoot = join(root, ARCHIVE_DIR);\n const archived = await subdirs(archiveRoot);\n out.push(...(await Promise.all(archived.map((n) => readFeature(join(archiveRoot, n), n, true)))));\n }\n\n return out.sort((a, b) => compareIds(a.id, b.id) || compareIds(a.dirName, b.dirName));\n}\n\n/**\n * Which side of `features/archive/` a lookup may land on. The policy is an\n * argument, not a default, so every command's stance on shipped features is\n * visible at its call site: delta/validate/archive work on in-flight features\n * (\"exclude\"), show/verify/new read shipped ones too (\"include\"), unarchive\n * takes back nothing else (\"only\").\n */\nexport type ArchivedPolicy = \"exclude\" | \"include\" | \"only\";\n\n/**\n * Resolve a feature argument — a canonical id (`FEAT-5`) or an exact directory\n * name (`FEAT-5-slug`) — to its entry. The entry's `id` is the canonical\n * spelling, and it is the only name a caller may use from here on: tag filters,\n * coherence, and the cross-feature self-exclusion scans all compare against\n * `feature.id`, never against the raw argument.\n *\n * Matching is EXACT, on one of two spellings: the directory name as it is on\n * disk, or the canonical id derived from it. It used to also match any dirName\n * starting with `arg + \"-\"`, which made the argument a prefix query in\n * disguise: with FEAT-401 and FEAT-402 in flight, `loam archive FEAT` archived\n * whichever sorted first, and `loam delta billing` reached into\n * `billing-7-rewrite`. A destructive command must never pick a target the\n * caller did not name, so a prefix now resolves to nothing at all.\n *\n * An exact directory name still wins over the id spelling (`FEAT-5` the\n * directory over `FEAT-5-slug`, whose id is also `FEAT-5`), and under \"include\"\n * an active feature wins over an archived one. Ties among equally-exact\n * candidates go to the first by name — deterministic, unlike the raw readdir\n * order this replaces; `ambiguousFeatureMessage` lists them for the caller.\n */\nexport async function resolveFeature(\n docsDir: string,\n arg: string,\n archived: ArchivedPolicy,\n context?: FleetContext,\n): Promise<FeatureEntry | null> {\n return (await featureCandidates(docsDir, arg, archived, context))[0] ?? null;\n}\n\n/**\n * Every feature the argument names exactly, best first. Split out of\n * `resolveFeature` so a caller that wants to REFUSE an ambiguous argument can\n * see the tie instead of inheriting the winner.\n */\nexport async function featureCandidates(\n docsDir: string,\n arg: string,\n archived: ArchivedPolicy,\n context?: FleetContext,\n): Promise<FeatureEntry[]> {\n const all = await listFeatures(docsDir, { includeArchived: archived !== \"exclude\" }, context);\n return all\n .filter((f) => archived === \"include\" || f.archived === (archived === \"only\"))\n .filter((f) => f.dirName === arg || f.id === arg)\n .sort(\n (a, b) =>\n Number(b.dirName === arg) - Number(a.dirName === arg) ||\n Number(a.archived) - Number(b.archived) ||\n compareIds(a.dirName, b.dirName),\n );\n}\n\n/**\n * The message for an argument that names more than one feature — two directories\n * carrying the same id (`FEAT-6-aaa` and `FEAT-6-zzz`). `resolveFeature` still\n * picks one deterministically, but a command that is about to WRITE should say\n * which directories collided and make the caller spell one out.\n */\nexport function ambiguousFeatureMessage(arg: string, candidates: FeatureEntry[]): string {\n return (\n `'${arg}' names ${candidates.length} features: ${candidates.map((c) => c.dirName).join(\", \")}. ` +\n \"Pass the directory name you mean.\"\n );\n}\n\n/**\n * The message for an exclude-mode miss. When the feature exists under\n * `features/archive/`, \"no feature\" is a lie the caller would have to debug —\n * the honest answer is \"already archived\", plus how to look at it. The `--json`\n * code stays `unknown-target` either way: the target is unknown to the command\n * that asked, and the prose carries the diagnosis.\n */\nexport async function missingFeatureMessage(\n docsDir: string,\n arg: string,\n context?: FleetContext,\n): Promise<string> {\n const shipped = await resolveFeature(docsDir, arg, \"only\", context);\n if (shipped !== null) {\n return (\n `Feature '${shipped.id}' is already archived (features/archive/${shipped.dirName}) — ` +\n `\\`loam show ${shipped.id}\\` reads it, \\`loam unarchive ${shipped.id}\\` re-opens it.`\n );\n }\n return `No feature '${arg}' under ${featuresDir(docsDir)}.`;\n}\n"]}
@@ -0,0 +1,38 @@
1
+ /**
2
+ * The validation report — what `loam validate` produces before anything is
3
+ * printed.
4
+ *
5
+ * Checks build findings; renderers turn them into a human view or into the JSON
6
+ * contract. Keeping the two apart is what makes `--all` and `--json` possible
7
+ * without a second implementation of every check, and it is why a finding
8
+ * carries a stable `code`: prose is for people, codes are for callers.
9
+ */
10
+ /** A target is valid when nothing in it is an error. Warnings never gate. */
11
+ export function targetValid(target) {
12
+ return !target.findings.some((f) => f.severity === "error");
13
+ }
14
+ export function reportValid(targets) {
15
+ return targets.every(targetValid);
16
+ }
17
+ export function countSeverity(targets, severity) {
18
+ return targets.reduce((n, t) => n + t.findings.filter((f) => f.severity === severity).length, 0);
19
+ }
20
+ export function findingJson(f) {
21
+ return {
22
+ severity: f.severity,
23
+ code: f.code,
24
+ ...(f.subject === undefined ? {} : { subject: f.subject }),
25
+ ...(f.gates === undefined ? {} : { gates: f.gates }),
26
+ message: f.message,
27
+ details: f.details ?? [],
28
+ };
29
+ }
30
+ export function targetJson(t) {
31
+ return {
32
+ kind: t.kind,
33
+ id: t.id,
34
+ valid: targetValid(t),
35
+ findings: t.findings.map(findingJson),
36
+ };
37
+ }
38
+ //# sourceMappingURL=report.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"report.js","sourceRoot":"","sources":["../../src/core/report.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAgDH,6EAA6E;AAC7E,MAAM,UAAU,WAAW,CAAC,MAAoB;IAC9C,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,KAAK,OAAO,CAAC,CAAC;AAC9D,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,OAAuB;IACjD,OAAO,OAAO,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC;AACpC,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,OAAuB,EAAE,QAAkB;IACvE,OAAO,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,KAAK,QAAQ,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC;AACnG,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,CAAU;IACpC,OAAO;QACL,QAAQ,EAAE,CAAC,CAAC,QAAQ;QACpB,IAAI,EAAE,CAAC,CAAC,IAAI;QACZ,GAAG,CAAC,CAAC,CAAC,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC;QAC1D,GAAG,CAAC,CAAC,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,KAAK,EAAE,CAAC;QACpD,OAAO,EAAE,CAAC,CAAC,OAAO;QAClB,OAAO,EAAE,CAAC,CAAC,OAAO,IAAI,EAAE;KACzB,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,UAAU,CAAC,CAAe;IACxC,OAAO;QACL,IAAI,EAAE,CAAC,CAAC,IAAI;QACZ,EAAE,EAAE,CAAC,CAAC,EAAE;QACR,KAAK,EAAE,WAAW,CAAC,CAAC,CAAC;QACrB,QAAQ,EAAE,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,WAAW,CAAC;KACtC,CAAC;AACJ,CAAC","sourcesContent":["/**\n * The validation report — what `loam validate` produces before anything is\n * printed.\n *\n * Checks build findings; renderers turn them into a human view or into the JSON\n * contract. Keeping the two apart is what makes `--all` and `--json` possible\n * without a second implementation of every check, and it is why a finding\n * carries a stable `code`: prose is for people, codes are for callers.\n */\n\nexport type Severity = \"ok\" | \"warn\" | \"error\";\n\n/** How the text renderer should lay a finding out. The JSON contract ignores this. */\nexport interface TextHint {\n /** Leading spaces. Default 0. */\n indent?: number;\n /** Print the ✓/⚠/✗ marker. Default true. */\n marker?: boolean;\n /** Block header printed once, before the first finding that carries it. */\n header?: string;\n /** Prefix for each detail line (e.g. \"- \" for a list of names). */\n detailPrefix?: string;\n}\n\nexport interface Finding {\n severity: Severity;\n /** Stable machine identifier — prose may change, this may not. */\n code: string;\n message: string;\n /**\n * What the finding is about, when that is narrower than the target: the\n * service a feature's per-service check ran on, say. Without it a caller\n * would have to read the service name back out of the message.\n */\n subject?: string;\n details?: string[];\n /**\n * Coherence findings only: whether `loam archive` refuses on this issue\n * without `--approve` (issue.ts explains why severity alone cannot say).\n * Absent on findings that never face the archive gate.\n */\n gates?: boolean;\n text?: TextHint;\n}\n\nexport interface TargetReport {\n /**\n * What was validated. `landscape` is the fleet itself — the checks that need\n * every service and every element in view at once, so they belong to no single\n * service and only run under `--all`.\n */\n kind: \"service\" | \"feature\" | \"landscape\";\n id: string;\n findings: Finding[];\n}\n\n/** A target is valid when nothing in it is an error. Warnings never gate. */\nexport function targetValid(target: TargetReport): boolean {\n return !target.findings.some((f) => f.severity === \"error\");\n}\n\nexport function reportValid(targets: TargetReport[]): boolean {\n return targets.every(targetValid);\n}\n\nexport function countSeverity(targets: TargetReport[], severity: Severity): number {\n return targets.reduce((n, t) => n + t.findings.filter((f) => f.severity === severity).length, 0);\n}\n\nexport function findingJson(f: Finding): Record<string, unknown> {\n return {\n severity: f.severity,\n code: f.code,\n ...(f.subject === undefined ? {} : { subject: f.subject }),\n ...(f.gates === undefined ? {} : { gates: f.gates }),\n message: f.message,\n details: f.details ?? [],\n };\n}\n\nexport function targetJson(t: TargetReport): Record<string, unknown> {\n return {\n kind: t.kind,\n id: t.id,\n valid: targetValid(t),\n findings: t.findings.map(findingJson),\n };\n}\n"]}