@ybotok/loam 0.0.0 → 0.1.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. package/CHANGELOG.md +138 -0
  2. package/COMPARISON.md +63 -0
  3. package/LICENSE +21 -0
  4. package/MIGRATING-from-OpenSpec.md +202 -0
  5. package/README.md +246 -9
  6. package/SCHEMA.md +425 -0
  7. package/SECURITY.md +9 -0
  8. package/dist/cli.js +142 -0
  9. package/dist/cli.js.map +1 -0
  10. package/dist/commands/adopt.js +240 -0
  11. package/dist/commands/adopt.js.map +1 -0
  12. package/dist/commands/archive.js +1313 -0
  13. package/dist/commands/archive.js.map +1 -0
  14. package/dist/commands/delta.js +388 -0
  15. package/dist/commands/delta.js.map +1 -0
  16. package/dist/commands/dependencies.js +83 -0
  17. package/dist/commands/dependencies.js.map +1 -0
  18. package/dist/commands/docs-repo-gate.js +104 -0
  19. package/dist/commands/docs-repo-gate.js.map +1 -0
  20. package/dist/commands/doctor.js +85 -0
  21. package/dist/commands/doctor.js.map +1 -0
  22. package/dist/commands/explore.js +195 -0
  23. package/dist/commands/explore.js.map +1 -0
  24. package/dist/commands/format.js +17 -0
  25. package/dist/commands/format.js.map +1 -0
  26. package/dist/commands/gherkin.js +309 -0
  27. package/dist/commands/gherkin.js.map +1 -0
  28. package/dist/commands/init.js +250 -0
  29. package/dist/commands/init.js.map +1 -0
  30. package/dist/commands/instructions.js +70 -0
  31. package/dist/commands/instructions.js.map +1 -0
  32. package/dist/commands/list.js +291 -0
  33. package/dist/commands/list.js.map +1 -0
  34. package/dist/commands/migrate-openspec.js +950 -0
  35. package/dist/commands/migrate-openspec.js.map +1 -0
  36. package/dist/commands/new.js +413 -0
  37. package/dist/commands/new.js.map +1 -0
  38. package/dist/commands/rebase.js +356 -0
  39. package/dist/commands/rebase.js.map +1 -0
  40. package/dist/commands/show.js +320 -0
  41. package/dist/commands/show.js.map +1 -0
  42. package/dist/commands/status.js +254 -0
  43. package/dist/commands/status.js.map +1 -0
  44. package/dist/commands/unarchive.js +317 -0
  45. package/dist/commands/unarchive.js.map +1 -0
  46. package/dist/commands/validate.js +1591 -0
  47. package/dist/commands/validate.js.map +1 -0
  48. package/dist/commands/verify.js +775 -0
  49. package/dist/commands/verify.js.map +1 -0
  50. package/dist/commands/vouch.js +343 -0
  51. package/dist/commands/vouch.js.map +1 -0
  52. package/dist/core/agent.js +2065 -0
  53. package/dist/core/agent.js.map +1 -0
  54. package/dist/core/agents-stamp.js +68 -0
  55. package/dist/core/agents-stamp.js.map +1 -0
  56. package/dist/core/arch.js +130 -0
  57. package/dist/core/arch.js.map +1 -0
  58. package/dist/core/brief.js +733 -0
  59. package/dist/core/brief.js.map +1 -0
  60. package/dist/core/coherence.js +577 -0
  61. package/dist/core/coherence.js.map +1 -0
  62. package/dist/core/concurrency.js +51 -0
  63. package/dist/core/concurrency.js.map +1 -0
  64. package/dist/core/config.js +180 -0
  65. package/dist/core/config.js.map +1 -0
  66. package/dist/core/delta.js +476 -0
  67. package/dist/core/delta.js.map +1 -0
  68. package/dist/core/dependencies.js +356 -0
  69. package/dist/core/dependencies.js.map +1 -0
  70. package/dist/core/docs.js +144 -0
  71. package/dist/core/docs.js.map +1 -0
  72. package/dist/core/doctor.js +559 -0
  73. package/dist/core/doctor.js.map +1 -0
  74. package/dist/core/document-bytes.js +59 -0
  75. package/dist/core/document-bytes.js.map +1 -0
  76. package/dist/core/explore.js +225 -0
  77. package/dist/core/explore.js.map +1 -0
  78. package/dist/core/fleet-context.js +225 -0
  79. package/dist/core/fleet-context.js.map +1 -0
  80. package/dist/core/frontmatter.js +199 -0
  81. package/dist/core/frontmatter.js.map +1 -0
  82. package/dist/core/gherkin.js +463 -0
  83. package/dist/core/gherkin.js.map +1 -0
  84. package/dist/core/health.js +67 -0
  85. package/dist/core/health.js.map +1 -0
  86. package/dist/core/ids.js +122 -0
  87. package/dist/core/ids.js.map +1 -0
  88. package/dist/core/issue.js +9 -0
  89. package/dist/core/issue.js.map +1 -0
  90. package/dist/core/json.js +74 -0
  91. package/dist/core/json.js.map +1 -0
  92. package/dist/core/likec4.js +479 -0
  93. package/dist/core/likec4.js.map +1 -0
  94. package/dist/core/maturity.js +66 -0
  95. package/dist/core/maturity.js.map +1 -0
  96. package/dist/core/openapi-merge.js +552 -0
  97. package/dist/core/openapi-merge.js.map +1 -0
  98. package/dist/core/openapi.js +255 -0
  99. package/dist/core/openapi.js.map +1 -0
  100. package/dist/core/openspec-inventory.js +1440 -0
  101. package/dist/core/openspec-inventory.js.map +1 -0
  102. package/dist/core/path-safety.js +168 -0
  103. package/dist/core/path-safety.js.map +1 -0
  104. package/dist/core/provenance.js +720 -0
  105. package/dist/core/provenance.js.map +1 -0
  106. package/dist/core/records.js +33 -0
  107. package/dist/core/records.js.map +1 -0
  108. package/dist/core/repo.js +436 -0
  109. package/dist/core/repo.js.map +1 -0
  110. package/dist/core/report.js +47 -0
  111. package/dist/core/report.js.map +1 -0
  112. package/dist/core/results.js +199 -0
  113. package/dist/core/results.js.map +1 -0
  114. package/dist/core/spec.js +481 -0
  115. package/dist/core/spec.js.map +1 -0
  116. package/dist/core/staging.js +890 -0
  117. package/dist/core/staging.js.map +1 -0
  118. package/dist/core/status.js +1104 -0
  119. package/dist/core/status.js.map +1 -0
  120. package/dist/core/steps.js +72 -0
  121. package/dist/core/steps.js.map +1 -0
  122. package/dist/core/verify.js +807 -0
  123. package/dist/core/verify.js.map +1 -0
  124. package/dist/core/version.js +24 -0
  125. package/dist/core/version.js.map +1 -0
  126. package/package.json +63 -4
@@ -0,0 +1,122 @@
1
+ /**
2
+ * The spelling rules for the ids loam turns into directory names.
3
+ *
4
+ * A service id is not a label: it becomes `services/<id>/` in the shared docs
5
+ * repo, and it is interpolated into paths by adopt, validate, vouch, gherkin and
6
+ * new. That makes it caller-controlled path input, so it is validated in exactly
7
+ * one place — here — rather than by each command inventing its own guard. The
8
+ * failure mode this closes is not theoretical: `--service ../../etc` would have
9
+ * had `servicePaths()` hand a writer a directory outside the docs repo, and
10
+ * `--service ''` would have made `services//spec.md` collapse to a file the
11
+ * enumeration in repo.ts can never see again.
12
+ *
13
+ * The rule is deliberately narrower than "a path segment that happens to work":
14
+ * ids are also compared, sorted and printed in tables, so shell metacharacters,
15
+ * spaces and leading dots are refused even where the filesystem would accept
16
+ * them. Widening the alphabet later is a compatible change; narrowing it is not.
17
+ *
18
+ * "Works" also means "works on Windows", which this project tests in CI: the
19
+ * two rules below are the ones POSIX accepts and Windows does not. They used to
20
+ * live in migrate-openspec.ts alone, as a second, stricter copy of this grammar
21
+ * — so the PRIMARY authoring path (`--service`, `--touches`, `adopt`) accepted
22
+ * ids the migration refused, and `services/CON/` or `services/payments./` got
23
+ * as far as a directory nobody can create or open. Narrowing is not a
24
+ * compatible change, which is exactly why it belongs here, once, rather than in
25
+ * whichever command happened to think of it.
26
+ */
27
+ /** The one grammar: alphanumeric head, then alphanumerics, dot, underscore, hyphen. */
28
+ const SERVICE_ID = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
29
+ /**
30
+ * The MS-DOS device names Windows still reserves. Reserved with any extension
31
+ * and in any case (`nul`, `NUL.txt`), so the test is on the stem — the part
32
+ * before the first dot — uppercased.
33
+ */
34
+ const WINDOWS_DEVICE = /^(?:CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])$/;
35
+ /** Prose form of the rule, shown verbatim in every refusal so the fix is obvious. */
36
+ const SERVICE_ID_RULE = "a service id must start with a letter or digit and contain only letters, digits, '.', '_' or '-' " +
37
+ "(no slashes, no '..', no spaces) — it becomes the services/<id>/ directory name";
38
+ /** Refusal carrying the offending value, so a caller can quote it back. */
39
+ export class InvalidIdError extends Error {
40
+ value;
41
+ constructor(value, message) {
42
+ super(message);
43
+ this.value = value;
44
+ this.name = "InvalidIdError";
45
+ }
46
+ }
47
+ /**
48
+ * Why `id` is not a usable service id, or null when it is. Returning the reason
49
+ * instead of a boolean lets a caller that is already collecting findings report
50
+ * the same sentence a refusal would print.
51
+ */
52
+ export function serviceIdProblem(id, label = "--service") {
53
+ if (typeof id !== "string" || id === "") {
54
+ return `${label} must name a service: ${SERVICE_ID_RULE}.`;
55
+ }
56
+ // Checked ahead of the pattern purely for the message: '..' is the case a
57
+ // human is most likely to have typed on purpose, and "no '..'" is a more
58
+ // useful answer than "does not match the id grammar".
59
+ if (id.split(/[\\/]/).includes("..") || id.includes("..")) {
60
+ return `Invalid ${label} '${id}': ${SERVICE_ID_RULE}.`;
61
+ }
62
+ if (!SERVICE_ID.test(id)) {
63
+ return `Invalid ${label} '${id}': ${SERVICE_ID_RULE}.`;
64
+ }
65
+ // Both refusals below are about Windows, and both get their own sentence for
66
+ // the same reason `..` does: "does not match the id grammar" is true and
67
+ // useless, because the id LOOKS fine and the problem is what the filesystem
68
+ // does with it.
69
+ if (WINDOWS_DEVICE.test(id.split(".")[0].toUpperCase())) {
70
+ return (`Invalid ${label} '${id}': '${id.split(".")[0]}' is a reserved device name on Windows — ` +
71
+ `services/${id}/ cannot be created there at all, so the docs repo would only work on some of the machines that clone it.`);
72
+ }
73
+ if (/[. ]$/.test(id)) {
74
+ return (`Invalid ${label} '${id}': a service id may not end with '.' or a space — ` +
75
+ `Windows strips both when creating a directory, so '${id}' and '${id.replace(/[. ]+$/, "")}' would silently become one directory there and two everywhere else.`);
76
+ }
77
+ return null;
78
+ }
79
+ /**
80
+ * Assert that `id` may be used as a `services/<id>/` directory name. Throws
81
+ * InvalidIdError; commands catch it and report `invalid-option`, because a bad
82
+ * id is always something the caller typed, never a state of the repository.
83
+ */
84
+ export function assertServiceId(id, label = "--service") {
85
+ const problem = serviceIdProblem(id, label);
86
+ if (problem !== null)
87
+ throw new InvalidIdError(typeof id === "string" ? id : String(id), problem);
88
+ }
89
+ /**
90
+ * Feature ids are `<word>-<number>`: the id has to survive being read back off
91
+ * the directory name (`FEAT-101-payment-splitting` -> `FEAT-101`), or the
92
+ * feature would answer to a name it was never given.
93
+ *
94
+ * Here for the same reason the service grammar is — this was spelled twice,
95
+ * privately, in `commands/new.ts` and `core/openspec-inventory.ts`, and
96
+ * docs/DESIGN.md rule 7 recorded the pair as a hazard rather than a fact. The
97
+ * third caller is what made it one: `loam explore --as <FEAT>` interpolates its
98
+ * argument into a `loam new` line that loam PRINTS for an agent to run, so
99
+ * without this check `explore` cheerfully handed back a command `new` refuses.
100
+ * A guard test catches that class only for literal source strings — a command
101
+ * assembled at runtime from argv is invisible to it — which is exactly why the
102
+ * grammar has to be shared rather than re-derived by whoever needs it next.
103
+ */
104
+ const FEATURE_ID = /^[A-Za-z][A-Za-z0-9]*-\d+$/;
105
+ /** Prose form, shown verbatim in every refusal so the fix is obvious. */
106
+ export const FEATURE_ID_RULE = "Expected <word>-<number>, e.g. FEAT-101 or BUG-42.";
107
+ export function isFeatureId(id) {
108
+ return typeof id === "string" && FEATURE_ID.test(id);
109
+ }
110
+ /**
111
+ * Why `id` is not a usable feature id, or null when it is — the `serviceIdProblem`
112
+ * shape, so a caller already collecting findings reports the sentence a refusal
113
+ * would print.
114
+ */
115
+ export function featureIdProblem(id, label = "feature id") {
116
+ if (!isFeatureId(id)) {
117
+ const shown = typeof id === "string" ? `'${id}'` : String(id);
118
+ return `${shown} is not a usable ${label}. ${FEATURE_ID_RULE}`;
119
+ }
120
+ return null;
121
+ }
122
+ //# sourceMappingURL=ids.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ids.js","sourceRoot":"","sources":["../../src/core/ids.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The spelling rules for the ids loam turns into directory names.\n *\n * A service id is not a label: it becomes `services/<id>/` in the shared docs\n * repo, and it is interpolated into paths by adopt, validate, vouch, gherkin and\n * new. That makes it caller-controlled path input, so it is validated in exactly\n * one place — here — rather than by each command inventing its own guard. The\n * failure mode this closes is not theoretical: `--service ../../etc` would have\n * had `servicePaths()` hand a writer a directory outside the docs repo, and\n * `--service ''` would have made `services//spec.md` collapse to a file the\n * enumeration in repo.ts can never see again.\n *\n * The rule is deliberately narrower than \"a path segment that happens to work\":\n * ids are also compared, sorted and printed in tables, so shell metacharacters,\n * spaces and leading dots are refused even where the filesystem would accept\n * them. Widening the alphabet later is a compatible change; narrowing it is not.\n *\n * \"Works\" also means \"works on Windows\", which this project tests in CI: the\n * two rules below are the ones POSIX accepts and Windows does not. They used to\n * live in migrate-openspec.ts alone, as a second, stricter copy of this grammar\n * — so the PRIMARY authoring path (`--service`, `--touches`, `adopt`) accepted\n * ids the migration refused, and `services/CON/` or `services/payments./` got\n * as far as a directory nobody can create or open. Narrowing is not a\n * compatible change, which is exactly why it belongs here, once, rather than in\n * whichever command happened to think of it.\n */\n\n/** The one grammar: alphanumeric head, then alphanumerics, dot, underscore, hyphen. */\nconst SERVICE_ID = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;\n\n/**\n * The MS-DOS device names Windows still reserves. Reserved with any extension\n * and in any case (`nul`, `NUL.txt`), so the test is on the stem — the part\n * before the first dot — uppercased.\n */\nconst WINDOWS_DEVICE = /^(?:CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])$/;\n\n/** Prose form of the rule, shown verbatim in every refusal so the fix is obvious. */\nconst SERVICE_ID_RULE =\n \"a service id must start with a letter or digit and contain only letters, digits, '.', '_' or '-' \" +\n \"(no slashes, no '..', no spaces) — it becomes the services/<id>/ directory name\";\n\n/** Refusal carrying the offending value, so a caller can quote it back. */\nexport class InvalidIdError extends Error {\n constructor(\n readonly value: string,\n message: string,\n ) {\n super(message);\n this.name = \"InvalidIdError\";\n }\n}\n\n/**\n * Why `id` is not a usable service id, or null when it is. Returning the reason\n * instead of a boolean lets a caller that is already collecting findings report\n * the same sentence a refusal would print.\n */\nexport function serviceIdProblem(id: unknown, label = \"--service\"): string | null {\n if (typeof id !== \"string\" || id === \"\") {\n return `${label} must name a service: ${SERVICE_ID_RULE}.`;\n }\n // Checked ahead of the pattern purely for the message: '..' is the case a\n // human is most likely to have typed on purpose, and \"no '..'\" is a more\n // useful answer than \"does not match the id grammar\".\n if (id.split(/[\\\\/]/).includes(\"..\") || id.includes(\"..\")) {\n return `Invalid ${label} '${id}': ${SERVICE_ID_RULE}.`;\n }\n if (!SERVICE_ID.test(id)) {\n return `Invalid ${label} '${id}': ${SERVICE_ID_RULE}.`;\n }\n // Both refusals below are about Windows, and both get their own sentence for\n // the same reason `..` does: \"does not match the id grammar\" is true and\n // useless, because the id LOOKS fine and the problem is what the filesystem\n // does with it.\n if (WINDOWS_DEVICE.test(id.split(\".\")[0]!.toUpperCase())) {\n return (\n `Invalid ${label} '${id}': '${id.split(\".\")[0]}' is a reserved device name on Windows — ` +\n `services/${id}/ cannot be created there at all, so the docs repo would only work on some of the machines that clone it.`\n );\n }\n if (/[. ]$/.test(id)) {\n return (\n `Invalid ${label} '${id}': a service id may not end with '.' or a space — ` +\n `Windows strips both when creating a directory, so '${id}' and '${id.replace(/[. ]+$/, \"\")}' would silently become one directory there and two everywhere else.`\n );\n }\n return null;\n}\n\n/**\n * Assert that `id` may be used as a `services/<id>/` directory name. Throws\n * InvalidIdError; commands catch it and report `invalid-option`, because a bad\n * id is always something the caller typed, never a state of the repository.\n */\nexport function assertServiceId(id: unknown, label = \"--service\"): asserts id is string {\n const problem = serviceIdProblem(id, label);\n if (problem !== null) throw new InvalidIdError(typeof id === \"string\" ? id : String(id), problem);\n}\n\n/**\n * Feature ids are `<word>-<number>`: the id has to survive being read back off\n * the directory name (`FEAT-101-payment-splitting` -> `FEAT-101`), or the\n * feature would answer to a name it was never given.\n *\n * Here for the same reason the service grammar is — this was spelled twice,\n * privately, in `commands/new.ts` and `core/openspec-inventory.ts`, and\n * docs/DESIGN.md rule 7 recorded the pair as a hazard rather than a fact. The\n * third caller is what made it one: `loam explore --as <FEAT>` interpolates its\n * argument into a `loam new` line that loam PRINTS for an agent to run, so\n * without this check `explore` cheerfully handed back a command `new` refuses.\n * A guard test catches that class only for literal source strings — a command\n * assembled at runtime from argv is invisible to it — which is exactly why the\n * grammar has to be shared rather than re-derived by whoever needs it next.\n */\nconst FEATURE_ID = /^[A-Za-z][A-Za-z0-9]*-\\d+$/;\n\n/** Prose form, shown verbatim in every refusal so the fix is obvious. */\nexport const FEATURE_ID_RULE = \"Expected <word>-<number>, e.g. FEAT-101 or BUG-42.\";\n\nexport function isFeatureId(id: unknown): id is string {\n return typeof id === \"string\" && FEATURE_ID.test(id);\n}\n\n/**\n * Why `id` is not a usable feature id, or null when it is — the `serviceIdProblem`\n * shape, so a caller already collecting findings reports the sentence a refusal\n * would print.\n */\nexport function featureIdProblem(id: unknown, label = \"feature id\"): string | null {\n if (!isFeatureId(id)) {\n const shown = typeof id === \"string\" ? `'${id}'` : String(id);\n return `${shown} is not a usable ${label}. ${FEATURE_ID_RULE}`;\n }\n return null;\n}\n"]}
@@ -0,0 +1,9 @@
1
+ /**
2
+ * A coherence breach: something that makes a feature unsafe to merge into the
3
+ * living docs. Shared by every check the archive gate runs.
4
+ */
5
+ /** Whether `loam archive` refuses on this issue without `--approve`. */
6
+ export function gatesArchive(i) {
7
+ return i.gates ?? i.severity === "error";
8
+ }
9
+ //# sourceMappingURL=issue.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"issue.js","sourceRoot":"","sources":["../../src/core/issue.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAoBH,wEAAwE;AACxE,MAAM,UAAU,YAAY,CAAC,CAAQ;IACnC,OAAO,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,QAAQ,KAAK,OAAO,CAAC;AAC3C,CAAC","sourcesContent":["/**\n * A coherence breach: something that makes a feature unsafe to merge into the\n * living docs. Shared by every check the archive gate runs.\n */\n\nexport interface Issue {\n severity: \"error\" | \"warn\";\n /** Stable machine identifier for the breach — see the labels below. */\n code: IssueCode;\n /** What the breach is about when that is narrower than the feature — usually a service. */\n subject?: string;\n message: string;\n /**\n * Does this issue stop `loam archive`? Severity and gating answer two\n * different questions: severity says whether the DOCUMENT is valid (`loam\n * validate` fails on errors), gating says whether the MERGE is safe. They\n * usually agree — errors gate, warnings do not — so the field is stated only\n * where they diverge: a legal document whose merge would drop authored\n * content is a warning that gates.\n */\n gates?: boolean;\n}\n\n/** Whether `loam archive` refuses on this issue without `--approve`. */\nexport function gatesArchive(i: Issue): boolean {\n return i.gates ?? i.severity === \"error\";\n}\n\nexport type IssueCode =\n /* --- cross-axis: C4 <-> requirements <-> OpenAPI --- */\n /** the architecture axis could not be read at all */\n | \"delta.invalid\"\n /** the delta declares elements or edges but none carry the feature tag — loam cannot see any of it */\n | \"delta.nothing-tagged\"\n /** E1 — a requirement governs an operation its service's OpenAPI does not define */\n | \"spec-api.op-undefined\"\n /** E1, softened — the operation is defined by another feature still in flight; archive that one first */\n | \"spec-api.op-pending\"\n /** E2 — a C4 edge calls an operation the target's OpenAPI does not define */\n | \"c4-api.op-undefined\"\n /** E2, softened — the operation is defined by another feature still in flight; archive that one first */\n | \"c4-api.op-pending\"\n /** E2's lifecycle shadow — a NEW tagged edge consumes an operation the living provider contract marks `deprecated: true`; advisory, never a gate */\n | \"c4-api.op-deprecated\"\n /** a NEW tagged edge consumes an operation this same feature removes */\n | \"c4-api.op-removing\"\n /** an operation-removal marker addresses no living path+method */\n | \"openapi.remove-target-missing\"\n /** an operation-removal marker's id differs from the living operation at that path+method */\n | \"openapi.remove-target-mismatch\"\n /** a REMOVED requirement governs an operation but the feature has no matching removal marker */\n | \"openapi.remove-marker-missing\"\n /** an operation-removal marker is not justified by a REMOVED requirement */\n | \"openapi.remove-marker-unjustified\"\n /** W1 — an operation is called but no requirement governs it */\n | \"c4.op-ungoverned\"\n /** W2 — the feature adds an operation no architecture edge consumes */\n | \"api.op-unconsumed\"\n /** W3 — a new service arrives with no requirement delta */\n | \"service.no-requirement-delta\"\n /** W4 — a \"Calls\" edge carries no operation link */\n | \"c4.op-link-missing\"\n /* --- delta shape: does the diff apply to the living spec it claims to change? --- */\n /** a heading that nearly matches the delta grammar — its requirements merge as nothing */\n | \"delta.unknown-section\"\n /** a delta file with requirements but no delta section anywhere — the whole file would merge as nothing */\n | \"delta.no-delta-sections\"\n /** a requirement stranded under a prose heading — archive will not merge it */\n | \"delta.requirement-not-merged\"\n /** a Requirement-ID in a delta violates the stable-id grammar */\n | \"delta.requirement-id-invalid\"\n /** one delta requirement declares Requirement-ID more than once */\n | \"delta.requirement-id-repeated\"\n /** one Requirement-ID identifies multiple requirements in a delta */\n | \"delta.requirement-id-duplicate\"\n /** the living spec has malformed/ambiguous IDs and cannot be selected safely */\n | \"delta.living-requirement-id-invalid\"\n /** a delta's stable ID and heading select different living requirements */\n | \"delta.requirement-identity-collision\"\n /** MODIFIED a requirement the living spec does not have */\n | \"delta.modified-unknown\"\n /** REMOVED a requirement the living spec does not have */\n | \"delta.removed-unknown\"\n /** ADDED a requirement the living spec already has — the merge would replace it */\n | \"delta.added-duplicate\"\n /** ADDED a name that differs only in case from a living requirement — the merge matches exactly, so both would coexist */\n | \"delta.added-near-duplicate\"\n /** MODIFIED something another feature in flight introduces — an ordering dependency */\n | \"delta.modified-pending\"\n /** REMOVED something another feature in flight introduces */\n | \"delta.removed-pending\"\n /** two features in flight add the same requirement to the same service */\n | \"delta.added-conflict\"\n /* --- archive plan: breaches only the merge computation itself can see --- */\n /** a LIVING requirement outside `## Requirements` — the merge rewrites only that section, so it would land twice */\n | \"living.requirement-outside-requirements\"\n /** the delta redefines an operation the living OpenAPI already has — the merge overwrites it wholesale */\n | \"openapi.op-modified\"\n /** a component the merged operations carry already exists in the living OpenAPI with different content — the merge overwrites it wholesale */\n | \"openapi.component-modified\"\n /** a $ref reachable from the merged operations resolves in neither the feature's OpenAPI nor the living one — merging would write a dangling reference */\n | \"openapi.ref-unresolved\"\n /** the delta redefines a PATH-level key (parameters, servers, summary) the living OpenAPI already has — the overwrite applies to every operation on that path */\n | \"openapi.path-item-modified\"\n /** an `x-loam-remove: true` marker with no operationId — loam cannot tell which operation it retires, and the marker itself would reach the living contract */\n | \"openapi.remove-marker-anonymous\"\n /** an `x-loam-remove: true` written at PATH level, beside the methods — it addresses no operation, so it retires nothing and is not a contract key either */\n | \"openapi.remove-marker-path-level\"\n /** the living OpenAPI defines one operationId in two (path, method) slots — every join on the id picks one arbitrarily */\n | \"openapi.duplicate-operationid\"\n /** an `x-loam-based-on` that is not a digest, or sits on an operation the living contract does not have */\n | \"openapi.baseline-invalid\"\n /** a feature operation with no `x-loam-based-on` — the merge cannot tell whether the delta EDITS it or merely quotes it */\n | \"openapi.baseline-missing\"\n /** the living operation changed since this delta edited it — merging would discard whoever landed in between */\n | \"openapi.baseline-stale\"\n /** the feature retires an operation the LIVING fleet still consumes — a landscape edge's `metadata { op }`, or another service's living requirement */\n | \"openapi.remove-op-consumed\"\n /** two requirements in the LIVING document share one heading — MODIFIED rewrites the first, REMOVED deletes both, so no delta applies predictably */\n | \"delta.living-duplicate-requirement\"\n /** another feature in flight MODIFIES/REMOVES the same living requirement — whichever archives second replaces the other's text wholesale */\n | \"delta.modified-conflict\"\n /** a `Based-On:` line whose value is not a digest, or declared twice — the pin cannot be compared, so it protects nothing */\n | \"delta.baseline-invalid\"\n /** a MODIFIED/REMOVED requirement with no `Based-On:` — nothing can tell whether the living text moved under it */\n | \"delta.baseline-missing\"\n /** the living requirement changed since this delta was written — merging it would silently discard whoever landed in between */\n | \"delta.baseline-stale\"\n /** the archive creates `services/<id>/` but nothing writes its `model.likec4` — the fleet gate will report the service incomplete */\n | \"service.no-model\"\n /* --- docs-repo contract: --all only, never a merge question --- */\n /** AGENTS.md carries no version stamp, or one older than the running binary — the agent contract may have drifted */\n | \"agents.stale\";\n"]}
@@ -0,0 +1,74 @@
1
+ /**
2
+ * The `--json` envelope — loam's machine contract.
3
+ *
4
+ * Every JSON response carries `ok`. Success spreads the payload alongside it;
5
+ * failure carries a stable `error.code` a caller can branch on without parsing
6
+ * prose. Errors go to stdout too: a consumer reads one stream and always gets
7
+ * valid JSON, whether the command succeeded or not. The exit code still tells
8
+ * the shell what happened.
9
+ *
10
+ * Key casing: camelCase, with one deliberate exception — a key that mirrors a
11
+ * frontmatter field verbatim keeps that field's snake_case spelling
12
+ * (`last_verified`, `sources_digest` in vouch's payload), so the envelope and
13
+ * the document it describes spell the same fact the same way. snake_case only
14
+ * where a key mirrors a frontmatter field verbatim; camelCase everywhere else.
15
+ */
16
+ import { existsSync } from "node:fs";
17
+ import { relative } from "node:path";
18
+ import { configPath } from "./config.js";
19
+ /**
20
+ * Version of the top-level JSON envelope, independent of the CLI/package
21
+ * version. Additive payload fields do not require a bump; incompatible changes
22
+ * to `ok`, `error`, or their semantics do.
23
+ */
24
+ export const JSON_CONTRACT_VERSION = "1.0";
25
+ export function emitJson(payload) {
26
+ console.log(JSON.stringify({ contractVersion: JSON_CONTRACT_VERSION, ok: true, ...payload }, null, 2));
27
+ }
28
+ /** Paths in the contract are repo-relative, with forward slashes: diffable across machines. */
29
+ export function repoPath(docsDir, abs) {
30
+ return relative(docsDir, abs).split(/[\\/]/).join("/");
31
+ }
32
+ /** Emit a failure envelope and set the exit code. Returns false, to `return` from a caller. */
33
+ export function emitJsonError(code, message, details = {}) {
34
+ console.log(JSON.stringify({ contractVersion: JSON_CONTRACT_VERSION, ok: false, error: { code, message }, ...details }, null, 2));
35
+ process.exitCode = 1;
36
+ return false;
37
+ }
38
+ /**
39
+ * Report a failure in whichever mode the caller is in. Text mode goes to
40
+ * stderr as it always has; JSON mode goes into the envelope.
41
+ */
42
+ export function fail(json, code, message) {
43
+ if (json) {
44
+ emitJsonError(code, message);
45
+ return;
46
+ }
47
+ console.error(message);
48
+ process.exitCode = 1;
49
+ }
50
+ /** The message every command prints when there is no loam.json. */
51
+ const NO_CONFIG_MESSAGE = "No loam.json found. Run `loam init --docs <dir>` first.";
52
+ /**
53
+ * The message the service-scoped commands print when neither `--service` nor
54
+ * loam.json names one. Four commands ask the same question and owe the same two
55
+ * answers, and a fifth would have been written by hand. `loam delta` keeps its
56
+ * own longer sentence on purpose — it can say which feature was being read.
57
+ */
58
+ export const NO_SERVICE_MESSAGE = "No service. Pass --service <id> or set it in loam.json.";
59
+ /**
60
+ * Report "no config" in whichever mode the caller is in — distinguishing a
61
+ * config that is absent from one that exists but would not load, because the
62
+ * two point at opposite fixes: a missing config wants `loam init`, a corrupt
63
+ * one wants repair, and `init` would silently rewrite the corrupt file.
64
+ * Callers reach here only after `loadConfig()` returned null, so the file
65
+ * existing is proof it failed to load.
66
+ */
67
+ export function reportNoConfig(json) {
68
+ if (existsSync(configPath())) {
69
+ fail(json, "config-invalid", "loam.json exists but could not be loaded. Fix it (or delete it and re-run `loam init`).");
70
+ return;
71
+ }
72
+ fail(json, "no-config", NO_CONFIG_MESSAGE);
73
+ }
74
+ //# sourceMappingURL=json.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"json.js","sourceRoot":"","sources":["../../src/core/json.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AACrC,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAEzC;;;;GAIG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG,KAAK,CAAC;AAsF3C,MAAM,UAAU,QAAQ,CAAC,OAAgC;IACvD,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,eAAe,EAAE,qBAAqB,EAAE,EAAE,EAAE,IAAI,EAAE,GAAG,OAAO,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;AACzG,CAAC;AAED,+FAA+F;AAC/F,MAAM,UAAU,QAAQ,CAAC,OAAe,EAAE,GAAW;IACnD,OAAO,QAAQ,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AACzD,CAAC;AAED,+FAA+F;AAC/F,MAAM,UAAU,aAAa,CAC3B,IAAe,EACf,OAAe,EACf,OAAO,GAA4B,EAAE;IAErC,OAAO,CAAC,GAAG,CACT,IAAI,CAAC,SAAS,CACZ,EAAE,eAAe,EAAE,qBAAqB,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,EAAE,GAAG,OAAO,EAAE,EAC3F,IAAI,EACJ,CAAC,CACF,CACF,CAAC;IACF,OAAO,CAAC,QAAQ,GAAG,CAAC,CAAC;IACrB,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,IAAI,CAAC,IAAa,EAAE,IAAe,EAAE,OAAe;IAClE,IAAI,IAAI,EAAE,CAAC;QACT,aAAa,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;QAC7B,OAAO;IACT,CAAC;IACD,OAAO,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;IACvB,OAAO,CAAC,QAAQ,GAAG,CAAC,CAAC;AACvB,CAAC;AAED,mEAAmE;AACnE,MAAM,iBAAiB,GAAG,yDAAyD,CAAC;AAEpF;;;;;GAKG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,yDAAyD,CAAC;AAE5F;;;;;;;GAOG;AACH,MAAM,UAAU,cAAc,CAAC,IAAa;IAC1C,IAAI,UAAU,CAAC,UAAU,EAAE,CAAC,EAAE,CAAC;QAC7B,IAAI,CACF,IAAI,EACJ,gBAAgB,EAChB,yFAAyF,CAC1F,CAAC;QACF,OAAO;IACT,CAAC;IACD,IAAI,CAAC,IAAI,EAAE,WAAW,EAAE,iBAAiB,CAAC,CAAC;AAC7C,CAAC","sourcesContent":["/**\n * The `--json` envelope — loam's machine contract.\n *\n * Every JSON response carries `ok`. Success spreads the payload alongside it;\n * failure carries a stable `error.code` a caller can branch on without parsing\n * prose. Errors go to stdout too: a consumer reads one stream and always gets\n * valid JSON, whether the command succeeded or not. The exit code still tells\n * the shell what happened.\n *\n * Key casing: camelCase, with one deliberate exception — a key that mirrors a\n * frontmatter field verbatim keeps that field's snake_case spelling\n * (`last_verified`, `sources_digest` in vouch's payload), so the envelope and\n * the document it describes spell the same fact the same way. snake_case only\n * where a key mirrors a frontmatter field verbatim; camelCase everywhere else.\n */\nimport { existsSync } from \"node:fs\";\nimport { relative } from \"node:path\";\nimport { configPath } from \"./config.js\";\n\n/**\n * Version of the top-level JSON envelope, independent of the CLI/package\n * version. Additive payload fields do not require a bump; incompatible changes\n * to `ok`, `error`, or their semantics do.\n */\nexport const JSON_CONTRACT_VERSION = \"1.0\";\n\n/**\n * Stable failure codes. Prose may change; these may not.\n *\n * The `sources-*` pair mirrors the `sources.*` finding codes on purpose: the\n * same breach is recognisable whether it arrives as a refusal from `vouch` or\n * as a finding from `validate`.\n *\n * The `feature-active` / `snapshot-*` group is `unarchive` refusing to guess:\n * each names a different reason the undo is not one, and a caller has to tell\n * them apart to know whether re-running could ever work. Its commit failures\n * split the same way archive's do: `restore-failed` means nothing was restored\n * or everything was rolled back — the living docs are unchanged and re-running\n * can work; `rollback-incomplete` (shared with archive, same meaning) means\n * some files could not be put back, and the message lists them.\n *\n * The `answers-*` group is `loam verify --record` refusing an answer set that\n * does not answer the current checklist. They are separated for the same reason:\n * an unreadable file is a bug in whatever wrote it, a mismatch means the feature\n * moved and the claims have to be answered again, and an unevidenced\n * confirmation is the one an agent can fix on the spot.\n *\n * The archive group is `loam archive` refusing or failing, and each code is a\n * different answer to \"what do I fix, and can I trust the repo?\": `not-coherent`\n * is the gate — errors in the FEATURE, fix them or override with `--approve`;\n * `living-outside-requirements` is the one refusal `--approve` does not move —\n * the LIVING spec holds requirements outside `## Requirements`, and the merge\n * would duplicate them, so the fix is in the living docs; `archive-exists` is a\n * destination collision under `features/archive/`; `merge-failed` is a merge\n * that could not be computed or was rolled back — either way the living docs\n * are unchanged; `rollback-incomplete` is the one that demands a human: the\n * merge failed AND some files could not be restored.\n *\n * `config-invalid` is distinct from `no-config` because the fixes point in\n * opposite directions: a missing config wants `loam init`, a corrupt one wants\n * repair — and an agent that ran `init` on a corrupt file would silently\n * rewrite it.\n *\n * `internal` is the one code with no stable meaning: an unexpected throw. It\n * exists so a `--json` consumer still receives an envelope instead of a stack\n * trace on stdout's sibling stream.\n */\nexport type ErrorCode =\n | \"no-config\"\n | \"config-invalid\"\n | \"unknown-target\"\n | \"invalid-option\"\n | \"already-exists\"\n | \"sources-absent\"\n | \"sources-path-missing\"\n | \"not-coherent\"\n | \"living-outside-requirements\"\n | \"archive-exists\"\n | \"merge-failed\"\n | \"rollback-incomplete\"\n | \"feature-active\"\n | \"snapshot-missing\"\n | \"snapshot-stale\"\n /** A snapshot pre-image whose bytes no longer match the digest archive recorded for them: `unarchive` will not restore text nobody wrote. */\n | \"snapshot-corrupt\"\n /** A `loam archive`/`unarchive` was killed mid-commit: the living docs are half-written, and this run refuses rather than writing over the evidence. */\n | \"commit-interrupted\"\n | \"restore-failed\"\n | \"answers-unreadable\"\n | \"answers-mismatch\"\n | \"answers-unevidenced\"\n | \"service-mismatch\"\n | \"unknown-service\"\n | \"repository-unavailable\"\n /** `loam verify --record` without `--service` over a federated (schema 2) record: it would erase other repositories' attestations. */\n | \"record-federated\"\n /** A `verification.yaml` that exists but cannot be read as a record — never overwritten, never reported as absent. */\n | \"record-unreadable\"\n /** `loam gherkin <FEAT>` refusing to overwrite a `.feature` file owned by another feature still in flight. */\n | \"gherkin-conflict\"\n /** `loam vouch` found the spec changed under it between reading and stamping — another vouch or an edit landed first, and nothing was written. */\n | \"vouch-raced\"\n /** `docsDir` in loam.json points at nothing: the docs repo was never cloned, or the path is wrong. A read command refuses rather than reporting an empty fleet. */\n | \"docs-missing\"\n /** `docsDir` is a directory but has no `services/`: it is some other directory, most often the service repo itself after a typo. */\n | \"services-missing\"\n /** Another `loam archive`/`unarchive` holds the docs repo's advisory lock: nothing was read or written, and re-running once it finishes works. */\n | \"docs-busy\"\n | \"internal\";\n\nexport function emitJson(payload: Record<string, unknown>): void {\n console.log(JSON.stringify({ contractVersion: JSON_CONTRACT_VERSION, ok: true, ...payload }, null, 2));\n}\n\n/** Paths in the contract are repo-relative, with forward slashes: diffable across machines. */\nexport function repoPath(docsDir: string, abs: string): string {\n return relative(docsDir, abs).split(/[\\\\/]/).join(\"/\");\n}\n\n/** Emit a failure envelope and set the exit code. Returns false, to `return` from a caller. */\nexport function emitJsonError(\n code: ErrorCode,\n message: string,\n details: Record<string, unknown> = {},\n): false {\n console.log(\n JSON.stringify(\n { contractVersion: JSON_CONTRACT_VERSION, ok: false, error: { code, message }, ...details },\n null,\n 2,\n ),\n );\n process.exitCode = 1;\n return false;\n}\n\n/**\n * Report a failure in whichever mode the caller is in. Text mode goes to\n * stderr as it always has; JSON mode goes into the envelope.\n */\nexport function fail(json: boolean, code: ErrorCode, message: string): void {\n if (json) {\n emitJsonError(code, message);\n return;\n }\n console.error(message);\n process.exitCode = 1;\n}\n\n/** The message every command prints when there is no loam.json. */\nconst NO_CONFIG_MESSAGE = \"No loam.json found. Run `loam init --docs <dir>` first.\";\n\n/**\n * The message the service-scoped commands print when neither `--service` nor\n * loam.json names one. Four commands ask the same question and owe the same two\n * answers, and a fifth would have been written by hand. `loam delta` keeps its\n * own longer sentence on purpose — it can say which feature was being read.\n */\nexport const NO_SERVICE_MESSAGE = \"No service. Pass --service <id> or set it in loam.json.\";\n\n/**\n * Report \"no config\" in whichever mode the caller is in — distinguishing a\n * config that is absent from one that exists but would not load, because the\n * two point at opposite fixes: a missing config wants `loam init`, a corrupt\n * one wants repair, and `init` would silently rewrite the corrupt file.\n * Callers reach here only after `loadConfig()` returned null, so the file\n * existing is proof it failed to load.\n */\nexport function reportNoConfig(json: boolean): void {\n if (existsSync(configPath())) {\n fail(\n json,\n \"config-invalid\",\n \"loam.json exists but could not be loaded. Fix it (or delete it and re-run `loam init`).\",\n );\n return;\n }\n fail(json, \"no-config\", NO_CONFIG_MESSAGE);\n}\n"]}