@ybotok/loam 0.2.0-alpha.1 → 0.2.0-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/CHANGELOG.md +87 -0
  2. package/README.md +6 -6
  3. package/ROADMAP.md +123 -18
  4. package/dist/commands/archive/plan/gate.js +18 -12
  5. package/dist/commands/archive/plan/gate.js.map +1 -1
  6. package/dist/commands/archive/plan/landscape.js +19 -0
  7. package/dist/commands/archive/plan/landscape.js.map +1 -1
  8. package/dist/commands/archive/run.js +2 -1
  9. package/dist/commands/archive/run.js.map +1 -1
  10. package/dist/commands/validate/arch-coverage.js +11 -1
  11. package/dist/commands/validate/arch-coverage.js.map +1 -1
  12. package/dist/commands/validate/fleet/obligations.js +43 -9
  13. package/dist/commands/validate/fleet/obligations.js.map +1 -1
  14. package/dist/commands/validate/fleet/usecases/steps.js +3 -3
  15. package/dist/commands/validate/fleet/usecases/steps.js.map +1 -1
  16. package/dist/commands/validate/service/service.js +1 -0
  17. package/dist/commands/validate/service/service.js.map +1 -1
  18. package/dist/commands/validate/service/specs.js +33 -1
  19. package/dist/commands/validate/service/specs.js.map +1 -1
  20. package/dist/core/agent/workflows/check.js +3 -0
  21. package/dist/core/agent/workflows/check.js.map +1 -1
  22. package/dist/core/brief/unchecked.js +1 -0
  23. package/dist/core/brief/unchecked.js.map +1 -1
  24. package/dist/core/c4/arch.js +72 -180
  25. package/dist/core/c4/arch.js.map +1 -1
  26. package/dist/core/c4/likec4.js +2 -0
  27. package/dist/core/c4/likec4.js.map +1 -1
  28. package/dist/core/c4/parsed/deployment.js +158 -0
  29. package/dist/core/c4/parsed/deployment.js.map +1 -0
  30. package/dist/core/c4/project/load.js +4 -0
  31. package/dist/core/c4/project/load.js.map +1 -1
  32. package/dist/core/c4/project/staged.js +220 -0
  33. package/dist/core/c4/project/staged.js.map +1 -0
  34. package/dist/core/c4/resolve/service.js +46 -0
  35. package/dist/core/c4/resolve/service.js.map +1 -1
  36. package/dist/core/c4/resolve/steps.js +144 -0
  37. package/dist/core/c4/resolve/steps.js.map +1 -0
  38. package/dist/core/c4/splice/delta-blocks.js +15 -2
  39. package/dist/core/c4/splice/delta-blocks.js.map +1 -1
  40. package/dist/core/c4/workspace.js +2 -0
  41. package/dist/core/c4/workspace.js.map +1 -1
  42. package/dist/core/coherence/coherence.js +36 -0
  43. package/dist/core/coherence/coherence.js.map +1 -1
  44. package/dist/core/deployment/delta.js +188 -0
  45. package/dist/core/deployment/delta.js.map +1 -0
  46. package/dist/core/deployment/objects.js +145 -0
  47. package/dist/core/deployment/objects.js.map +1 -0
  48. package/dist/core/pack/living.js +19 -0
  49. package/dist/core/pack/living.js.map +1 -1
  50. package/dist/core/repo/authored/paths.js +18 -0
  51. package/dist/core/repo/authored/paths.js.map +1 -1
  52. package/dist/core/repo/paths.js +43 -2
  53. package/dist/core/repo/paths.js.map +1 -1
  54. package/dist/core/status/feature/artifacts.js +29 -4
  55. package/dist/core/status/feature/artifacts.js.map +1 -1
  56. package/dist/core/status/fleet/fleet.js +11 -1
  57. package/dist/core/status/fleet/fleet.js.map +1 -1
  58. package/dist/core/usecases/delta/overlay.js +28 -191
  59. package/dist/core/usecases/delta/overlay.js.map +1 -1
  60. package/dist/core/usecases/fleet.js.map +1 -1
  61. package/dist/core/usecases/operations.js +2 -2
  62. package/dist/core/usecases/operations.js.map +1 -1
  63. package/dist/core/vocabulary/issue.js +3 -0
  64. package/dist/core/vocabulary/issue.js.map +1 -1
  65. package/package.json +1 -1
@@ -140,7 +140,7 @@ export function featureSpecPaths(featureDir, service) {
140
140
  };
141
141
  }
142
142
  export function landscapePath(docsDir) {
143
- return join(docsDir, "architecture", "landscape.likec4");
143
+ return join(docsDir, "architecture", LANDSCAPE_FILE);
144
144
  }
145
145
  /** The fleet's authorization vocabulary — beside the fleet map, for the same reason. */
146
146
  export function permissionsPath(docsDir) {
@@ -190,8 +190,49 @@ export function fleetAdrsDir(docsDir) {
190
190
  * ever parses the file, and staleness against the tree is a byte compare.
191
191
  */
192
192
  export function subsystemViewsPath(docsDir) {
193
- return join(docsDir, "architecture", "subsystems.likec4");
193
+ return join(docsDir, "architecture", SUBSYSTEM_VIEWS_FILE);
194
194
  }
195
+ /** The generated views' file name, spelled once because the reservation below reads it too. */
196
+ const SUBSYSTEM_VIEWS_FILE = "subsystems.likec4";
197
+ /**
198
+ * Names inside `architecture/` that LOAM owns, and what goes wrong if a
199
+ * feature's own document claims one.
200
+ *
201
+ * `features/<FEAT>/deployment/<name>.likec4` merges as a whole-file copy
202
+ * straight into `architecture/` — unlike a flow, which lands under
203
+ * `usecases/` — so its `<name>` is free to be any of these, and two of them
204
+ * fail in ways nothing downstream would report:
205
+ *
206
+ * - `subsystems.likec4` is GENERATED and is EXCLUDED from every project load
207
+ * (docs/DESIGN.md rule 26 keeps it a byte compare), so topology landing
208
+ * there is invisible to loam from the moment it archives — and the next
209
+ * `loam subsystem sync` overwrites the file wholesale. Silent both ways,
210
+ * which is the worst shape a failure can have;
211
+ * - `landscape.likec4` is the living map the archive SPLICES; a whole-file
212
+ * copy over it would discard the fleet;
213
+ * - `usecases/` is the flow tree, walked by the use-case scan. A deployment
214
+ * document filed there parses (one project) and is then looked for by a
215
+ * reader asking a different question.
216
+ *
217
+ * Returns the reason, or null when the name is the author's to choose. Takes
218
+ * the `/`-separated rel a directory walk produced, never a string from argv.
219
+ */
220
+ export function reservedArchitectureName(rel) {
221
+ const parts = rel.split("/");
222
+ if (parts[0] === USECASE_DIR) {
223
+ return `\`${USECASE_DIR}/\` is the use-case tree — a flow belongs in \`features/<FEAT>/usecases/\`, which merges there`;
224
+ }
225
+ if (rel === SUBSYSTEM_VIEWS_FILE) {
226
+ return "`subsystems.likec4` is generated by `loam subsystem sync`, which overwrites it wholesale — and nothing in loam ever parses it, so anything landing there is invisible from the moment it archives";
227
+ }
228
+ if (rel === LANDSCAPE_FILE) {
229
+ return "`landscape.likec4` is the living fleet map, which the archive SPLICES into rather than replaces";
230
+ }
231
+ return null;
232
+ }
233
+ /** The living map's file name, and the flow tree's directory — spelled once, read by the reservation. */
234
+ const LANDSCAPE_FILE = "landscape.likec4";
235
+ const USECASE_DIR = "usecases";
195
236
  export function featuresDir(docsDir) {
196
237
  return join(docsDir, "features");
197
238
  }
@@ -1 +1 @@
1
- {"version":3,"file":"paths.js","sourceRoot":"","sources":["../../../src/core/repo/paths.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Where every artifact lives. Artifact filenames are spelled here and nowhere\n * else, so a rename is one edit rather than a grep.\n *\n * This is also the module that carries loam's one path guarantee.\n * `unfiledServicePaths(docsDir, service)` spells `<docsDir>/services/<service>/`,\n * so both parts are caller-controlled path input, and `node:path` cannot help:\n * `join(...paths: string[])` accepts every string there is. The guarantee IS\n * the parameter brands and nothing else — `PathableService` for the id (a name\n * whose provenance is the repository, unconstructible from document text),\n * `DocsDir`/`FeatureDir` for the roots (a resolved config or validated\n * `--docs`; a directory an enumeration read), and `ServiceDir` for\n * `servicePathsAt` (the enumeration's own resolved directory, narrower still)\n * — the `kernel/ids/` package holds the only casts, so an unchecked value at\n * these call sites does not compile.\n * Code that spells `services/<id>/` or a feature layout with a bare join is\n * outside the guarantee — `commands/new.ts` and the openspec migrator both do,\n * held instead by `resolveInside` at the write.\n */\nimport { join } from \"node:path\";\nimport { serviceDirOf, type DocsDir, type FeatureDir, type ServiceDir } from \"../kernel/ids/dirs.js\";\nimport type { PathableService } from \"../kernel/ids/service.js\";\nimport type { SubsystemName } from \"../kernel/ids/subsystem.js\";\n\n/** Directory under features/ holding shipped features. Never a feature itself. */\nexport const ARCHIVE_DIR = \"archive\";\n\n/* ------------------------------------------------------------------ */\n/* Paths — artifact filenames are spelled here and nowhere else */\n/* ------------------------------------------------------------------ */\n\n/**\n * The living artifact filenames, as one table, because three questions read\n * them: `servicePathsAt` below spells the paths, the tree walk classifies\n * a directory as a service by whether any of these names sit in it, and\n * `core/diff/base-state.ts` classifies the SAME way over a base git ref's\n * tree listing (exported for that reader — a filename respelled there would\n * be the drift this table exists to prevent). The walk cannot import this\n * module — `repo → repo/tree` is the package direction, and an edge back\n * would be the cycle `scripts/package-graph.mjs` refuses — so `repo.ts`\n * injects `isServiceArtifactName` into the walk request instead.\n */\nexport const ARTIFACT_FILES = {\n model: \"model.likec4\",\n spec: \"spec.md\",\n archSpec: \"arch.spec.md\",\n openapi: \"openapi.yaml\",\n asyncapi: \"asyncapi.yaml\",\n runbook: \"runbook.md\",\n health: \"health.yaml\",\n steps: \"steps.yaml\",\n} as const;\nconst ADRS_DIR = \"adrs\";\n\n/** Does a directory entry name a living service artifact? The classification half of the table above. */\nexport function isServiceArtifactName(name: string, kind: \"file\" | \"dir\"): boolean {\n if (kind === \"dir\") return name === ADRS_DIR;\n return Object.values(ARTIFACT_FILES).some((file) => file === name);\n}\n\nexport interface ServicePaths {\n dir: ServiceDir;\n model: string;\n spec: string;\n archSpec: string;\n openapi: string;\n /** The async contract — the event axis's sibling of `openapi`. */\n asyncapi: string;\n runbook: string;\n health: string;\n /**\n * The step catalogue — which phrases this service's suite has DECIDED to\n * define. Authored and opt-in; its CONTENTS are read by `loam steps` alone and\n * nothing in `validate` consults them\n * (`core/gherkin/steps/catalogue.ts` records why).\n *\n * Its EXISTENCE is read more widely, and that is not the same statement: the\n * filename is in `ARTIFACT_FILES`, so `isServiceArtifactName` classifies it\n * and the tree walk treats a directory holding one as a service. That is\n * correct — it is a service artifact — and it is worth writing down, because\n * it means a `steps.yaml` written at the wrong path mints a service.\n */\n steps: string;\n adrsDir: string;\n}\n\n/**\n * The artifact paths of a service whose directory is already KNOWN — the\n * enumeration's `dir`, at whatever depth the tree walk found it. This is the\n * spelling every reader of an existing service must use: joining\n * `services/<id>/` at the root is only true for a fleet nobody has filed, and\n * a moved service would silently grade as absent through it.\n */\nexport function servicePathsAt(dir: ServiceDir): ServicePaths {\n return {\n dir,\n model: join(dir, ARTIFACT_FILES.model),\n spec: join(dir, ARTIFACT_FILES.spec),\n archSpec: join(dir, ARTIFACT_FILES.archSpec),\n openapi: join(dir, ARTIFACT_FILES.openapi),\n asyncapi: join(dir, ARTIFACT_FILES.asyncapi),\n runbook: join(dir, ARTIFACT_FILES.runbook),\n health: join(dir, ARTIFACT_FILES.health),\n steps: join(dir, ARTIFACT_FILES.steps),\n adrsDir: join(dir, ADRS_DIR),\n };\n}\n\n/**\n * The UNFILED spelling — `<docsDir>/services/<id>/`, the tree's root level.\n * This is where creation lands (`adopt`'s brief, a new service materialised by\n * `archive`, the OpenSpec migration) and the honest fallback for a service the\n * enumeration does not answer; for a service that EXISTS, resolve through the\n * enumeration instead (`servicePathsAt(entry.dir)`, or\n * `locateServicePaths` in `service-target.ts`, which chooses between the two).\n * Renamed from `servicePaths` when the tree landed, precisely so the compiler\n * would put every remaining caller in front of a reviewer: a root join that\n * survives unreviewed is a moved service silently grading as absent.\n */\nexport function unfiledServicePaths(docsDir: DocsDir, service: PathableService): ServicePaths {\n return servicePathsAt(serviceDirOf(join(docsDir, \"services\", service)));\n}\n\n/**\n * The creation spelling INSIDE a subsystem — `<subsystemDir>/<id>/`, the\n * target `adopt --subsystem` briefs so an adoption need not land unfiled and\n * cost a second command. The same two provenances the unfiled spelling\n * combines, one level down: the subsystem directory came off the tree walk's\n * readdir, and the id passed the service grammar before it reached here.\n */\nexport function servicePathsUnder(subsystemDir: string, service: PathableService): ServicePaths {\n return servicePathsAt(serviceDirOf(join(subsystemDir, service)));\n}\n\n/**\n * The directory a SUBSYSTEM occupies (or will occupy) under a parent — the\n * spelling `subsystem new` creates and `subsystem rename` renames into. The\n * same guarantee-by-brand as the service spellings above: the parent came off\n * the tree walk's readdir (or is `servicesDir` itself), and demanding\n * `SubsystemName` here is what makes the validator unskippable — a raw argv\n * string at this call site does not compile, so no future verb can hoist the\n * join above the check that earns it.\n */\nexport function subsystemPathUnder(parentDir: string, name: SubsystemName): string {\n return join(parentDir, name);\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: FeatureDir): 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 /** The async contract delta — the event axis's sibling of `openapi`. */\n asyncapi: string;\n}\n\nexport function featureSpecPaths(featureDir: FeatureDir, service: PathableService): 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 asyncapi: join(dir, \"asyncapi.yaml\"),\n };\n}\n\nexport function landscapePath(docsDir: DocsDir): string {\n return join(docsDir, \"architecture\", \"landscape.likec4\");\n}\n\n/** The fleet's authorization vocabulary — beside the fleet map, for the same reason. */\nexport function permissionsPath(docsDir: DocsDir): string {\n return join(docsDir, \"architecture\", \"permissions.yaml\");\n}\n\n/** The fleet's capability vocabulary — beside the other two fleet documents, for the same reason. */\nexport function capabilitiesPath(docsDir: DocsDir): string {\n return join(docsDir, \"architecture\", \"capabilities.yaml\");\n}\n\n/**\n * The fleet's ARCHITECTURAL OBLIGATION vocabulary — beside the landscape it tags\n * and the two vocabularies it sits with, for their reason: it is a fact about\n * the fleet that no single service owns.\n */\nexport function obligationsPath(docsDir: DocsDir): string {\n return join(docsDir, \"architecture\", \"obligations.yaml\");\n}\n\n/**\n * The FLEET's decision records — `architecture/adrs/`, beside the landscape and\n * the two vocabularies.\n *\n * ADRs used to exist at two altitudes only, `services/<id>/adrs/` and\n * `features/<FEAT>/adrs/`, so a decision about the fleet itself — \"event\n * publishers use a transactional outbox\", \"cross-service calls carry a circuit\n * breaker\" — had no home but one arbitrary service's directory, where the next\n * reader of the other fifty services never finds it. This is not a new axis: it\n * is the same `ADRS_DIR` name one level up, spelled through the same constant so\n * the two altitudes cannot drift into `adrs/` and `decisions/`.\n *\n * PRESENCE-TRACKED AND NOTHING MORE, exactly like the service directory it\n * mirrors: `loam list` counts the files (`repo.ts`'s `fleetAdrCount`) and no\n * check anywhere reads one. A fleet with no fleet-level ADRs owes nothing and\n * must produce no finding — which is also why `loam init` does NOT scaffold the\n * directory. git does not carry an empty one, so it would vanish on the first\n * clone and come back as a diff on the next `init`; and an empty `adrs/` in a\n * fresh repo reads as an obligation nobody has met, when there is no obligation\n * at all.\n */\nexport function fleetAdrsDir(docsDir: DocsDir): string {\n return join(docsDir, \"architecture\", ADRS_DIR);\n}\n\n/**\n * The GENERATED subsystem views — beside the fleet map because the LikeC4\n * renderer merges the whole `architecture/` project, and a view can only\n * `include` elements the landscape beside it defines. `loam subsystem sync`\n * owns the bytes (`core/repo/tree/views.ts` renders them); nothing in loam\n * ever parses the file, and staleness against the tree is a byte compare.\n */\nexport function subsystemViewsPath(docsDir: DocsDir): string {\n return join(docsDir, \"architecture\", \"subsystems.likec4\");\n}\n\nexport function featuresDir(docsDir: DocsDir): string {\n return join(docsDir, \"features\");\n}\n\n/**\n * The root of the tree that IS the fleet — where an unfiled service and a\n * root-level subsystem both sit. The `subsystem` verbs join validated names\n * under it; the enumerating walk spells the same join for itself\n * (`repo/tree/walk.ts`), because it cannot import this module.\n */\nexport function servicesDir(docsDir: DocsDir): string {\n return join(docsDir, \"services\");\n}\n\n/**\n * The agent contract's filename, and where it sits in a docs repo.\n *\n * Both spellings are exported because the two questions are different: `init`,\n * `doctor` and `validate` want a path under a docsDir they already hold, while\n * `docs.ts` builds the scaffold as `[relative path, bytes]` pairs and needs the\n * bare name. Four modules spelled the literal out for themselves, and one of\n * them — `init` — uses it to decide whether a directory IS a docs repo at all,\n * so a rename that reached three of the four would leave `init` recognising a\n * file nothing else writes any more.\n *\n * `openspec-inventory.ts`'s \"AGENTS.md\" is deliberately NOT this constant: it\n * names a file in a foreign OpenSpec tree that happens to share the name.\n */\nexport const AGENTS_FILENAME = \"AGENTS.md\";\n\nexport function agentsPath(docsDir: DocsDir): string {\n return join(docsDir, AGENTS_FILENAME);\n}\n\n/**\n * Where shipped features live. `features/archive/` is spelled here and nowhere\n * else for the same reason every other artifact path is: `archive` and\n * `unarchive` move directories in and out of it, and a layout string that two\n * commands re-derive is a layout two commands can disagree about. The\n * `features/archive/…` strings in refusal PROSE are not this path — they are\n * what a reader types into `ls`, and they stay spelled out.\n */\nexport function archiveDir(docsDir: DocsDir): string {\n return join(featuresDir(docsDir), ARCHIVE_DIR);\n}\n"]}
1
+ {"version":3,"file":"paths.js","sourceRoot":"","sources":["../../../src/core/repo/paths.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Where every artifact lives. Artifact filenames are spelled here and nowhere\n * else, so a rename is one edit rather than a grep.\n *\n * This is also the module that carries loam's one path guarantee.\n * `unfiledServicePaths(docsDir, service)` spells `<docsDir>/services/<service>/`,\n * so both parts are caller-controlled path input, and `node:path` cannot help:\n * `join(...paths: string[])` accepts every string there is. The guarantee IS\n * the parameter brands and nothing else — `PathableService` for the id (a name\n * whose provenance is the repository, unconstructible from document text),\n * `DocsDir`/`FeatureDir` for the roots (a resolved config or validated\n * `--docs`; a directory an enumeration read), and `ServiceDir` for\n * `servicePathsAt` (the enumeration's own resolved directory, narrower still)\n * — the `kernel/ids/` package holds the only casts, so an unchecked value at\n * these call sites does not compile.\n * Code that spells `services/<id>/` or a feature layout with a bare join is\n * outside the guarantee — `commands/new.ts` and the openspec migrator both do,\n * held instead by `resolveInside` at the write.\n */\nimport { join } from \"node:path\";\nimport { serviceDirOf, type DocsDir, type FeatureDir, type ServiceDir } from \"../kernel/ids/dirs.js\";\nimport type { PathableService } from \"../kernel/ids/service.js\";\nimport type { SubsystemName } from \"../kernel/ids/subsystem.js\";\n\n/** Directory under features/ holding shipped features. Never a feature itself. */\nexport const ARCHIVE_DIR = \"archive\";\n\n/* ------------------------------------------------------------------ */\n/* Paths — artifact filenames are spelled here and nowhere else */\n/* ------------------------------------------------------------------ */\n\n/**\n * The living artifact filenames, as one table, because three questions read\n * them: `servicePathsAt` below spells the paths, the tree walk classifies\n * a directory as a service by whether any of these names sit in it, and\n * `core/diff/base-state.ts` classifies the SAME way over a base git ref's\n * tree listing (exported for that reader — a filename respelled there would\n * be the drift this table exists to prevent). The walk cannot import this\n * module — `repo → repo/tree` is the package direction, and an edge back\n * would be the cycle `scripts/package-graph.mjs` refuses — so `repo.ts`\n * injects `isServiceArtifactName` into the walk request instead.\n */\nexport const ARTIFACT_FILES = {\n model: \"model.likec4\",\n spec: \"spec.md\",\n archSpec: \"arch.spec.md\",\n openapi: \"openapi.yaml\",\n asyncapi: \"asyncapi.yaml\",\n runbook: \"runbook.md\",\n health: \"health.yaml\",\n steps: \"steps.yaml\",\n} as const;\nconst ADRS_DIR = \"adrs\";\n\n/** Does a directory entry name a living service artifact? The classification half of the table above. */\nexport function isServiceArtifactName(name: string, kind: \"file\" | \"dir\"): boolean {\n if (kind === \"dir\") return name === ADRS_DIR;\n return Object.values(ARTIFACT_FILES).some((file) => file === name);\n}\n\nexport interface ServicePaths {\n dir: ServiceDir;\n model: string;\n spec: string;\n archSpec: string;\n openapi: string;\n /** The async contract — the event axis's sibling of `openapi`. */\n asyncapi: string;\n runbook: string;\n health: string;\n /**\n * The step catalogue — which phrases this service's suite has DECIDED to\n * define. Authored and opt-in; its CONTENTS are read by `loam steps` alone and\n * nothing in `validate` consults them\n * (`core/gherkin/steps/catalogue.ts` records why).\n *\n * Its EXISTENCE is read more widely, and that is not the same statement: the\n * filename is in `ARTIFACT_FILES`, so `isServiceArtifactName` classifies it\n * and the tree walk treats a directory holding one as a service. That is\n * correct — it is a service artifact — and it is worth writing down, because\n * it means a `steps.yaml` written at the wrong path mints a service.\n */\n steps: string;\n adrsDir: string;\n}\n\n/**\n * The artifact paths of a service whose directory is already KNOWN — the\n * enumeration's `dir`, at whatever depth the tree walk found it. This is the\n * spelling every reader of an existing service must use: joining\n * `services/<id>/` at the root is only true for a fleet nobody has filed, and\n * a moved service would silently grade as absent through it.\n */\nexport function servicePathsAt(dir: ServiceDir): ServicePaths {\n return {\n dir,\n model: join(dir, ARTIFACT_FILES.model),\n spec: join(dir, ARTIFACT_FILES.spec),\n archSpec: join(dir, ARTIFACT_FILES.archSpec),\n openapi: join(dir, ARTIFACT_FILES.openapi),\n asyncapi: join(dir, ARTIFACT_FILES.asyncapi),\n runbook: join(dir, ARTIFACT_FILES.runbook),\n health: join(dir, ARTIFACT_FILES.health),\n steps: join(dir, ARTIFACT_FILES.steps),\n adrsDir: join(dir, ADRS_DIR),\n };\n}\n\n/**\n * The UNFILED spelling — `<docsDir>/services/<id>/`, the tree's root level.\n * This is where creation lands (`adopt`'s brief, a new service materialised by\n * `archive`, the OpenSpec migration) and the honest fallback for a service the\n * enumeration does not answer; for a service that EXISTS, resolve through the\n * enumeration instead (`servicePathsAt(entry.dir)`, or\n * `locateServicePaths` in `service-target.ts`, which chooses between the two).\n * Renamed from `servicePaths` when the tree landed, precisely so the compiler\n * would put every remaining caller in front of a reviewer: a root join that\n * survives unreviewed is a moved service silently grading as absent.\n */\nexport function unfiledServicePaths(docsDir: DocsDir, service: PathableService): ServicePaths {\n return servicePathsAt(serviceDirOf(join(docsDir, \"services\", service)));\n}\n\n/**\n * The creation spelling INSIDE a subsystem — `<subsystemDir>/<id>/`, the\n * target `adopt --subsystem` briefs so an adoption need not land unfiled and\n * cost a second command. The same two provenances the unfiled spelling\n * combines, one level down: the subsystem directory came off the tree walk's\n * readdir, and the id passed the service grammar before it reached here.\n */\nexport function servicePathsUnder(subsystemDir: string, service: PathableService): ServicePaths {\n return servicePathsAt(serviceDirOf(join(subsystemDir, service)));\n}\n\n/**\n * The directory a SUBSYSTEM occupies (or will occupy) under a parent — the\n * spelling `subsystem new` creates and `subsystem rename` renames into. The\n * same guarantee-by-brand as the service spellings above: the parent came off\n * the tree walk's readdir (or is `servicesDir` itself), and demanding\n * `SubsystemName` here is what makes the validator unskippable — a raw argv\n * string at this call site does not compile, so no future verb can hoist the\n * join above the check that earns it.\n */\nexport function subsystemPathUnder(parentDir: string, name: SubsystemName): string {\n return join(parentDir, name);\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: FeatureDir): 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 /** The async contract delta — the event axis's sibling of `openapi`. */\n asyncapi: string;\n}\n\nexport function featureSpecPaths(featureDir: FeatureDir, service: PathableService): 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 asyncapi: join(dir, \"asyncapi.yaml\"),\n };\n}\n\nexport function landscapePath(docsDir: DocsDir): string {\n return join(docsDir, \"architecture\", LANDSCAPE_FILE);\n}\n\n/** The fleet's authorization vocabulary — beside the fleet map, for the same reason. */\nexport function permissionsPath(docsDir: DocsDir): string {\n return join(docsDir, \"architecture\", \"permissions.yaml\");\n}\n\n/** The fleet's capability vocabulary — beside the other two fleet documents, for the same reason. */\nexport function capabilitiesPath(docsDir: DocsDir): string {\n return join(docsDir, \"architecture\", \"capabilities.yaml\");\n}\n\n/**\n * The fleet's ARCHITECTURAL OBLIGATION vocabulary — beside the landscape it tags\n * and the two vocabularies it sits with, for their reason: it is a fact about\n * the fleet that no single service owns.\n */\nexport function obligationsPath(docsDir: DocsDir): string {\n return join(docsDir, \"architecture\", \"obligations.yaml\");\n}\n\n/**\n * The FLEET's decision records — `architecture/adrs/`, beside the landscape and\n * the two vocabularies.\n *\n * ADRs used to exist at two altitudes only, `services/<id>/adrs/` and\n * `features/<FEAT>/adrs/`, so a decision about the fleet itself — \"event\n * publishers use a transactional outbox\", \"cross-service calls carry a circuit\n * breaker\" — had no home but one arbitrary service's directory, where the next\n * reader of the other fifty services never finds it. This is not a new axis: it\n * is the same `ADRS_DIR` name one level up, spelled through the same constant so\n * the two altitudes cannot drift into `adrs/` and `decisions/`.\n *\n * PRESENCE-TRACKED AND NOTHING MORE, exactly like the service directory it\n * mirrors: `loam list` counts the files (`repo.ts`'s `fleetAdrCount`) and no\n * check anywhere reads one. A fleet with no fleet-level ADRs owes nothing and\n * must produce no finding — which is also why `loam init` does NOT scaffold the\n * directory. git does not carry an empty one, so it would vanish on the first\n * clone and come back as a diff on the next `init`; and an empty `adrs/` in a\n * fresh repo reads as an obligation nobody has met, when there is no obligation\n * at all.\n */\nexport function fleetAdrsDir(docsDir: DocsDir): string {\n return join(docsDir, \"architecture\", ADRS_DIR);\n}\n\n/**\n * The GENERATED subsystem views — beside the fleet map because the LikeC4\n * renderer merges the whole `architecture/` project, and a view can only\n * `include` elements the landscape beside it defines. `loam subsystem sync`\n * owns the bytes (`core/repo/tree/views.ts` renders them); nothing in loam\n * ever parses the file, and staleness against the tree is a byte compare.\n */\nexport function subsystemViewsPath(docsDir: DocsDir): string {\n return join(docsDir, \"architecture\", SUBSYSTEM_VIEWS_FILE);\n}\n\n/** The generated views' file name, spelled once because the reservation below reads it too. */\nconst SUBSYSTEM_VIEWS_FILE = \"subsystems.likec4\";\n\n/**\n * Names inside `architecture/` that LOAM owns, and what goes wrong if a\n * feature's own document claims one.\n *\n * `features/<FEAT>/deployment/<name>.likec4` merges as a whole-file copy\n * straight into `architecture/` — unlike a flow, which lands under\n * `usecases/` — so its `<name>` is free to be any of these, and two of them\n * fail in ways nothing downstream would report:\n *\n * - `subsystems.likec4` is GENERATED and is EXCLUDED from every project load\n * (docs/DESIGN.md rule 26 keeps it a byte compare), so topology landing\n * there is invisible to loam from the moment it archives — and the next\n * `loam subsystem sync` overwrites the file wholesale. Silent both ways,\n * which is the worst shape a failure can have;\n * - `landscape.likec4` is the living map the archive SPLICES; a whole-file\n * copy over it would discard the fleet;\n * - `usecases/` is the flow tree, walked by the use-case scan. A deployment\n * document filed there parses (one project) and is then looked for by a\n * reader asking a different question.\n *\n * Returns the reason, or null when the name is the author's to choose. Takes\n * the `/`-separated rel a directory walk produced, never a string from argv.\n */\nexport function reservedArchitectureName(rel: string): string | null {\n const parts = rel.split(\"/\");\n if (parts[0] === USECASE_DIR) {\n return `\\`${USECASE_DIR}/\\` is the use-case tree — a flow belongs in \\`features/<FEAT>/usecases/\\`, which merges there`;\n }\n if (rel === SUBSYSTEM_VIEWS_FILE) {\n return \"`subsystems.likec4` is generated by `loam subsystem sync`, which overwrites it wholesale — and nothing in loam ever parses it, so anything landing there is invisible from the moment it archives\";\n }\n if (rel === LANDSCAPE_FILE) {\n return \"`landscape.likec4` is the living fleet map, which the archive SPLICES into rather than replaces\";\n }\n return null;\n}\n\n/** The living map's file name, and the flow tree's directory — spelled once, read by the reservation. */\nconst LANDSCAPE_FILE = \"landscape.likec4\";\nconst USECASE_DIR = \"usecases\";\n\nexport function featuresDir(docsDir: DocsDir): string {\n return join(docsDir, \"features\");\n}\n\n/**\n * The root of the tree that IS the fleet — where an unfiled service and a\n * root-level subsystem both sit. The `subsystem` verbs join validated names\n * under it; the enumerating walk spells the same join for itself\n * (`repo/tree/walk.ts`), because it cannot import this module.\n */\nexport function servicesDir(docsDir: DocsDir): string {\n return join(docsDir, \"services\");\n}\n\n/**\n * The agent contract's filename, and where it sits in a docs repo.\n *\n * Both spellings are exported because the two questions are different: `init`,\n * `doctor` and `validate` want a path under a docsDir they already hold, while\n * `docs.ts` builds the scaffold as `[relative path, bytes]` pairs and needs the\n * bare name. Four modules spelled the literal out for themselves, and one of\n * them — `init` — uses it to decide whether a directory IS a docs repo at all,\n * so a rename that reached three of the four would leave `init` recognising a\n * file nothing else writes any more.\n *\n * `openspec-inventory.ts`'s \"AGENTS.md\" is deliberately NOT this constant: it\n * names a file in a foreign OpenSpec tree that happens to share the name.\n */\nexport const AGENTS_FILENAME = \"AGENTS.md\";\n\nexport function agentsPath(docsDir: DocsDir): string {\n return join(docsDir, AGENTS_FILENAME);\n}\n\n/**\n * Where shipped features live. `features/archive/` is spelled here and nowhere\n * else for the same reason every other artifact path is: `archive` and\n * `unarchive` move directories in and out of it, and a layout string that two\n * commands re-derive is a layout two commands can disagree about. The\n * `features/archive/…` strings in refusal PROSE are not this path — they are\n * what a reader types into `ls`, and they stay spelled out.\n */\nexport function archiveDir(docsDir: DocsDir): string {\n return join(featuresDir(docsDir), ARCHIVE_DIR);\n}\n"]}
@@ -127,8 +127,25 @@ export function featureArtifacts(docsDir, feature, services, grading) {
127
127
  for (const svc of services) {
128
128
  const p = featureSpecPaths(feature.dir, svc);
129
129
  const specFault = faulted("spec", svc);
130
- out.push(fileState("spec", { service: svc, path: rel(p.spec), exists: existsSync(p.spec) }, true, specFault));
131
- out.push(fileState("arch-spec", { service: svc, path: rel(p.archSpec), exists: existsSync(p.archSpec) }, false, specFault));
130
+ // What a service delta owes is a REQUIREMENT DELTA, not specifically the
131
+ // business one. `arch.spec.md` is the other corpus, with the same grammar
132
+ // and the same delta algebra, and `featureChecklist` derives claims from
133
+ // it — so a directory holding only the architectural document is a
134
+ // complete delta, and every gate already reads it as one: `coherence`'s
135
+ // `service.no-requirement-delta` counts the DIRECTORY, and
136
+ // `validate --feature` grades an arch-only delta clean at exit 0.
137
+ //
138
+ // Requiring `spec.md` unconditionally made this module the one document
139
+ // that disagreed. An architecture-only feature — an outbox added, a retry
140
+ // policy, an alert; the changes `arch.spec.md` exists for because no
141
+ // business scenario was ever going to carry them — reported `missing` and
142
+ // emitted `next.author-spec`, sending the author to write business
143
+ // requirements the change does not have, about a feature the gate accepts.
144
+ // status may be redder than the gates on a question they also ask; it may
145
+ // not invent an obligation they do not.
146
+ const archExists = existsSync(p.archSpec);
147
+ out.push(fileState("spec", { service: svc, path: rel(p.spec), exists: existsSync(p.spec) }, !archExists, specFault));
148
+ out.push(fileState("arch-spec", { service: svc, path: rel(p.archSpec), exists: archExists }, false, specFault));
132
149
  out.push(fileState("openapi", { service: svc, path: rel(p.openapi), exists: existsSync(p.openapi) }, owesContract(living.get(svc), contracted.has(svc), governs.has(svc)), faulted("openapi", svc)));
133
150
  // Never required: an event contract is genuinely optional — the axis's own
134
151
  // absence-grading rests on that — so absence is `done`, exactly like
@@ -145,7 +162,15 @@ export function featureArtifacts(docsDir, feature, services, grading) {
145
162
  // promise is realized by service requirements, and those are what the record
146
163
  // answers — so counting one as a feeder would report a capability-only
147
164
  // feature as ready to verify against a checklist with nothing in it.
148
- const feeders = out.filter((a) => a.id !== "intent" && a.id !== "arch-spec" && a.id !== "capabilities" && a.exists);
165
+ //
166
+ // `arch-spec` used to be excluded beside it, and that failed the same test
167
+ // the comment above states: `featureChecklist` DOES read `arch.spec.md`
168
+ // (`core/verify/checklist.ts`), and `loam verify` on an architecture-only
169
+ // feature returns a real checklist. The row said `blocked … needs delta,
170
+ // spec, openapi, asyncapi` and `next.verify` said there was nothing to derive
171
+ // a checklist from, about a command that derives one — which is the failure
172
+ // `blocked` is supposed to prevent, pointed the wrong way.
173
+ const feeders = out.filter((a) => a.id !== "intent" && a.id !== "capabilities" && a.exists);
149
174
  out.push({
150
175
  id: "verification",
151
176
  service: null,
@@ -153,7 +178,7 @@ export function featureArtifacts(docsDir, feature, services, grading) {
153
178
  exists: verification.state !== "absent",
154
179
  required: true,
155
180
  status: feeders.length === 0 ? "blocked" : verificationStatus(verification),
156
- blockedBy: feeders.length === 0 ? ["delta", "spec", "openapi", "asyncapi"] : [],
181
+ blockedBy: feeders.length === 0 ? ["delta", "spec", "arch-spec", "openapi", "asyncapi"] : [],
157
182
  });
158
183
  return out;
159
184
  }
@@ -1 +1 @@
1
- {"version":3,"file":"artifacts.js","sourceRoot":"","sources":["../../../../src/core/status/feature/artifacts.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The artifact table: which files a feature owes, whether each is on disk, and\n * which finding turns a row `draft`. Nothing is graded here — the shared\n * checks already ran, and this module only attributes what they reported to\n * the file a reader should edit.\n */\nimport { existsSync } from \"node:fs\";\nimport { repoPath } from \"../../envelope/json.js\";\nimport type { CapabilityDoc } from \"../../capabilities/tree.js\";\nimport type { PathableService } from \"../../kernel/ids/service.js\";\nimport type { FeatureEntry } from \"../../repo/entries.js\";\nimport { featurePaths, featureSpecPaths } from \"../../repo/paths.js\";\nimport { verificationPath } from \"../../verify/record.js\";\nimport type { Finding } from \"../../vocabulary/report.js\";\nimport { owesContract } from \"../contracts.js\";\nimport type { ServiceEntry } from \"../../repo/entries.js\";\nimport type { ArtifactId, ArtifactState, ArtifactStatus, VerificationState } from \"../report.js\";\nimport { verificationStatus } from \"../verification.js\";\nimport type { DocsDir } from \"../../kernel/ids/dirs.js\";\n\n/**\n * The `capability.*` codes filed against a capability DOCUMENT rather than\n * against a service requirement's join line. Spelled out rather than matched by\n * prefix because the family straddles both corpora, and the whole point of this\n * table is to name one file.\n */\nconst CAPABILITY_DOCUMENT_CODES = new Set([\n \"capability.requirement-unidentified\",\n \"capability.requirement-service-scoped\",\n \"capability.requirement-inert-join\",\n \"capability.uncovered\",\n \"capability.remove-requirement-realized\",\n]);\n\n/**\n * Which artifact an error names.\n *\n * Attribution is by code family, because that is the only machine-readable\n * pointer an `Issue` carries: `subject` narrows to a NAME, never to a file.\n * The families are unambiguous — `c4-api.*`/`c4-event.*`/`c4.*` and the two\n * `delta.*` codes about the LikeC4 document itself grade the architecture\n * axis, `openapi.*` grades the API contract, `asyncapi.*` the event contract,\n * and the remaining `delta.*` codes plus `spec-api.*`/`spec-event.*` are the\n * delta-shape checks, every one of which is about a requirement in a spec\n * delta.\n *\n * An error that maps to nothing (or whose subject is unknown) still lands in\n * `checks.issues` and still drives `next.fix-coherence` — it just does not turn\n * any single artifact `draft`, because guessing which file to blame is how a\n * reader gets sent to edit the wrong one.\n *\n * THE BUSINESS CORPUS IS THE ONE PLACE THE CODE FAMILY IS NOT ENOUGH. A\n * `delta.*` issue's `subject` is a service id OR a capability id\n * (`core/delta/scope.ts`), and so is a `scaffold.placeholder`'s\n * (`core/coherence/authoring/scaffold.ts` files one per unedited capability\n * delta), and nothing in either code says which — so this\n * function answers `spec` for both and `featureArtifacts` below resolves it by\n * MEMBERSHIP, against the two lists it already holds. Where a name is only a\n * capability the capability row is turned `draft` and no service row is; where\n * a fleet holds a service AND a capability of the same name, BOTH are, which is\n * the pessimistic answer this module is required to give and strictly better\n * than the old one (it named the service's spec delta and was silently wrong).\n * The capability-document codes below need no such resolution: they are only\n * ever filed against a capability.\n */\nfunction faultedArtifact(code: string, subject?: string): ArtifactId | null {\n if (code === \"delta.invalid\" || code === \"delta.nothing-tagged\") return \"delta\";\n if (code.startsWith(\"c4-api.\") || code.startsWith(\"c4-event.\") || code.startsWith(\"c4.\")) return \"delta\";\n if (code.startsWith(\"openapi.\")) return \"openapi\";\n if (code.startsWith(\"asyncapi.\")) return \"asyncapi\";\n // The capability document's own grades, and the two halves of the `Realizes:`\n // join a feature can break — every one of them names a\n // `features/<FEAT>/capabilities/<id>/spec.md`, never a service delta. The two\n // `capability.*` codes deliberately absent are `capability.unknown` and\n // `capability.realizes-unknown`: those grade a SERVICE requirement's join\n // lines and carry a service id, so the prefix alone would send their reader\n // to the wrong file.\n if (CAPABILITY_DOCUMENT_CODES.has(code)) return \"capabilities\";\n if (code.startsWith(\"delta.\") || code.startsWith(\"spec-api.\") || code.startsWith(\"spec-event.\")) return \"spec\";\n // The two families validate contributes: the frontmatter checks read only\n // intent.md, and a requirement with no scenario is a requirement in a spec\n // delta. Both name exactly one file, so both may turn a row `draft`.\n if (code.startsWith(\"frontmatter.\")) return \"intent\";\n if (code === \"requirements.missing-scenarios\") return \"spec\";\n // The authoring gate names exactly one file per finding too — leaving these\n // unmapped had the table calling intent.md `done` (\"nothing is owed here\")\n // while the archive exited 1 because it says nothing. A placeholder finding\n // carries no subject when it is the C4 delta's scaffolded description; with a\n // subject it is a SPEC DELTA IN EITHER CORPUS — a service id when the text\n // sits in `specs/<svc>/`, a capability id when it sits in\n // `capabilities/<id>/` — which is why `spec` here is resolved by membership\n // below and must not be narrowed to the service list.\n if (code === \"intent.empty\") return \"intent\";\n if (code === \"scaffold.placeholder\") return subject === undefined ? \"delta\" : \"spec\";\n return null;\n}\n\n/**\n * The artifact table. Every entry is graded by two questions in order — is it\n * there, and does anything the shared checks reported name it — so a file that\n * exists and is wrong can never be reported the way one that was never written\n * is.\n */\n/** What the grading of one feature's files needs beyond the files themselves. */\nexport interface Grading {\n /** The findings that make an artifact `draft` rather than `done`. */\n blocking: Finding[];\n verification: VerificationState;\n /** Operations some OTHER feature's contract already holds. */\n contracted: ReadonlySet<string>;\n /** Operations this feature's own requirements govern. */\n governs: ReadonlySet<string>;\n /** The enumeration's entries by id — where each living service is and what it has (owesContract reads both). */\n living: ReadonlyMap<string, ServiceEntry>;\n /**\n * The capability documents this feature's delta carries. Empty for every\n * feature in a fleet that has not adopted the business axis, which is what\n * keeps the rows out of those payloads entirely.\n */\n capabilities: readonly CapabilityDoc[];\n}\n\nexport function featureArtifacts(\n docsDir: DocsDir,\n feature: FeatureEntry,\n services: readonly PathableService[],\n grading: Grading,\n): ArtifactState[] {\n const { blocking, verification, contracted, governs, living, capabilities } = grading;\n const paths = featurePaths(feature.dir);\n const rel = (abs: string): string => repoPath(docsDir, abs);\n const faults = blocking.map((f) => ({ artifact: faultedArtifact(f.code, f.subject), subject: f.subject }));\n const faulted = (id: ArtifactId, service: string | null): boolean =>\n faults.some((f) => f.artifact === id && (service === null || f.subject === service));\n\n const out: ArtifactState[] = [\n fileState(\"intent\", { service: null, path: rel(paths.intent), exists: existsSync(paths.intent) }, true, faulted(\"intent\", null)),\n fileState(\"delta\", { service: null, path: rel(paths.delta), exists: existsSync(paths.delta) }, false, faulted(\"delta\", null)),\n ];\n\n // One row per capability document the feature carries, and none at all when\n // it carries none: the row is the answer to \"which file do I edit\", and a\n // fleet that has not adopted the axis has no such file to name. Never\n // `required` — no feature owes a business change — so absence never shows up\n // as work owed, and only a fault turns one `draft`. A `delta.*` fault carries\n // the capability id in `subject` with nothing saying it is not a service, so\n // the membership test is the resolution the code family cannot give.\n for (const doc of capabilities) {\n out.push({\n id: \"capabilities\",\n service: null,\n capability: doc.id,\n path: rel(doc.spec),\n exists: existsSync(doc.spec),\n required: false,\n status: faulted(\"capabilities\", doc.id) || faulted(\"spec\", doc.id) ? \"draft\" : \"done\",\n blockedBy: [],\n });\n }\n\n for (const svc of services) {\n const p = featureSpecPaths(feature.dir, svc);\n const specFault = faulted(\"spec\", svc);\n out.push(fileState(\"spec\", { service: svc, path: rel(p.spec), exists: existsSync(p.spec) }, true, specFault));\n out.push(fileState(\"arch-spec\", { service: svc, path: rel(p.archSpec), exists: existsSync(p.archSpec) }, false, specFault));\n out.push(\n fileState(\n \"openapi\",\n { service: svc, path: rel(p.openapi), exists: existsSync(p.openapi) },\n owesContract(living.get(svc), contracted.has(svc), governs.has(svc)),\n faulted(\"openapi\", svc),\n ),\n );\n // Never required: an event contract is genuinely optional — the axis's own\n // absence-grading rests on that — so absence is `done`, exactly like\n // arch.spec.md. Present and faulted still turns the row `draft`.\n out.push(\n fileState(\n \"asyncapi\",\n { service: svc, path: rel(p.asyncapi), exists: existsSync(p.asyncapi) },\n false,\n faulted(\"asyncapi\", svc),\n ),\n );\n }\n\n // The record is the one artifact with a real prerequisite: its checklist is\n // derived from the delta and the per-service deltas, so with none of them\n // written there is nothing to answer — authoring one is impossible rather\n // than merely premature, which is the whole distinction `blocked` carries.\n //\n // `capabilities` is excluded for exactly that reason and not by oversight:\n // `featureChecklist` derives no claim from a capability document — a business\n // promise is realized by service requirements, and those are what the record\n // answers — so counting one as a feeder would report a capability-only\n // feature as ready to verify against a checklist with nothing in it.\n const feeders = out.filter(\n (a) => a.id !== \"intent\" && a.id !== \"arch-spec\" && a.id !== \"capabilities\" && a.exists,\n );\n out.push({\n id: \"verification\",\n service: null,\n path: rel(verificationPath(feature.dir)),\n exists: verification.state !== \"absent\",\n required: true,\n status: feeders.length === 0 ? \"blocked\" : verificationStatus(verification),\n blockedBy: feeders.length === 0 ? [\"delta\", \"spec\", \"openapi\", \"asyncapi\"] : [],\n });\n return out;\n}\n\n/** Where an artifact is and whether it is there — the three facts a grade reads. */\ninterface FileFacts {\n service: string | null;\n path: string;\n exists: boolean;\n}\n\nfunction fileState(id: ArtifactId, file: FileFacts, required: boolean, faulted: boolean): ArtifactState {\n const { service, path, exists } = file;\n const status: ArtifactStatus = !exists\n ? required\n ? \"missing\"\n : \"done\"\n : faulted\n ? \"draft\"\n : \"done\";\n return { id, service, path, exists, required, status, blockedBy: [] };\n}\n"]}
1
+ {"version":3,"file":"artifacts.js","sourceRoot":"","sources":["../../../../src/core/status/feature/artifacts.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,QAAQ,EAAE,MAAM,wBAAwB,CAAC;AAIlD,OAAO,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AACrE,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAE1D,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAG/C,OAAO,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AAGxD;;;;;GAKG;AACH,MAAM,yBAAyB,GAAG,IAAI,GAAG,CAAC;IACxC,qCAAqC;IACrC,uCAAuC;IACvC,mCAAmC;IACnC,sBAAsB;IACtB,wCAAwC;CACzC,CAAC,CAAC;AAEH;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,SAAS,eAAe,CAAC,IAAY,EAAE,OAAgB;IACrD,IAAI,IAAI,KAAK,eAAe,IAAI,IAAI,KAAK,sBAAsB;QAAE,OAAO,OAAO,CAAC;IAChF,IAAI,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC,IAAI,IAAI,CAAC,UAAU,CAAC,WAAW,CAAC,IAAI,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC;QAAE,OAAO,OAAO,CAAC;IACzG,IAAI,IAAI,CAAC,UAAU,CAAC,UAAU,CAAC;QAAE,OAAO,SAAS,CAAC;IAClD,IAAI,IAAI,CAAC,UAAU,CAAC,WAAW,CAAC;QAAE,OAAO,UAAU,CAAC;IACpD,8EAA8E;IAC9E,uDAAuD;IACvD,8EAA8E;IAC9E,wEAAwE;IACxE,0EAA0E;IAC1E,4EAA4E;IAC5E,qBAAqB;IACrB,IAAI,yBAAyB,CAAC,GAAG,CAAC,IAAI,CAAC;QAAE,OAAO,cAAc,CAAC;IAC/D,IAAI,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,IAAI,IAAI,CAAC,UAAU,CAAC,WAAW,CAAC,IAAI,IAAI,CAAC,UAAU,CAAC,aAAa,CAAC;QAAE,OAAO,MAAM,CAAC;IAC/G,0EAA0E;IAC1E,2EAA2E;IAC3E,qEAAqE;IACrE,IAAI,IAAI,CAAC,UAAU,CAAC,cAAc,CAAC;QAAE,OAAO,QAAQ,CAAC;IACrD,IAAI,IAAI,KAAK,gCAAgC;QAAE,OAAO,MAAM,CAAC;IAC7D,4EAA4E;IAC5E,2EAA2E;IAC3E,4EAA4E;IAC5E,8EAA8E;IAC9E,2EAA2E;IAC3E,0DAA0D;IAC1D,4EAA4E;IAC5E,sDAAsD;IACtD,IAAI,IAAI,KAAK,cAAc;QAAE,OAAO,QAAQ,CAAC;IAC7C,IAAI,IAAI,KAAK,sBAAsB;QAAE,OAAO,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC;IACrF,OAAO,IAAI,CAAC;AACd,CAAC;AA2BD,MAAM,UAAU,gBAAgB,CAC9B,OAAgB,EAChB,OAAqB,EACrB,QAAoC,EACpC,OAAgB;IAEhB,MAAM,EAAE,QAAQ,EAAE,YAAY,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,GAAG,OAAO,CAAC;IACtF,MAAM,KAAK,GAAG,YAAY,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IACxC,MAAM,GAAG,GAAG,CAAC,GAAW,EAAU,EAAE,CAAC,QAAQ,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC;IAC5D,MAAM,MAAM,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,QAAQ,EAAE,eAAe,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,OAAO,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC;IAC3G,MAAM,OAAO,GAAG,CAAC,EAAc,EAAE,OAAsB,EAAW,EAAE,CAClE,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,KAAK,EAAE,IAAI,CAAC,OAAO,KAAK,IAAI,IAAI,CAAC,CAAC,OAAO,KAAK,OAAO,CAAC,CAAC,CAAC;IAEvF,MAAM,GAAG,GAAoB;QAC3B,SAAS,CAAC,QAAQ,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,GAAG,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,MAAM,EAAE,UAAU,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,EAAE,IAAI,EAAE,OAAO,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;QAChI,SAAS,CAAC,OAAO,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC,EAAE,MAAM,EAAE,UAAU,CAAC,KAAK,CAAC,KAAK,CAAC,EAAE,EAAE,KAAK,EAAE,OAAO,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;KAC9H,CAAC;IAEF,4EAA4E;IAC5E,0EAA0E;IAC1E,sEAAsE;IACtE,6EAA6E;IAC7E,8EAA8E;IAC9E,6EAA6E;IAC7E,qEAAqE;IACrE,KAAK,MAAM,GAAG,IAAI,YAAY,EAAE,CAAC;QAC/B,GAAG,CAAC,IAAI,CAAC;YACP,EAAE,EAAE,cAAc;YAClB,OAAO,EAAE,IAAI;YACb,UAAU,EAAE,GAAG,CAAC,EAAE;YAClB,IAAI,EAAE,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC;YACnB,MAAM,EAAE,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC;YAC5B,QAAQ,EAAE,KAAK;YACf,MAAM,EAAE,OAAO,CAAC,cAAc,EAAE,GAAG,CAAC,EAAE,CAAC,IAAI,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM;YACrF,SAAS,EAAE,EAAE;SACd,CAAC,CAAC;IACL,CAAC;IAED,KAAK,MAAM,GAAG,IAAI,QAAQ,EAAE,CAAC;QAC3B,MAAM,CAAC,GAAG,gBAAgB,CAAC,OAAO,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;QAC7C,MAAM,SAAS,GAAG,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;QACvC,yEAAyE;QACzE,0EAA0E;QAC1E,yEAAyE;QACzE,mEAAmE;QACnE,wEAAwE;QACxE,2DAA2D;QAC3D,kEAAkE;QAClE,EAAE;QACF,wEAAwE;QACxE,0EAA0E;QAC1E,qEAAqE;QACrE,0EAA0E;QAC1E,mEAAmE;QACnE,2EAA2E;QAC3E,0EAA0E;QAC1E,wCAAwC;QACxC,MAAM,UAAU,GAAG,UAAU,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC;QAC1C,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,EAAE,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,EAAE,CAAC,UAAU,EAAE,SAAS,CAAC,CAAC,CAAC;QACrH,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,WAAW,EAAE,EAAE,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,GAAG,CAAC,CAAC,CAAC,QAAQ,CAAC,EAAE,MAAM,EAAE,UAAU,EAAE,EAAE,KAAK,EAAE,SAAS,CAAC,CAAC,CAAC;QAChH,GAAG,CAAC,IAAI,CACN,SAAS,CACP,SAAS,EACT,EAAE,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,GAAG,CAAC,CAAC,CAAC,OAAO,CAAC,EAAE,MAAM,EAAE,UAAU,CAAC,CAAC,CAAC,OAAO,CAAC,EAAE,EACrE,YAAY,CAAC,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,UAAU,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,EACpE,OAAO,CAAC,SAAS,EAAE,GAAG,CAAC,CACxB,CACF,CAAC;QACF,2EAA2E;QAC3E,qEAAqE;QACrE,iEAAiE;QACjE,GAAG,CAAC,IAAI,CACN,SAAS,CACP,UAAU,EACV,EAAE,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,GAAG,CAAC,CAAC,CAAC,QAAQ,CAAC,EAAE,MAAM,EAAE,UAAU,CAAC,CAAC,CAAC,QAAQ,CAAC,EAAE,EACvE,KAAK,EACL,OAAO,CAAC,UAAU,EAAE,GAAG,CAAC,CACzB,CACF,CAAC;IACJ,CAAC;IAED,4EAA4E;IAC5E,0EAA0E;IAC1E,0EAA0E;IAC1E,2EAA2E;IAC3E,EAAE;IACF,2EAA2E;IAC3E,8EAA8E;IAC9E,6EAA6E;IAC7E,uEAAuE;IACvE,qEAAqE;IACrE,EAAE;IACF,2EAA2E;IAC3E,wEAAwE;IACxE,0EAA0E;IAC1E,yEAAyE;IACzE,8EAA8E;IAC9E,4EAA4E;IAC5E,2DAA2D;IAC3D,MAAM,OAAO,GAAG,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,QAAQ,IAAI,CAAC,CAAC,EAAE,KAAK,cAAc,IAAI,CAAC,CAAC,MAAM,CAAC,CAAC;IAC5F,GAAG,CAAC,IAAI,CAAC;QACP,EAAE,EAAE,cAAc;QAClB,OAAO,EAAE,IAAI;QACb,IAAI,EAAE,GAAG,CAAC,gBAAgB,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QACxC,MAAM,EAAE,YAAY,CAAC,KAAK,KAAK,QAAQ;QACvC,QAAQ,EAAE,IAAI;QACd,MAAM,EAAE,OAAO,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,kBAAkB,CAAC,YAAY,CAAC;QAC3E,SAAS,EAAE,OAAO,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,SAAS,EAAE,UAAU,CAAC,CAAC,CAAC,CAAC,EAAE;KAC7F,CAAC,CAAC;IACH,OAAO,GAAG,CAAC;AACb,CAAC;AASD,SAAS,SAAS,CAAC,EAAc,EAAE,IAAe,EAAE,QAAiB,EAAE,OAAgB;IACrF,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;IACvC,MAAM,MAAM,GAAmB,CAAC,MAAM;QACpC,CAAC,CAAC,QAAQ;YACR,CAAC,CAAC,SAAS;YACX,CAAC,CAAC,MAAM;QACV,CAAC,CAAC,OAAO;YACP,CAAC,CAAC,OAAO;YACT,CAAC,CAAC,MAAM,CAAC;IACb,OAAO,EAAE,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,EAAE,EAAE,CAAC;AACxE,CAAC","sourcesContent":["/**\n * The artifact table: which files a feature owes, whether each is on disk, and\n * which finding turns a row `draft`. Nothing is graded here — the shared\n * checks already ran, and this module only attributes what they reported to\n * the file a reader should edit.\n */\nimport { existsSync } from \"node:fs\";\nimport { repoPath } from \"../../envelope/json.js\";\nimport type { CapabilityDoc } from \"../../capabilities/tree.js\";\nimport type { PathableService } from \"../../kernel/ids/service.js\";\nimport type { FeatureEntry } from \"../../repo/entries.js\";\nimport { featurePaths, featureSpecPaths } from \"../../repo/paths.js\";\nimport { verificationPath } from \"../../verify/record.js\";\nimport type { Finding } from \"../../vocabulary/report.js\";\nimport { owesContract } from \"../contracts.js\";\nimport type { ServiceEntry } from \"../../repo/entries.js\";\nimport type { ArtifactId, ArtifactState, ArtifactStatus, VerificationState } from \"../report.js\";\nimport { verificationStatus } from \"../verification.js\";\nimport type { DocsDir } from \"../../kernel/ids/dirs.js\";\n\n/**\n * The `capability.*` codes filed against a capability DOCUMENT rather than\n * against a service requirement's join line. Spelled out rather than matched by\n * prefix because the family straddles both corpora, and the whole point of this\n * table is to name one file.\n */\nconst CAPABILITY_DOCUMENT_CODES = new Set([\n \"capability.requirement-unidentified\",\n \"capability.requirement-service-scoped\",\n \"capability.requirement-inert-join\",\n \"capability.uncovered\",\n \"capability.remove-requirement-realized\",\n]);\n\n/**\n * Which artifact an error names.\n *\n * Attribution is by code family, because that is the only machine-readable\n * pointer an `Issue` carries: `subject` narrows to a NAME, never to a file.\n * The families are unambiguous — `c4-api.*`/`c4-event.*`/`c4.*` and the two\n * `delta.*` codes about the LikeC4 document itself grade the architecture\n * axis, `openapi.*` grades the API contract, `asyncapi.*` the event contract,\n * and the remaining `delta.*` codes plus `spec-api.*`/`spec-event.*` are the\n * delta-shape checks, every one of which is about a requirement in a spec\n * delta.\n *\n * An error that maps to nothing (or whose subject is unknown) still lands in\n * `checks.issues` and still drives `next.fix-coherence` — it just does not turn\n * any single artifact `draft`, because guessing which file to blame is how a\n * reader gets sent to edit the wrong one.\n *\n * THE BUSINESS CORPUS IS THE ONE PLACE THE CODE FAMILY IS NOT ENOUGH. A\n * `delta.*` issue's `subject` is a service id OR a capability id\n * (`core/delta/scope.ts`), and so is a `scaffold.placeholder`'s\n * (`core/coherence/authoring/scaffold.ts` files one per unedited capability\n * delta), and nothing in either code says which — so this\n * function answers `spec` for both and `featureArtifacts` below resolves it by\n * MEMBERSHIP, against the two lists it already holds. Where a name is only a\n * capability the capability row is turned `draft` and no service row is; where\n * a fleet holds a service AND a capability of the same name, BOTH are, which is\n * the pessimistic answer this module is required to give and strictly better\n * than the old one (it named the service's spec delta and was silently wrong).\n * The capability-document codes below need no such resolution: they are only\n * ever filed against a capability.\n */\nfunction faultedArtifact(code: string, subject?: string): ArtifactId | null {\n if (code === \"delta.invalid\" || code === \"delta.nothing-tagged\") return \"delta\";\n if (code.startsWith(\"c4-api.\") || code.startsWith(\"c4-event.\") || code.startsWith(\"c4.\")) return \"delta\";\n if (code.startsWith(\"openapi.\")) return \"openapi\";\n if (code.startsWith(\"asyncapi.\")) return \"asyncapi\";\n // The capability document's own grades, and the two halves of the `Realizes:`\n // join a feature can break — every one of them names a\n // `features/<FEAT>/capabilities/<id>/spec.md`, never a service delta. The two\n // `capability.*` codes deliberately absent are `capability.unknown` and\n // `capability.realizes-unknown`: those grade a SERVICE requirement's join\n // lines and carry a service id, so the prefix alone would send their reader\n // to the wrong file.\n if (CAPABILITY_DOCUMENT_CODES.has(code)) return \"capabilities\";\n if (code.startsWith(\"delta.\") || code.startsWith(\"spec-api.\") || code.startsWith(\"spec-event.\")) return \"spec\";\n // The two families validate contributes: the frontmatter checks read only\n // intent.md, and a requirement with no scenario is a requirement in a spec\n // delta. Both name exactly one file, so both may turn a row `draft`.\n if (code.startsWith(\"frontmatter.\")) return \"intent\";\n if (code === \"requirements.missing-scenarios\") return \"spec\";\n // The authoring gate names exactly one file per finding too — leaving these\n // unmapped had the table calling intent.md `done` (\"nothing is owed here\")\n // while the archive exited 1 because it says nothing. A placeholder finding\n // carries no subject when it is the C4 delta's scaffolded description; with a\n // subject it is a SPEC DELTA IN EITHER CORPUS — a service id when the text\n // sits in `specs/<svc>/`, a capability id when it sits in\n // `capabilities/<id>/` — which is why `spec` here is resolved by membership\n // below and must not be narrowed to the service list.\n if (code === \"intent.empty\") return \"intent\";\n if (code === \"scaffold.placeholder\") return subject === undefined ? \"delta\" : \"spec\";\n return null;\n}\n\n/**\n * The artifact table. Every entry is graded by two questions in order — is it\n * there, and does anything the shared checks reported name it — so a file that\n * exists and is wrong can never be reported the way one that was never written\n * is.\n */\n/** What the grading of one feature's files needs beyond the files themselves. */\nexport interface Grading {\n /** The findings that make an artifact `draft` rather than `done`. */\n blocking: Finding[];\n verification: VerificationState;\n /** Operations some OTHER feature's contract already holds. */\n contracted: ReadonlySet<string>;\n /** Operations this feature's own requirements govern. */\n governs: ReadonlySet<string>;\n /** The enumeration's entries by id — where each living service is and what it has (owesContract reads both). */\n living: ReadonlyMap<string, ServiceEntry>;\n /**\n * The capability documents this feature's delta carries. Empty for every\n * feature in a fleet that has not adopted the business axis, which is what\n * keeps the rows out of those payloads entirely.\n */\n capabilities: readonly CapabilityDoc[];\n}\n\nexport function featureArtifacts(\n docsDir: DocsDir,\n feature: FeatureEntry,\n services: readonly PathableService[],\n grading: Grading,\n): ArtifactState[] {\n const { blocking, verification, contracted, governs, living, capabilities } = grading;\n const paths = featurePaths(feature.dir);\n const rel = (abs: string): string => repoPath(docsDir, abs);\n const faults = blocking.map((f) => ({ artifact: faultedArtifact(f.code, f.subject), subject: f.subject }));\n const faulted = (id: ArtifactId, service: string | null): boolean =>\n faults.some((f) => f.artifact === id && (service === null || f.subject === service));\n\n const out: ArtifactState[] = [\n fileState(\"intent\", { service: null, path: rel(paths.intent), exists: existsSync(paths.intent) }, true, faulted(\"intent\", null)),\n fileState(\"delta\", { service: null, path: rel(paths.delta), exists: existsSync(paths.delta) }, false, faulted(\"delta\", null)),\n ];\n\n // One row per capability document the feature carries, and none at all when\n // it carries none: the row is the answer to \"which file do I edit\", and a\n // fleet that has not adopted the axis has no such file to name. Never\n // `required` — no feature owes a business change — so absence never shows up\n // as work owed, and only a fault turns one `draft`. A `delta.*` fault carries\n // the capability id in `subject` with nothing saying it is not a service, so\n // the membership test is the resolution the code family cannot give.\n for (const doc of capabilities) {\n out.push({\n id: \"capabilities\",\n service: null,\n capability: doc.id,\n path: rel(doc.spec),\n exists: existsSync(doc.spec),\n required: false,\n status: faulted(\"capabilities\", doc.id) || faulted(\"spec\", doc.id) ? \"draft\" : \"done\",\n blockedBy: [],\n });\n }\n\n for (const svc of services) {\n const p = featureSpecPaths(feature.dir, svc);\n const specFault = faulted(\"spec\", svc);\n // What a service delta owes is a REQUIREMENT DELTA, not specifically the\n // business one. `arch.spec.md` is the other corpus, with the same grammar\n // and the same delta algebra, and `featureChecklist` derives claims from\n // it — so a directory holding only the architectural document is a\n // complete delta, and every gate already reads it as one: `coherence`'s\n // `service.no-requirement-delta` counts the DIRECTORY, and\n // `validate --feature` grades an arch-only delta clean at exit 0.\n //\n // Requiring `spec.md` unconditionally made this module the one document\n // that disagreed. An architecture-only feature — an outbox added, a retry\n // policy, an alert; the changes `arch.spec.md` exists for because no\n // business scenario was ever going to carry them — reported `missing` and\n // emitted `next.author-spec`, sending the author to write business\n // requirements the change does not have, about a feature the gate accepts.\n // status may be redder than the gates on a question they also ask; it may\n // not invent an obligation they do not.\n const archExists = existsSync(p.archSpec);\n out.push(fileState(\"spec\", { service: svc, path: rel(p.spec), exists: existsSync(p.spec) }, !archExists, specFault));\n out.push(fileState(\"arch-spec\", { service: svc, path: rel(p.archSpec), exists: archExists }, false, specFault));\n out.push(\n fileState(\n \"openapi\",\n { service: svc, path: rel(p.openapi), exists: existsSync(p.openapi) },\n owesContract(living.get(svc), contracted.has(svc), governs.has(svc)),\n faulted(\"openapi\", svc),\n ),\n );\n // Never required: an event contract is genuinely optional — the axis's own\n // absence-grading rests on that — so absence is `done`, exactly like\n // arch.spec.md. Present and faulted still turns the row `draft`.\n out.push(\n fileState(\n \"asyncapi\",\n { service: svc, path: rel(p.asyncapi), exists: existsSync(p.asyncapi) },\n false,\n faulted(\"asyncapi\", svc),\n ),\n );\n }\n\n // The record is the one artifact with a real prerequisite: its checklist is\n // derived from the delta and the per-service deltas, so with none of them\n // written there is nothing to answer — authoring one is impossible rather\n // than merely premature, which is the whole distinction `blocked` carries.\n //\n // `capabilities` is excluded for exactly that reason and not by oversight:\n // `featureChecklist` derives no claim from a capability document — a business\n // promise is realized by service requirements, and those are what the record\n // answers — so counting one as a feeder would report a capability-only\n // feature as ready to verify against a checklist with nothing in it.\n //\n // `arch-spec` used to be excluded beside it, and that failed the same test\n // the comment above states: `featureChecklist` DOES read `arch.spec.md`\n // (`core/verify/checklist.ts`), and `loam verify` on an architecture-only\n // feature returns a real checklist. The row said `blocked … needs delta,\n // spec, openapi, asyncapi` and `next.verify` said there was nothing to derive\n // a checklist from, about a command that derives one — which is the failure\n // `blocked` is supposed to prevent, pointed the wrong way.\n const feeders = out.filter((a) => a.id !== \"intent\" && a.id !== \"capabilities\" && a.exists);\n out.push({\n id: \"verification\",\n service: null,\n path: rel(verificationPath(feature.dir)),\n exists: verification.state !== \"absent\",\n required: true,\n status: feeders.length === 0 ? \"blocked\" : verificationStatus(verification),\n blockedBy: feeders.length === 0 ? [\"delta\", \"spec\", \"arch-spec\", \"openapi\", \"asyncapi\"] : [],\n });\n return out;\n}\n\n/** Where an artifact is and whether it is there — the three facts a grade reads. */\ninterface FileFacts {\n service: string | null;\n path: string;\n exists: boolean;\n}\n\nfunction fileState(id: ArtifactId, file: FileFacts, required: boolean, faulted: boolean): ArtifactState {\n const { service, path, exists } = file;\n const status: ArtifactStatus = !exists\n ? required\n ? \"missing\"\n : \"done\"\n : faulted\n ? \"draft\"\n : \"done\";\n return { id, service, path, exists, required, status, blockedBy: [] };\n}\n"]}
@@ -143,7 +143,17 @@ async function fleetFeature(docsDir, feature, view) {
143
143
  const governs = governedServices(await scanDeltas(docsDir, feature, feature.services, context));
144
144
  for (const svc of feature.services) {
145
145
  const p = featureSpecPaths(feature.dir, svc);
146
- if (!existsSync(p.spec))
146
+ // EITHER corpus satisfies it. What a service delta owes is a requirement
147
+ // delta, and `arch.spec.md` is the other one — same grammar, same delta
148
+ // algebra, read by `featureChecklist` — so an architecture-only feature is
149
+ // complete and every gate already says so. This form asked for `spec.md`
150
+ // specifically, which broke the rule the comment two lines down states
151
+ // about the contract question: the two forms disagreed, `loam status`
152
+ // reporting `missing <svc>/spec` over a feature `loam status <FEAT>` and
153
+ // `loam validate --feature` both called complete. The name stays
154
+ // `<svc>/spec` when NEITHER exists, because the business delta is the one
155
+ // an author writes by default.
156
+ if (!existsSync(p.spec) && !existsSync(p.archSpec))
147
157
  missing.push(`${svc}/spec`);
148
158
  // The same question featureArtifacts asks, through the same function: the
149
159
  // two forms must never disagree about whether a feature owes a contract.
@@ -1 +1 @@
1
- {"version":3,"file":"fleet.js","sourceRoot":"","sources":["../../../../src/core/status/fleet/fleet.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The whole repository — why the fleet form is cheaper than the feature form.\n *\n * `loam status` with no feature runs over every active feature, and deriving a\n * coherence report or a verification checklist per feature means spinning a\n * fresh LikeC4 workspace per feature. On a fleet-sized repository that is the\n * cost that gets a command switched off — `list` made the same call about its\n * verification column. So the fleet form grades nothing: it reports artifact\n * PRESENCE, the dependency order, and a verification record only where one\n * already exists, and its `next[]` hands off to `loam status <FEAT>`, which is\n * where the checks actually run.\n */\nimport { existsSync } from \"node:fs\";\nimport { readFile } from \"node:fs/promises\";\nimport { analyzeDependencies } from \"../../dependencies/dependencies.js\";\nimport { type DependencyGraph } from \"../../dependencies/facts.js\";\nimport { isLandscapeStub } from \"../../scaffold/landscape.js\";\nimport { repoPath } from \"../../envelope/json.js\";\nimport { FleetContext } from \"../../fleet-context.js\";\nimport { compareIds, type FeatureEntry, type ServiceEntry } from \"../../repo/entries.js\";\nimport { featurePaths, featureSpecPaths, landscapePath } from \"../../repo/paths.js\";\nimport { listFeatures, listServices } from \"../../repo/repo.js\";\nimport { contractOwners, contractsHeldElsewhere, owesContract } from \"../contracts.js\";\nimport { readInterruptedCommit } from \"../interrupted.js\";\nimport {\n undocumented,\n vouched,\n type ArtifactStatus,\n type FleetFeatureState,\n type FleetStatusReport,\n type InterruptedCommit,\n} from \"../report.js\";\nimport { governedServices, scanDeltas } from \"../scan.js\";\nimport { fullyVerified, verificationState } from \"../verification.js\";\nimport { fleetNext, type Binding } from \"./next.js\";\nimport type { DocsDir } from \"../../kernel/ids/dirs.js\";\n\n/**\n * The fleet view: how much of `services/` anyone has written down, every\n * feature in flight with the stage it has reached, and the order they have to\n * land in. Nothing here is graded — the module header says why — so the stages\n * are drawn from artifact presence and from a record that already exists, and\n * `next[]` hands off to `loam status <FEAT>` for everything else.\n */\nexport async function fleetStatus(\n docsDir: DocsDir,\n opts: { service?: string; bound?: string; context?: FleetContext } = {},\n): Promise<FleetStatusReport> {\n const context = opts.context ?? new FleetContext();\n const interrupted = await readInterruptedCommit(docsDir);\n const narrowed = opts.service;\n const all = await listServices(docsDir, context);\n const services = narrowed === undefined ? all : all.filter((s) => s.id === narrowed);\n // The service this repository says it IS, and whether the fleet has heard of\n // it. Asked against the UNNARROWED list, because \"is it adopted\" is a question\n // about the fleet and not about the view.\n //\n // Both facts travel, where only the unadopted case used to. This value was a\n // `string | null` that went null exactly when the service directory EXISTED,\n // so the walk could see the repository it stands in only while that\n // repository was undocumented — and a developer in an adopted service repo\n // got the whole fleet's worklist with their own boundary buried in it.\n const binding: Binding | null =\n opts.bound === undefined ? null : { id: opts.bound, adopted: all.some((s) => s.id === opts.bound) };\n const graph = await analyzeDependencies(docsDir, undefined, context);\n const entries = await listFeatures(docsDir, {}, context);\n const inScope = narrowed === undefined ? entries : entries.filter((f) => f.services.some((s) => s === narrowed));\n // Once for the fleet, not once per feature: which features hold a contract\n // for which service is one fact about the repository, and every feature below\n // reads its own row out of it. Built over the UNNARROWED list on purpose — a\n // `--service` view still has to know that a feature outside it discharges the\n // contract, or the narrowing would invent an obligation.\n const owners = await contractOwners(docsDir, context);\n\n const features = await Promise.all(\n inScope.map((f) =>\n fleetFeature(docsDir, f, {\n graph,\n contracted: contractsHeldElsewhere(owners, f.id),\n living: new Map(all.map((s) => [s.id, s])),\n context,\n interrupted,\n }),\n ),\n );\n features.sort((a, b) => compareIds(a.id, b.id));\n\n // The first-hour ladder's fact, computed exactly when the ladder can apply\n // — an unnarrowed run over a fleet with zero services and zero features —\n // so the read disappears from every repository that has anything in it.\n // Withheld on narrowed runs for the same reason the binding is withheld\n // below: `--service X` is an explicit question about X, and the\n // repository's onboarding ladder would be a different question's answer.\n const teaching =\n narrowed === undefined && services.length === 0 && features.length === 0\n ? { landscape: await landscapeTeaching(docsDir) }\n : null;\n\n return {\n interrupted,\n services: {\n total: services.length,\n undocumented: services.filter(undocumented).length,\n draft: services.filter((s) => !undocumented(s) && !vouched(s)).length,\n vouched: services.filter(vouched).length,\n // A subset of `vouched`, never a fourth bucket: those services ARE\n // vouched, and the three counts still sum to `total`. It rides beside\n // the count it qualifies because this line is the fleet's trust\n // headline, and \"118 vouched\" over a fleet where 90 of them were read\n // from a sample is the misreading the scope exists to prevent.\n sampledVouched: services.filter((s) => vouched(s) && s.vouchScope === \"sampled\").length,\n },\n features,\n order: graph.order.filter((id) => features.some((f) => f.id === id)),\n service: narrowed ?? null,\n // The binding is passed only when this run was NOT narrowed, and that guard\n // is unchanged now that the binding also decides ORDER. `--service X` is an\n // explicit question about X, and answering it with a step about whichever\n // service loam.json happens to name would be a different question's answer\n // at the top of the list — which is as true of a rung promoted to the front\n // as it was of `next.adopt-bound` emitted out of nowhere. A narrowed run\n // sees at most the one service it asked about anyway, so withholding the\n // binding here costs the reader nothing and keeps `--service X` answering\n // only about X.\n next: fleetNext(\n { services, features, graph, interrupted, teaching },\n narrowed === undefined ? binding : null,\n ),\n };\n}\n\n/**\n * Which first-hour state the fleet map is in: absent, still the scaffold's own\n * untouched bytes, or authored. One file read and a byte comparison — the\n * module header's cost rule stands, no LikeC4 workspace is spun here.\n */\nasync function landscapeTeaching(docsDir: DocsDir): Promise<\"missing\" | \"stub\" | \"authored\"> {\n const lp = landscapePath(docsDir);\n if (!existsSync(lp)) return \"missing\";\n try {\n return isLandscapeStub(await readFile(lp, \"utf8\")) ? \"stub\" : \"authored\";\n } catch {\n // Unreadable is not untouched. A map that cannot be read — a directory in\n // the file's place, a permission — must not be called the scaffold's stub\n // (\"missing\" and \"stub\" would each put a false statement in next[]), so it\n // reads as \"authored\", which only drops the one teaching step: the gate\n // entry still names `loam validate --all`, where an unreadable landscape\n // is actually graded, and letting this read throw would hand the whole\n // orientation surface — a command that reports and never gates — to the\n // repository-unavailable refusal over a file status does not even parse.\n return \"authored\";\n }\n}\n\n/** What a feature's fleet row needs about the fleet AROUND it. */\ninterface FleetView {\n graph: DependencyGraph;\n /** Operations some other feature's contract already holds. */\n contracted: ReadonlySet<string>;\n /** The enumeration's entries by id — where each living service is and what it has (owesContract reads both). */\n living: ReadonlyMap<string, ServiceEntry>;\n context: FleetContext;\n interrupted: InterruptedCommit | null;\n}\n\nasync function fleetFeature(\n docsDir: DocsDir,\n feature: FeatureEntry,\n view: FleetView,\n): Promise<FleetFeatureState> {\n const { graph, contracted, living, context, interrupted } = view;\n const paths = featurePaths(feature.dir);\n const missing: string[] = [];\n if (!existsSync(paths.intent)) missing.push(\"intent\");\n if (feature.services.length === 0) missing.push(\"spec\");\n // Requirement text, not a coherence run: `Operations:` lines are read off the\n // same cached parse `list` already pays for, and the contract question needs\n // them (owesContract). No LikeC4 workspace is spun here, which is the cost\n // this form actually refuses.\n const governs = governedServices(await scanDeltas(docsDir, feature, feature.services, context));\n for (const svc of feature.services) {\n const p = featureSpecPaths(feature.dir, svc);\n if (!existsSync(p.spec)) missing.push(`${svc}/spec`);\n // The same question featureArtifacts asks, through the same function: the\n // two forms must never disagree about whether a feature owes a contract.\n if (owesContract(living.get(svc), contracted.has(svc), governs.has(svc)) && !existsSync(p.openapi)) {\n missing.push(`${svc}/openapi`);\n }\n }\n const verification = (await verificationState(docsDir, feature)).state;\n const blockedBy = graph.nodes.find((n) => n.id === feature.id)?.dependsOn ?? [];\n\n // No `draft` here, ever: that verdict needs the coherence run this form\n // refuses to pay for, and inferring it from presence alone would be a second\n // opinion about validity — the one thing status must never invent.\n //\n // The interrupted commit leads, exactly as it does in the feature form: every\n // presence test below is a question about files a killed `archive` may have\n // half-written, so answering `done` from them is a claim about bytes nobody\n // has established. Without this the two forms contradicted each other over\n // one repository — `loam status --json` said `done` and offered \"ship it\"\n // while `loam status <FEAT>` said `blocked` — and the rule in report.ts's\n // header is that this projection is never greener than the gates.\n const stage: ArtifactStatus =\n interrupted !== null\n ? \"blocked\"\n : missing.length > 0\n ? \"missing\"\n : blockedBy.length > 0\n ? \"blocked\"\n : fullyVerified(verification)\n ? \"done\"\n : \"ready\";\n\n return {\n id: feature.id,\n dirName: feature.dirName,\n path: repoPath(docsDir, feature.dir),\n stage,\n services: feature.services,\n blockedBy,\n missing,\n verification,\n };\n}\n"]}
1
+ {"version":3,"file":"fleet.js","sourceRoot":"","sources":["../../../../src/core/status/fleet/fleet.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The whole repository — why the fleet form is cheaper than the feature form.\n *\n * `loam status` with no feature runs over every active feature, and deriving a\n * coherence report or a verification checklist per feature means spinning a\n * fresh LikeC4 workspace per feature. On a fleet-sized repository that is the\n * cost that gets a command switched off — `list` made the same call about its\n * verification column. So the fleet form grades nothing: it reports artifact\n * PRESENCE, the dependency order, and a verification record only where one\n * already exists, and its `next[]` hands off to `loam status <FEAT>`, which is\n * where the checks actually run.\n */\nimport { existsSync } from \"node:fs\";\nimport { readFile } from \"node:fs/promises\";\nimport { analyzeDependencies } from \"../../dependencies/dependencies.js\";\nimport { type DependencyGraph } from \"../../dependencies/facts.js\";\nimport { isLandscapeStub } from \"../../scaffold/landscape.js\";\nimport { repoPath } from \"../../envelope/json.js\";\nimport { FleetContext } from \"../../fleet-context.js\";\nimport { compareIds, type FeatureEntry, type ServiceEntry } from \"../../repo/entries.js\";\nimport { featurePaths, featureSpecPaths, landscapePath } from \"../../repo/paths.js\";\nimport { listFeatures, listServices } from \"../../repo/repo.js\";\nimport { contractOwners, contractsHeldElsewhere, owesContract } from \"../contracts.js\";\nimport { readInterruptedCommit } from \"../interrupted.js\";\nimport {\n undocumented,\n vouched,\n type ArtifactStatus,\n type FleetFeatureState,\n type FleetStatusReport,\n type InterruptedCommit,\n} from \"../report.js\";\nimport { governedServices, scanDeltas } from \"../scan.js\";\nimport { fullyVerified, verificationState } from \"../verification.js\";\nimport { fleetNext, type Binding } from \"./next.js\";\nimport type { DocsDir } from \"../../kernel/ids/dirs.js\";\n\n/**\n * The fleet view: how much of `services/` anyone has written down, every\n * feature in flight with the stage it has reached, and the order they have to\n * land in. Nothing here is graded — the module header says why — so the stages\n * are drawn from artifact presence and from a record that already exists, and\n * `next[]` hands off to `loam status <FEAT>` for everything else.\n */\nexport async function fleetStatus(\n docsDir: DocsDir,\n opts: { service?: string; bound?: string; context?: FleetContext } = {},\n): Promise<FleetStatusReport> {\n const context = opts.context ?? new FleetContext();\n const interrupted = await readInterruptedCommit(docsDir);\n const narrowed = opts.service;\n const all = await listServices(docsDir, context);\n const services = narrowed === undefined ? all : all.filter((s) => s.id === narrowed);\n // The service this repository says it IS, and whether the fleet has heard of\n // it. Asked against the UNNARROWED list, because \"is it adopted\" is a question\n // about the fleet and not about the view.\n //\n // Both facts travel, where only the unadopted case used to. This value was a\n // `string | null` that went null exactly when the service directory EXISTED,\n // so the walk could see the repository it stands in only while that\n // repository was undocumented — and a developer in an adopted service repo\n // got the whole fleet's worklist with their own boundary buried in it.\n const binding: Binding | null =\n opts.bound === undefined ? null : { id: opts.bound, adopted: all.some((s) => s.id === opts.bound) };\n const graph = await analyzeDependencies(docsDir, undefined, context);\n const entries = await listFeatures(docsDir, {}, context);\n const inScope = narrowed === undefined ? entries : entries.filter((f) => f.services.some((s) => s === narrowed));\n // Once for the fleet, not once per feature: which features hold a contract\n // for which service is one fact about the repository, and every feature below\n // reads its own row out of it. Built over the UNNARROWED list on purpose — a\n // `--service` view still has to know that a feature outside it discharges the\n // contract, or the narrowing would invent an obligation.\n const owners = await contractOwners(docsDir, context);\n\n const features = await Promise.all(\n inScope.map((f) =>\n fleetFeature(docsDir, f, {\n graph,\n contracted: contractsHeldElsewhere(owners, f.id),\n living: new Map(all.map((s) => [s.id, s])),\n context,\n interrupted,\n }),\n ),\n );\n features.sort((a, b) => compareIds(a.id, b.id));\n\n // The first-hour ladder's fact, computed exactly when the ladder can apply\n // — an unnarrowed run over a fleet with zero services and zero features —\n // so the read disappears from every repository that has anything in it.\n // Withheld on narrowed runs for the same reason the binding is withheld\n // below: `--service X` is an explicit question about X, and the\n // repository's onboarding ladder would be a different question's answer.\n const teaching =\n narrowed === undefined && services.length === 0 && features.length === 0\n ? { landscape: await landscapeTeaching(docsDir) }\n : null;\n\n return {\n interrupted,\n services: {\n total: services.length,\n undocumented: services.filter(undocumented).length,\n draft: services.filter((s) => !undocumented(s) && !vouched(s)).length,\n vouched: services.filter(vouched).length,\n // A subset of `vouched`, never a fourth bucket: those services ARE\n // vouched, and the three counts still sum to `total`. It rides beside\n // the count it qualifies because this line is the fleet's trust\n // headline, and \"118 vouched\" over a fleet where 90 of them were read\n // from a sample is the misreading the scope exists to prevent.\n sampledVouched: services.filter((s) => vouched(s) && s.vouchScope === \"sampled\").length,\n },\n features,\n order: graph.order.filter((id) => features.some((f) => f.id === id)),\n service: narrowed ?? null,\n // The binding is passed only when this run was NOT narrowed, and that guard\n // is unchanged now that the binding also decides ORDER. `--service X` is an\n // explicit question about X, and answering it with a step about whichever\n // service loam.json happens to name would be a different question's answer\n // at the top of the list — which is as true of a rung promoted to the front\n // as it was of `next.adopt-bound` emitted out of nowhere. A narrowed run\n // sees at most the one service it asked about anyway, so withholding the\n // binding here costs the reader nothing and keeps `--service X` answering\n // only about X.\n next: fleetNext(\n { services, features, graph, interrupted, teaching },\n narrowed === undefined ? binding : null,\n ),\n };\n}\n\n/**\n * Which first-hour state the fleet map is in: absent, still the scaffold's own\n * untouched bytes, or authored. One file read and a byte comparison — the\n * module header's cost rule stands, no LikeC4 workspace is spun here.\n */\nasync function landscapeTeaching(docsDir: DocsDir): Promise<\"missing\" | \"stub\" | \"authored\"> {\n const lp = landscapePath(docsDir);\n if (!existsSync(lp)) return \"missing\";\n try {\n return isLandscapeStub(await readFile(lp, \"utf8\")) ? \"stub\" : \"authored\";\n } catch {\n // Unreadable is not untouched. A map that cannot be read — a directory in\n // the file's place, a permission — must not be called the scaffold's stub\n // (\"missing\" and \"stub\" would each put a false statement in next[]), so it\n // reads as \"authored\", which only drops the one teaching step: the gate\n // entry still names `loam validate --all`, where an unreadable landscape\n // is actually graded, and letting this read throw would hand the whole\n // orientation surface — a command that reports and never gates — to the\n // repository-unavailable refusal over a file status does not even parse.\n return \"authored\";\n }\n}\n\n/** What a feature's fleet row needs about the fleet AROUND it. */\ninterface FleetView {\n graph: DependencyGraph;\n /** Operations some other feature's contract already holds. */\n contracted: ReadonlySet<string>;\n /** The enumeration's entries by id — where each living service is and what it has (owesContract reads both). */\n living: ReadonlyMap<string, ServiceEntry>;\n context: FleetContext;\n interrupted: InterruptedCommit | null;\n}\n\nasync function fleetFeature(\n docsDir: DocsDir,\n feature: FeatureEntry,\n view: FleetView,\n): Promise<FleetFeatureState> {\n const { graph, contracted, living, context, interrupted } = view;\n const paths = featurePaths(feature.dir);\n const missing: string[] = [];\n if (!existsSync(paths.intent)) missing.push(\"intent\");\n if (feature.services.length === 0) missing.push(\"spec\");\n // Requirement text, not a coherence run: `Operations:` lines are read off the\n // same cached parse `list` already pays for, and the contract question needs\n // them (owesContract). No LikeC4 workspace is spun here, which is the cost\n // this form actually refuses.\n const governs = governedServices(await scanDeltas(docsDir, feature, feature.services, context));\n for (const svc of feature.services) {\n const p = featureSpecPaths(feature.dir, svc);\n // EITHER corpus satisfies it. What a service delta owes is a requirement\n // delta, and `arch.spec.md` is the other one — same grammar, same delta\n // algebra, read by `featureChecklist` — so an architecture-only feature is\n // complete and every gate already says so. This form asked for `spec.md`\n // specifically, which broke the rule the comment two lines down states\n // about the contract question: the two forms disagreed, `loam status`\n // reporting `missing <svc>/spec` over a feature `loam status <FEAT>` and\n // `loam validate --feature` both called complete. The name stays\n // `<svc>/spec` when NEITHER exists, because the business delta is the one\n // an author writes by default.\n if (!existsSync(p.spec) && !existsSync(p.archSpec)) missing.push(`${svc}/spec`);\n // The same question featureArtifacts asks, through the same function: the\n // two forms must never disagree about whether a feature owes a contract.\n if (owesContract(living.get(svc), contracted.has(svc), governs.has(svc)) && !existsSync(p.openapi)) {\n missing.push(`${svc}/openapi`);\n }\n }\n const verification = (await verificationState(docsDir, feature)).state;\n const blockedBy = graph.nodes.find((n) => n.id === feature.id)?.dependsOn ?? [];\n\n // No `draft` here, ever: that verdict needs the coherence run this form\n // refuses to pay for, and inferring it from presence alone would be a second\n // opinion about validity — the one thing status must never invent.\n //\n // The interrupted commit leads, exactly as it does in the feature form: every\n // presence test below is a question about files a killed `archive` may have\n // half-written, so answering `done` from them is a claim about bytes nobody\n // has established. Without this the two forms contradicted each other over\n // one repository — `loam status --json` said `done` and offered \"ship it\"\n // while `loam status <FEAT>` said `blocked` — and the rule in report.ts's\n // header is that this projection is never greener than the gates.\n const stage: ArtifactStatus =\n interrupted !== null\n ? \"blocked\"\n : missing.length > 0\n ? \"missing\"\n : blockedBy.length > 0\n ? \"blocked\"\n : fullyVerified(verification)\n ? \"done\"\n : \"ready\";\n\n return {\n id: feature.id,\n dirName: feature.dirName,\n path: repoPath(docsDir, feature.dir),\n stage,\n services: feature.services,\n blockedBy,\n missing,\n verification,\n };\n}\n"]}