@ybotok/loam 0.1.0-beta.1 → 0.1.0-beta.3

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 (120) hide show
  1. package/CHANGELOG.md +96 -0
  2. package/README.md +6 -1
  3. package/SCHEMA.md +22 -4
  4. package/dist/cli.js +4 -0
  5. package/dist/cli.js.map +1 -1
  6. package/dist/commands/adopt.js +17 -3
  7. package/dist/commands/adopt.js.map +1 -1
  8. package/dist/commands/archive.js +24 -8
  9. package/dist/commands/archive.js.map +1 -1
  10. package/dist/commands/delta.js +74 -47
  11. package/dist/commands/delta.js.map +1 -1
  12. package/dist/commands/dependencies.js +7 -2
  13. package/dist/commands/dependencies.js.map +1 -1
  14. package/dist/commands/docs-repo-gate.js +104 -0
  15. package/dist/commands/docs-repo-gate.js.map +1 -0
  16. package/dist/commands/explore.js +195 -0
  17. package/dist/commands/explore.js.map +1 -0
  18. package/dist/commands/format.js +17 -0
  19. package/dist/commands/format.js.map +1 -0
  20. package/dist/commands/gherkin.js +87 -48
  21. package/dist/commands/gherkin.js.map +1 -1
  22. package/dist/commands/init.js +3 -3
  23. package/dist/commands/init.js.map +1 -1
  24. package/dist/commands/instructions.js +80 -0
  25. package/dist/commands/instructions.js.map +1 -0
  26. package/dist/commands/list.js +10 -76
  27. package/dist/commands/list.js.map +1 -1
  28. package/dist/commands/migrate-openspec.js +45 -28
  29. package/dist/commands/migrate-openspec.js.map +1 -1
  30. package/dist/commands/new.js +35 -34
  31. package/dist/commands/new.js.map +1 -1
  32. package/dist/commands/rebase.js +2 -4
  33. package/dist/commands/rebase.js.map +1 -1
  34. package/dist/commands/show.js +26 -31
  35. package/dist/commands/show.js.map +1 -1
  36. package/dist/commands/status.js +17 -25
  37. package/dist/commands/status.js.map +1 -1
  38. package/dist/commands/unarchive.js +12 -5
  39. package/dist/commands/unarchive.js.map +1 -1
  40. package/dist/commands/validate.js +306 -142
  41. package/dist/commands/validate.js.map +1 -1
  42. package/dist/commands/verify.js +33 -17
  43. package/dist/commands/verify.js.map +1 -1
  44. package/dist/commands/vouch.js +156 -60
  45. package/dist/commands/vouch.js.map +1 -1
  46. package/dist/core/agent.js +392 -42
  47. package/dist/core/agent.js.map +1 -1
  48. package/dist/core/agents-stamp.js +47 -10
  49. package/dist/core/agents-stamp.js.map +1 -1
  50. package/dist/core/arch.js +28 -2
  51. package/dist/core/arch.js.map +1 -1
  52. package/dist/core/asyncapi.js +253 -0
  53. package/dist/core/asyncapi.js.map +1 -0
  54. package/dist/core/brief.js +28 -6
  55. package/dist/core/brief.js.map +1 -1
  56. package/dist/core/coherence.js +37 -13
  57. package/dist/core/coherence.js.map +1 -1
  58. package/dist/core/concurrency.js +51 -0
  59. package/dist/core/concurrency.js.map +1 -0
  60. package/dist/core/config.js +10 -2
  61. package/dist/core/config.js.map +1 -1
  62. package/dist/core/delta.js +21 -2
  63. package/dist/core/delta.js.map +1 -1
  64. package/dist/core/dependencies.js +46 -42
  65. package/dist/core/dependencies.js.map +1 -1
  66. package/dist/core/docs.js +65 -3
  67. package/dist/core/docs.js.map +1 -1
  68. package/dist/core/doctor.js +79 -27
  69. package/dist/core/doctor.js.map +1 -1
  70. package/dist/core/document-bytes.js +59 -0
  71. package/dist/core/document-bytes.js.map +1 -0
  72. package/dist/core/explore.js +225 -0
  73. package/dist/core/explore.js.map +1 -0
  74. package/dist/core/fleet-context.js +29 -63
  75. package/dist/core/fleet-context.js.map +1 -1
  76. package/dist/core/frontmatter.js +1 -5
  77. package/dist/core/frontmatter.js.map +1 -1
  78. package/dist/core/gherkin.js +3 -61
  79. package/dist/core/gherkin.js.map +1 -1
  80. package/dist/core/health.js +1 -5
  81. package/dist/core/health.js.map +1 -1
  82. package/dist/core/ids.js +34 -5
  83. package/dist/core/ids.js.map +1 -1
  84. package/dist/core/json.js +8 -1
  85. package/dist/core/json.js.map +1 -1
  86. package/dist/core/likec4.js +19 -4
  87. package/dist/core/likec4.js.map +1 -1
  88. package/dist/core/maturity.js +66 -0
  89. package/dist/core/maturity.js.map +1 -0
  90. package/dist/core/openapi-merge.js +39 -6
  91. package/dist/core/openapi-merge.js.map +1 -1
  92. package/dist/core/openapi.js +17 -5
  93. package/dist/core/openapi.js.map +1 -1
  94. package/dist/core/openspec-inventory.js +64 -27
  95. package/dist/core/openspec-inventory.js.map +1 -1
  96. package/dist/core/path-safety.js +59 -0
  97. package/dist/core/path-safety.js.map +1 -1
  98. package/dist/core/provenance.js +102 -10
  99. package/dist/core/provenance.js.map +1 -1
  100. package/dist/core/records.js +33 -0
  101. package/dist/core/records.js.map +1 -0
  102. package/dist/core/repo.js +77 -1
  103. package/dist/core/repo.js.map +1 -1
  104. package/dist/core/report.js +9 -0
  105. package/dist/core/report.js.map +1 -1
  106. package/dist/core/results.js +1 -3
  107. package/dist/core/results.js.map +1 -1
  108. package/dist/core/spec.js +28 -4
  109. package/dist/core/spec.js.map +1 -1
  110. package/dist/core/staging.js +124 -17
  111. package/dist/core/staging.js.map +1 -1
  112. package/dist/core/status.js +152 -35
  113. package/dist/core/status.js.map +1 -1
  114. package/dist/core/steps.js +72 -0
  115. package/dist/core/steps.js.map +1 -0
  116. package/dist/core/verify.js +18 -4
  117. package/dist/core/verify.js.map +1 -1
  118. package/dist/core/version.js +17 -1
  119. package/dist/core/version.js.map +1 -1
  120. package/package.json +1 -1
@@ -1,15 +1,18 @@
1
1
  import { existsSync } from "node:fs";
2
+ import { createInterface } from "node:readline/promises";
2
3
  import { loadConfig } from "../core/config.js";
3
- import { emitJson, fail, repoPath, reportNoConfig } from "../core/json.js";
4
+ import { emitJson, fail, NO_SERVICE_MESSAGE, repoPath, reportNoConfig } from "../core/json.js";
4
5
  import { listField, parseFrontmatter, withFrontmatterFields } from "../core/frontmatter.js";
5
- import { contentDigest, emptySourcesMessage, encodeSourceIndex, missingSources, patternSources, sourcesDigest, unsafeSources, } from "../core/provenance.js";
6
+ import { contentDigest, emptySourcesMessage, encodeSourceIndex, gitIdentity, missingSources, patternSources, sourcesDigest, today, unsafeSources, } from "../core/provenance.js";
6
7
  import { SPEC_AXES, servicePaths } from "../core/repo.js";
8
+ import { plural } from "./format.js";
7
9
  import { message, NotUtf8Error, readUtf8, rollbackStaged, stageWrites, swapStaged, } from "../core/staging.js";
8
10
  export function registerVouch(program) {
9
11
  program
10
12
  .command("vouch")
11
13
  .description("Vouch for a service's living specs: stamp spec.md, and arch.spec.md when present, verified against the code they describe")
12
14
  .option("--service <id>", "service to vouch for (defaults to the configured service)")
15
+ .option("--yes", "skip the confirmation — required when stdin is not a terminal, and still records `vouched_by`")
13
16
  .option("--json", "emit the machine contract instead of the human view")
14
17
  .action(async (opts) => {
15
18
  const json = opts.json === true;
@@ -20,7 +23,7 @@ export function registerVouch(program) {
20
23
  }
21
24
  const service = opts.service ?? config.service;
22
25
  if (service === undefined) {
23
- return fail(json, "invalid-option", "No service. Pass --service <id> or set it in loam.json.");
26
+ return fail(json, "invalid-option", NO_SERVICE_MESSAGE);
24
27
  }
25
28
  // Vouching hashes the code the doc was written from, so it can only run
26
29
  // where that code is. From anywhere else the paths are someone else's.
@@ -28,9 +31,53 @@ export function registerVouch(program) {
28
31
  const here = config.service === undefined ? "this is not a service repo" : `this repo is '${config.service}'`;
29
32
  return fail(json, "invalid-option", `Cannot vouch for '${service}' from here: ${here}. \`sources\` resolve against the service's own repo — run it there.`);
30
33
  }
34
+ // The repo root, not the cwd — see the comment on `repoDir` below; the
35
+ // identity has to be asked of the same directory the sources resolve in,
36
+ // or a subdirectory with its own git config would answer for it.
37
+ const repoDir = config.root ?? process.cwd();
38
+ // Who. Before any reading, because a run that cannot name a person has
39
+ // nothing to offer at the end of it and should not spend a digest finding
40
+ // that out.
41
+ const vouchedBy = await gitIdentity(repoDir);
42
+ if (vouchedBy === null) {
43
+ return fail(json, "vouch-unattributable", `Cannot vouch for '${service}': git names no \`user.email\` in ${repoDir}, so the stamp would ` +
44
+ "record a claim with nobody behind it — which is the one thing `status: verified` must not mean. " +
45
+ "Set it (`git config user.email you@example.com`) and re-run.");
46
+ }
47
+ // Whether a person is actually here. `vouch` is the only command in loam
48
+ // whose output is a claim about a HUMAN act — every other check is
49
+ // internal consistency, which fluent prose satisfies — and it used to run
50
+ // unattended, unattributed, twice in a row, stamping `verified` both
51
+ // times. Meanwhile the generated skill files pre-approved `Bash(loam:*)`,
52
+ // so the same agent that wrote the draft was permitted to promote it.
53
+ // That inverts loam's own argument about test evidence: an agent must not
54
+ // be able to SAY a scenario is tested, and it must not be able to say a
55
+ // spec matches the code either. The allowlist no longer covers this
56
+ // command (core/agent.ts), and nothing but a terminal or an explicit
57
+ // `--yes` gets past here.
58
+ if (opts.yes !== true) {
59
+ if (process.stdin.isTTY !== true) {
60
+ return fail(json, "vouch-unattended", `Cannot vouch for '${service}' with nothing on the other end of stdin: this command records that ` +
61
+ "a PERSON read the code and says the document matches it, and nobody was asked. Run it from a " +
62
+ "terminal, or pass --yes to state that you are standing behind it anyway — `vouched_by` records " +
63
+ `${vouchedBy} either way.`);
64
+ }
65
+ // `--json` and a prompt do not compose: the envelope is one JSON
66
+ // document on stdout, and a question printed beside it is not parseable
67
+ // by the consumer that asked for it. A JSON caller is a program, so it
68
+ // gets the same answer a pipe does — say so with --yes.
69
+ if (json) {
70
+ return fail(json, "vouch-unattended", `Cannot vouch for '${service}' in --json mode without --yes: the confirmation is a question for a ` +
71
+ "person, and it cannot be asked on a stream whose whole contract is one JSON document.");
72
+ }
73
+ if (!(await confirmVouch(service, vouchedBy, config.docsDir))) {
74
+ return fail(json, "vouch-declined", `Nothing was stamped for '${service}'.`);
75
+ }
76
+ }
31
77
  const outcome = await vouch({
32
78
  docsDir: config.docsDir,
33
79
  service,
80
+ vouchedBy,
34
81
  // The repo root, not the cwd. `sources:` are spelled relative to the
35
82
  // repository — that is what they mean in the frontmatter and what
36
83
  // `loam validate` resolves them against — so vouching from a
@@ -38,20 +85,19 @@ export function registerVouch(program) {
38
85
  // (`sources-path-missing`) and refuse the stamp. `config.root` is the
39
86
  // directory loam.json was found in, which is the only definition of
40
87
  // "this repo" that does not move with the caller.
41
- repoDir: config.root ?? process.cwd(),
88
+ repoDir,
42
89
  today: today(new Date()),
43
90
  });
44
91
  if (!outcome.ok)
45
92
  return fail(json, outcome.code, outcome.message);
46
- // spec.md is required, so it always leads `stamped`; arch.spec.md is the
47
- // only possible second entry.
48
- const [spec, arch] = outcome.stamped;
93
+ const { spec, archSpec: arch } = outcome.stamped;
49
94
  if (json) {
50
95
  emitJson({
51
96
  service,
52
97
  path: repoPath(config.docsDir, spec.path),
53
98
  status: outcome.status,
54
99
  last_verified: outcome.lastVerified,
100
+ vouched_by: outcome.vouchedBy,
55
101
  sources: spec.sources,
56
102
  sources_digest: spec.digest,
57
103
  content_digest: spec.contentDigest,
@@ -61,7 +107,7 @@ export function registerVouch(program) {
61
107
  // arch.spec.md, so a consumer can tell "none present" from an older
62
108
  // loam that never reported the axis. status/last_verified are not
63
109
  // repeated — the vouch is one act, and they hold for every file in it.
64
- archSpec: arch === undefined
110
+ archSpec: arch === null
65
111
  ? null
66
112
  : {
67
113
  path: repoPath(config.docsDir, arch.path),
@@ -74,10 +120,13 @@ export function registerVouch(program) {
74
120
  });
75
121
  return;
76
122
  }
77
- for (const [i, s] of outcome.stamped.entries()) {
123
+ // spec.md first, arch.spec.md behind it when present — the order the
124
+ // person who vouched reads them in, and the order the axes are declared.
125
+ for (const [i, s] of [spec, ...(arch === null ? [] : [arch])].entries()) {
78
126
  console.log(`${i > 0 ? "\n" : ""}${service} vouched — ${repoPath(config.docsDir, s.path)}\n`);
79
127
  console.log(` status ${outcome.status}`);
80
128
  console.log(` last_verified ${outcome.lastVerified}`);
129
+ console.log(` vouched_by ${outcome.vouchedBy}`);
81
130
  console.log(` sources_digest ${s.digest} (${plural(s.files, "file")} from ${plural(s.sources.length, "source")})`);
82
131
  console.log(` content_digest ${s.contentDigest}`);
83
132
  // Said at the moment of stamping, not only later by `loam validate`:
@@ -92,6 +141,30 @@ export function registerVouch(program) {
92
141
  console.log(`\n\`loam validate\` will now say when that code moves out from under the spec — or when the spec moves under its own stamp.`);
93
142
  });
94
143
  }
144
+ /**
145
+ * Ask, on a terminal, and answer only on an explicit yes.
146
+ *
147
+ * The question states what is about to be claimed rather than asking for
148
+ * assent to a verb: "vouch?" invites a reflex, and the whole value of this
149
+ * command is that the reflex is the thing being interrupted. Default is no —
150
+ * a bare Enter, a closed stdin and a Ctrl-C all mean the same thing, because
151
+ * the only answer that may stamp a document is one somebody typed.
152
+ */
153
+ async function confirmVouch(service, vouchedBy, docsDir) {
154
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
155
+ try {
156
+ console.log(`\nVouching for '${service}' records that YOU read the code and say ` +
157
+ `${docsDir}/services/${service}/ describes it.\n` +
158
+ `It will be stamped \`status: verified\`, \`vouched_by: ${vouchedBy}\`.\n` +
159
+ "loam has not checked this and cannot: every other check it runs is internal " +
160
+ "consistency, which well-written prose satisfies on its own.\n");
161
+ const answer = await rl.question("Have you read the code? [y/N] ");
162
+ return /^y(es)?$/i.test(answer.trim());
163
+ }
164
+ finally {
165
+ rl.close();
166
+ }
167
+ }
95
168
  /**
96
169
  * Stamp `status`, `last_verified`, `sources_digest` and `content_digest` into
97
170
  * a service's living specs — spec.md, and arch.spec.md beside it when the
@@ -118,46 +191,25 @@ export async function vouch(req) {
118
191
  }
119
192
  // Verify first, stamp after: spec.md is required (checked above), arch.spec.md
120
193
  // rides when it exists, and one file that cannot be verified refuses the run
121
- // before anything is written.
122
- const verified = [];
123
- for (const axis of SPEC_AXES) {
124
- const path = paths[axis.key];
125
- if (!existsSync(path))
126
- continue;
127
- const outcome = await verifySpec(req, path, axis.file);
194
+ // before anything is written. The two axes are walked by name rather than
195
+ // accumulated in a loop, because everything downstream needs to know WHICH
196
+ // file it is holding — the required one or the optional one — and a loop can
197
+ // only say that positionally.
198
+ const [specAxis, archAxis] = SPEC_AXES;
199
+ const specVerified = await verifySpec(req, paths[specAxis.key], specAxis.file);
200
+ if (!specVerified.ok)
201
+ return specVerified;
202
+ let archVerified = null;
203
+ if (existsSync(paths[archAxis.key])) {
204
+ const outcome = await verifySpec(req, paths[archAxis.key], archAxis.file);
128
205
  if (!outcome.ok)
129
206
  return outcome;
130
- verified.push(outcome);
131
- }
132
- const stamped = [];
133
- const writes = [];
134
- for (const v of verified) {
135
- // Two passes on purpose: `content_digest` hashes the body BELOW the
136
- // frontmatter, and withFrontmatterFields promises that body byte-identical —
137
- // so hashing after the first stamp and writing the hash in a second one
138
- // yields a digest that is true of the file exactly as written. A re-vouch
139
- // takes the same road and refreshes every field, this one included.
140
- const restamped = withFrontmatterFields(v.raw, {
141
- status: "verified",
142
- last_verified: req.today,
143
- sources_digest: v.digest,
144
- // Beside the digest, what it was taken over. `sources_digest` alone can
145
- // only ever say THAT the code moved; the next `loam validate` reads this
146
- // back to say which files did.
147
- sources_files: encodeSourceIndex(v.index),
148
- });
149
- const bodyDigest = contentDigest(restamped);
150
- writes.push({ path: v.path, content: withFrontmatterFields(restamped, { content_digest: bodyDigest }) });
151
- stamped.push({
152
- path: v.path,
153
- file: v.file,
154
- digest: v.digest,
155
- contentDigest: bodyDigest,
156
- sources: v.sources,
157
- files: v.index.length,
158
- skipped: v.skipped,
159
- });
207
+ archVerified = outcome;
160
208
  }
209
+ const verified = [specVerified, ...(archVerified === null ? [] : [archVerified])];
210
+ const specPlan = planStamp(specVerified, req);
211
+ const archPlan = archVerified === null ? null : planStamp(archVerified, req);
212
+ const writes = [specPlan.write, ...(archPlan === null ? [] : [archPlan.write])];
161
213
  // Commit through archive's stage-and-swap machinery (core/staging.ts) rather
162
214
  // than a writeFile per file: every stamp is computed above in memory, so a
163
215
  // plain sequential write could only die BETWEEN the pair's writes — spec.md
@@ -179,7 +231,17 @@ export async function vouch(req) {
179
231
  // `vouch-raced` and stamp nothing). A file that is GONE is still a race —
180
232
  // `null` is not "empty", it is "the document this run described is not
181
233
  // there any more".
182
- const raced = staged.filter((s, i) => s.before === null || !s.before.equals(Buffer.from(verified[i].raw, "utf8")));
234
+ // Joined by path, not by index: pairing the two lists positionally assumed
235
+ // `stageWrites` returns one entry per planned write in order — true today,
236
+ // promised nowhere, and a compare-and-set that silently compares the wrong
237
+ // pair would stamp over somebody else's vouch. A staged write no verified
238
+ // file claims has nothing to be compared against, so it counts as raced;
239
+ // fail-closed is the only safe direction here.
240
+ const verifiedByPath = new Map(verified.map((v) => [v.path, v]));
241
+ const raced = staged.filter((s) => {
242
+ const v = verifiedByPath.get(s.write.path);
243
+ return v === undefined || s.before === null || !s.before.equals(Buffer.from(v.raw, "utf8"));
244
+ });
183
245
  if (raced.length > 0) {
184
246
  // Nothing has swapped yet, so the rollback is only the temp files going
185
247
  // away — the other writer's stamp is left exactly as it landed.
@@ -212,7 +274,53 @@ export async function vouch(req) {
212
274
  message: `${message(err)} — the vouch was rolled back, no spec was stamped`,
213
275
  };
214
276
  }
215
- return { ok: true, status: "verified", lastVerified: req.today, stamped };
277
+ return {
278
+ ok: true,
279
+ status: "verified",
280
+ lastVerified: req.today,
281
+ vouchedBy: req.vouchedBy,
282
+ stamped: { spec: specPlan.stamped, archSpec: archPlan === null ? null : archPlan.stamped },
283
+ };
284
+ }
285
+ /**
286
+ * One verified file's share of the commit: the bytes to write, and the record
287
+ * of what was stamped into them. Kept beside each other because they are the
288
+ * same computation — the report must describe the file that was actually
289
+ * written, not a second guess at it.
290
+ */
291
+ function planStamp(v, req) {
292
+ // Two passes on purpose: `content_digest` hashes the body BELOW the
293
+ // frontmatter, and withFrontmatterFields promises that body byte-identical —
294
+ // so hashing after the first stamp and writing the hash in a second one
295
+ // yields a digest that is true of the file exactly as written. A re-vouch
296
+ // takes the same road and refreshes every field, this one included.
297
+ const restamped = withFrontmatterFields(v.raw, {
298
+ status: "verified",
299
+ last_verified: req.today,
300
+ // WHO, beside when. Without it `status: verified` recorded that the word
301
+ // had been written and nothing about who wrote it, so a vouch and an
302
+ // agent's own draft left the same trace in the document — the one
303
+ // distinction this command exists to make.
304
+ vouched_by: req.vouchedBy,
305
+ sources_digest: v.digest,
306
+ // Beside the digest, what it was taken over. `sources_digest` alone can
307
+ // only ever say THAT the code moved; the next `loam validate` reads this
308
+ // back to say which files did.
309
+ sources_files: encodeSourceIndex(v.index),
310
+ });
311
+ const bodyDigest = contentDigest(restamped);
312
+ return {
313
+ write: { path: v.path, content: withFrontmatterFields(restamped, { content_digest: bodyDigest }) },
314
+ stamped: {
315
+ path: v.path,
316
+ file: v.file,
317
+ digest: v.digest,
318
+ contentDigest: bodyDigest,
319
+ sources: v.sources,
320
+ files: v.index.length,
321
+ skipped: v.skipped,
322
+ },
323
+ };
216
324
  }
217
325
  /**
218
326
  * The per-file half of the refusal discipline: everything vouch cannot verify
@@ -310,16 +418,4 @@ async function verifySpec(req, path, file) {
310
418
  }
311
419
  return { ok: true, path, file, raw, sources, digest, index, skipped };
312
420
  }
313
- /**
314
- * The local calendar day. A vouch is a person saying "today I read this", so it
315
- * is their date, not UTC's — `toISOString` files an evening vouch in the
316
- * Americas under tomorrow.
317
- */
318
- function today(now) {
319
- const pad = (n) => String(n).padStart(2, "0");
320
- return `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}`;
321
- }
322
- function plural(n, noun) {
323
- return `${n} ${noun}${n === 1 ? "" : "s"}`;
324
- }
325
421
  //# sourceMappingURL=vouch.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"vouch.js","sourceRoot":"","sources":["../../src/commands/vouch.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `loam vouch` — the human act SCHEMA.md has always described as \"promote draft\n * -> verified\".\n *\n * Everything else loam checks is internal consistency, and an agent writing\n * fluent prose satisfies all of it. This is the one command that records a\n * person: they read the code, they say the document matches it, and loam stamps\n * a digest of what they read — and a digest of the document they read it\n * against, so neither side can move quietly. From then on `loam validate` can\n * tell the difference between a document nobody has checked, one that still\n * matches the code, one the code has moved out from under, and one whose own\n * prose changed after the stamp.\n *\n * The stamp is only worth what it claims, so vouch refuses everything it cannot\n * actually verify — a frontmatter block that will not parse (whose fields\n * nobody can read, and whose rewrite would lose the author's lines), a spec\n * with no sources, a glob pattern (no longer supported), a source that is\n * gone, a directory holding no files, or a repo that is not this service's —\n * and refuses without writing anything.\n *\n * \"The document\" is every spec-axis file the service has: spec.md always, and\n * arch.spec.md beside it when present — same frontmatter conventions, same\n * stamp, one vouch. The refusal discipline is per file but the vouch is\n * all-or-nothing per service: one unverifiable file refuses the whole run, so\n * a `verified` service never means \"half-stamped\".\n */\nimport type { Command } from \"commander\";\nimport { existsSync } from \"node:fs\";\nimport { loadConfig } from \"../core/config.js\";\nimport { emitJson, fail, repoPath, reportNoConfig, type ErrorCode } from \"../core/json.js\";\nimport { listField, parseFrontmatter, withFrontmatterFields } from \"../core/frontmatter.js\";\nimport {\n contentDigest,\n emptySourcesMessage,\n encodeSourceIndex,\n missingSources,\n patternSources,\n sourcesDigest,\n unsafeSources,\n type SkippedSource,\n type SourceIndexEntry,\n} from \"../core/provenance.js\";\nimport { SPEC_AXES, servicePaths } from \"../core/repo.js\";\nimport {\n message,\n NotUtf8Error,\n readUtf8,\n rollbackStaged,\n stageWrites,\n swapStaged,\n type PlannedWrite,\n} from \"../core/staging.js\";\n\ninterface VouchOptions {\n service?: string;\n json?: boolean;\n}\n\nexport interface VouchRequest {\n docsDir: string;\n service: string;\n /** The service's own repo — what `sources` resolve against. */\n repoDir: string;\n /** The date to stamp. Injected rather than read off the clock, so it can be pinned. */\n today: string;\n}\n\n/** One spec-axis file's share of a successful vouch. */\nexport interface StampedSpec {\n /** Absolute path of the file that was stamped. */\n path: string;\n /** The axis's filename — \"spec.md\" or \"arch.spec.md\". */\n file: string;\n /** Digest of the sources, as stamped into `sources_digest`. */\n digest: string;\n /** Digest of the document's own body, as stamped into `content_digest`. */\n contentDigest: string;\n /** The `sources` entries, as written in the frontmatter. */\n sources: string[];\n /** How many files those entries expanded to. */\n files: number;\n /**\n * Paths under those entries the digest would not hash. A vouch over a spec\n * with a skipped path is still a vouch — the person read what they read — but\n * they are told, because the stamp cannot go stale over bytes it never saw.\n */\n skipped: SkippedSource[];\n}\n\nexport type VouchOutcome =\n | {\n ok: true;\n status: \"verified\";\n lastVerified: string;\n /** Every spec-axis file stamped, in SPEC_AXES order: spec.md first, arch.spec.md behind it when present. */\n stamped: StampedSpec[];\n }\n | {\n ok: false;\n // The last three are the commit phase failing: `vouch-raced` says the\n // document moved under the run and nothing was written at all,\n // `merge-failed` says the rollback held (nothing was stamped, re-running\n // can work), `rollback-incomplete` says it did not and the message lists\n // the files.\n code: Extract<\n ErrorCode,\n | \"unknown-target\"\n | \"sources-absent\"\n | \"sources-path-missing\"\n // The spec is there but is not UTF-8 text: a diagnosable state, not the\n // unexpected throw `internal` is for.\n | \"repository-unavailable\"\n | \"vouch-raced\"\n | \"merge-failed\"\n | \"rollback-incomplete\"\n >;\n message: string;\n };\n\nexport function registerVouch(program: Command): void {\n program\n .command(\"vouch\")\n .description(\n \"Vouch for a service's living specs: stamp spec.md, and arch.spec.md when present, verified against the code they describe\",\n )\n .option(\"--service <id>\", \"service to vouch for (defaults to the configured service)\")\n .option(\"--json\", \"emit the machine contract instead of the human view\")\n .action(async (opts: VouchOptions) => {\n const json = opts.json === true;\n\n const config = await loadConfig();\n if (!config) {\n reportNoConfig(json);\n return;\n }\n\n const service = opts.service ?? config.service;\n if (service === undefined) {\n return fail(json, \"invalid-option\", \"No service. Pass --service <id> or set it in loam.json.\");\n }\n // Vouching hashes the code the doc was written from, so it can only run\n // where that code is. From anywhere else the paths are someone else's.\n if (config.service !== service) {\n const here =\n config.service === undefined ? \"this is not a service repo\" : `this repo is '${config.service}'`;\n return fail(\n json,\n \"invalid-option\",\n `Cannot vouch for '${service}' from here: ${here}. \\`sources\\` resolve against the service's own repo — run it there.`,\n );\n }\n\n const outcome = await vouch({\n docsDir: config.docsDir,\n service,\n // The repo root, not the cwd. `sources:` are spelled relative to the\n // repository — that is what they mean in the frontmatter and what\n // `loam validate` resolves them against — so vouching from a\n // subdirectory used to report every real source as missing\n // (`sources-path-missing`) and refuse the stamp. `config.root` is the\n // directory loam.json was found in, which is the only definition of\n // \"this repo\" that does not move with the caller.\n repoDir: config.root ?? process.cwd(),\n today: today(new Date()),\n });\n if (!outcome.ok) return fail(json, outcome.code, outcome.message);\n\n // spec.md is required, so it always leads `stamped`; arch.spec.md is the\n // only possible second entry.\n const [spec, arch] = outcome.stamped;\n if (json) {\n emitJson({\n service,\n path: repoPath(config.docsDir, spec!.path),\n status: outcome.status,\n last_verified: outcome.lastVerified,\n sources: spec!.sources,\n sources_digest: spec!.digest,\n content_digest: spec!.contentDigest,\n files: spec!.files,\n skipped: spec!.skipped,\n // The architecture axis, same keys: null when the service has no\n // arch.spec.md, so a consumer can tell \"none present\" from an older\n // loam that never reported the axis. status/last_verified are not\n // repeated — the vouch is one act, and they hold for every file in it.\n archSpec:\n arch === undefined\n ? null\n : {\n path: repoPath(config.docsDir, arch.path),\n sources: arch.sources,\n sources_digest: arch.digest,\n content_digest: arch.contentDigest,\n files: arch.files,\n skipped: arch.skipped,\n },\n });\n return;\n }\n for (const [i, s] of outcome.stamped.entries()) {\n console.log(`${i > 0 ? \"\\n\" : \"\"}${service} vouched — ${repoPath(config.docsDir, s.path)}\\n`);\n console.log(` status ${outcome.status}`);\n console.log(` last_verified ${outcome.lastVerified}`);\n console.log(\n ` sources_digest ${s.digest} (${plural(s.files, \"file\")} from ${plural(s.sources.length, \"source\")})`,\n );\n console.log(` content_digest ${s.contentDigest}`);\n // Said at the moment of stamping, not only later by `loam validate`:\n // this is the one screen the person who vouched is actually looking at,\n // and what it lists is the part of the tree their promise does not cover.\n if (s.skipped.length > 0) {\n console.log(`\\n ⚠ ${plural(s.skipped.length, \"path\")} under those sources went unhashed:`);\n for (const skip of s.skipped) console.log(` ${skip.path} — ${skip.reason}`);\n }\n }\n console.log(\n `\\n\\`loam validate\\` will now say when that code moves out from under the spec — or when the spec moves under its own stamp.`,\n );\n });\n}\n\n/**\n * Stamp `status`, `last_verified`, `sources_digest` and `content_digest` into\n * a service's living specs — spec.md, and arch.spec.md beside it when the\n * service has one — leaving every body byte-identical.\n *\n * Nothing is written unless all four can be stamped truthfully for EVERY file —\n * a half-stamp (verified, but with no digest behind it; or one file stamped and\n * its sibling not) is exactly the claim this command exists to stop being\n * possible, so every present file is verified before any is written — and the\n * writing itself is staged and swapped in like archive's merge, so a failure\n * between the pair's two writes rolls the first back instead of leaving it.\n * The two digests are the two halves of one promise: `sources_digest` pins the\n * code that was read, `content_digest` pins the words it was read against, so\n * `loam validate` can see either side move.\n */\nexport async function vouch(req: VouchRequest): Promise<VouchOutcome> {\n const paths = servicePaths(req.docsDir, req.service);\n if (!existsSync(paths.spec)) {\n return {\n ok: false,\n code: \"unknown-target\",\n message: `No living spec at ${paths.spec}. Run \\`loam adopt\\` for '${req.service}' first.`,\n };\n }\n\n // Verify first, stamp after: spec.md is required (checked above), arch.spec.md\n // rides when it exists, and one file that cannot be verified refuses the run\n // before anything is written.\n const verified: VerifiedSpec[] = [];\n for (const axis of SPEC_AXES) {\n const path = paths[axis.key];\n if (!existsSync(path)) continue;\n const outcome = await verifySpec(req, path, axis.file);\n if (!outcome.ok) return outcome;\n verified.push(outcome);\n }\n\n const stamped: StampedSpec[] = [];\n const writes: PlannedWrite[] = [];\n for (const v of verified) {\n // Two passes on purpose: `content_digest` hashes the body BELOW the\n // frontmatter, and withFrontmatterFields promises that body byte-identical —\n // so hashing after the first stamp and writing the hash in a second one\n // yields a digest that is true of the file exactly as written. A re-vouch\n // takes the same road and refreshes every field, this one included.\n const restamped = withFrontmatterFields(v.raw, {\n status: \"verified\",\n last_verified: req.today,\n sources_digest: v.digest,\n // Beside the digest, what it was taken over. `sources_digest` alone can\n // only ever say THAT the code moved; the next `loam validate` reads this\n // back to say which files did.\n sources_files: encodeSourceIndex(v.index),\n });\n const bodyDigest = contentDigest(restamped);\n writes.push({ path: v.path, content: withFrontmatterFields(restamped, { content_digest: bodyDigest }) });\n stamped.push({\n path: v.path,\n file: v.file,\n digest: v.digest,\n contentDigest: bodyDigest,\n sources: v.sources,\n files: v.index.length,\n skipped: v.skipped,\n });\n }\n\n // Commit through archive's stage-and-swap machinery (core/staging.ts) rather\n // than a writeFile per file: every stamp is computed above in memory, so a\n // plain sequential write could only die BETWEEN the pair's writes — spec.md\n // verified, arch.spec.md still carrying the old stamp — the exact half-stamped\n // state the all-or-nothing verification exists to rule out, lost at the last\n // step to a full disk. Staging parks each file's new bytes beside it, swaps by\n // rename(2), and on failure restores what already swapped from its pre-image.\n const staged = await stageWrites(writes);\n\n // One shared docs repo, ten service repos, and nothing stopping two of them\n // from vouching at once. Between reading a spec and swapping the stamp in\n // there is a window in which somebody else's vouch (or an editor, or a merge)\n // can land in the same file — and because the new bytes were computed from\n // the OLD ones, swapping them in would take that stamp back out without a\n // word. `stageWrites` has already read what is on disk right now, so the\n // check is a comparison, not another read: if the file is not what was\n // verified, this run is describing a document that no longer exists.\n // Bytes, not text: `stageWrites` reads the file as a Buffer (a string\n // comparison against it is never equal, which made every vouch report\n // `vouch-raced` and stamp nothing). A file that is GONE is still a race —\n // `null` is not \"empty\", it is \"the document this run described is not\n // there any more\".\n const raced = staged.filter(\n (s, i) => s.before === null || !s.before.equals(Buffer.from(verified[i]!.raw, \"utf8\")),\n );\n if (raced.length > 0) {\n // Nothing has swapped yet, so the rollback is only the temp files going\n // away — the other writer's stamp is left exactly as it landed.\n await rollbackStaged(staged);\n return {\n ok: false,\n code: \"vouch-raced\",\n message:\n `${req.service}: ${raced.map((s) => s.write.path).join(\", \")} changed while this vouch was running — ` +\n `another vouch or an edit landed first. Nothing was stamped: re-read the document and re-run.`,\n };\n }\n\n try {\n await swapStaged(staged);\n } catch (err) {\n const failures = await rollbackStaged(staged);\n // Archive's two answers to \"can I trust the repo?\", reused rather than\n // minting vouch-only codes — a caller branches on the same fact either way:\n // rolled back → nothing changed, re-running can work; incomplete → the\n // files listed need a human. Only the prose is vouch's own.\n return failures.length > 0\n ? {\n ok: false,\n code: \"rollback-incomplete\",\n message: `${message(err)} — ROLLBACK INCOMPLETE, these files may be half-stamped and need checking by hand: ${failures.join(\", \")}`,\n }\n : {\n ok: false,\n code: \"merge-failed\",\n message: `${message(err)} — the vouch was rolled back, no spec was stamped`,\n };\n }\n return { ok: true, status: \"verified\", lastVerified: req.today, stamped };\n}\n\n/** A spec-axis file whose sources all check out, carrying what the stamp needs. */\ninterface VerifiedSpec {\n ok: true;\n path: string;\n file: string;\n /** The file exactly as read — what the stamp is applied to. */\n raw: string;\n sources: string[];\n digest: string;\n /** Per-file shas, in digest order — what `sources_files` is stamped from. */\n index: SourceIndexEntry[];\n skipped: SkippedSource[];\n}\n\n/**\n * The per-file half of the refusal discipline: everything vouch cannot verify\n * about ONE spec file, checked the same way for spec.md and arch.spec.md.\n */\nasync function verifySpec(\n req: VouchRequest,\n path: string,\n file: string,\n): Promise<VerifiedSpec | Extract<VouchOutcome, { ok: false }>> {\n // Through readUtf8, so the round trip below is exact: the stamp is computed\n // from `raw` and the race check compares `Buffer.from(raw)` against the bytes\n // on disk, which only agree if the file decoded without substitutions. A\n // non-UTF-8 spec is refused by name rather than stamped as verified with\n // every undecodable byte replaced. Refused HERE rather than left to the CLI's\n // top-level handler, which would report a diagnosable state as `internal`.\n // What it replaces is a diagnosis that was worse than none: a UTF-16 spec\n // decoded to a document with no frontmatter loam could find, so vouch refused\n // it as `sources-absent` — \"names no sources\" — over a file whose `sources:`\n // line is right there.\n let raw: string;\n try {\n raw = await readUtf8(path);\n } catch (err) {\n if (!(err instanceof NotUtf8Error)) throw err;\n return {\n ok: false,\n code: \"repository-unavailable\",\n message: `${req.service}: ${file} cannot be vouched for — ${err.message}`,\n };\n }\n const fm = parseFrontmatter(raw);\n // Before any sources reasoning: a header that does not parse hides whatever\n // fields the author wrote, so \"names no sources\" would be a false diagnosis —\n // and if the file ever reached the stamp, withFrontmatterFields' rule for\n // unreadable headers is replace-don't-merge, which would silently discard the\n // author's owner/service/sources lines. Refuse before the write path can see\n // the file. The code stays `sources-absent` — for vouch's purposes the\n // sources ARE unreadable-hence-absent, and the message carries the real\n // diagnosis — rather than minting a new ErrorCode for one refusal.\n if (fm.malformed) {\n return {\n ok: false,\n code: \"sources-absent\",\n message:\n `${req.service}: ${file} has a frontmatter block that cannot be read as YAML — ` +\n `its fields (\\`sources\\` included) are unreadable, and stamping would rewrite the header wholesale, ` +\n `losing what the author wrote. Fix the YAML between the \\`---\\` fences ` +\n `(\\`loam validate\\` reports it as \\`frontmatter.malformed\\`), then re-run.`,\n };\n }\n const sources = listField(fm, \"sources\");\n if (sources.length === 0) {\n return {\n ok: false,\n code: \"sources-absent\",\n message: `${req.service}: ${file} names no sources — there is nothing to vouch it against. Add \\`sources:\\` naming the code it was written from.`,\n };\n }\n\n // spec.md's refusals predate the second axis and keep their exact wording;\n // where they did not name the file, only the newer axis adds it.\n const label = file === \"spec.md\" ? req.service : `${req.service}: ${file}`;\n\n // Before the existence check: a pattern is not a path that \"does not exist\",\n // it is an entry loam no longer reads at all, and the refusal has to say so.\n const patterns = patternSources(sources);\n if (patterns.length > 0) {\n return {\n ok: false,\n code: \"sources-path-missing\",\n message: `${label}: ${patterns.length} source(s) are glob patterns — ${patterns.join(\", \")}. Patterns are no longer supported: name files or directories (a directory already covers everything beneath it). A stamp over a pattern would vouch for a file set nobody can be sure of.`,\n };\n }\n\n const unsafe = unsafeSources(req.repoDir, sources);\n if (unsafe.length > 0) {\n return {\n ok: false,\n code: \"sources-path-missing\",\n message: `${label}: ${unsafe.length} source(s) escape the service repo — ${unsafe.join(\", \")}. Sources must be relative paths contained by this repository, including through symlinks; vouch will not hash files outside it.`,\n };\n }\n\n const missing = missingSources(req.repoDir, sources);\n if (missing.length > 0) {\n return {\n ok: false,\n code: \"sources-path-missing\",\n message: `${label}: ${missing.length} source(s) do not exist — ${missing.join(\", \")}. Vouching now would stamp a claim about code that is not there.`,\n };\n }\n\n const { digest, index, skipped } = await sourcesDigest(req.repoDir, sources);\n if (index.length === 0) {\n // A directory that exists but holds no files — or only ones the walk leaves\n // out: dot-entries, `node_modules`, anything the repository itself ignores.\n // The paths \"resolve\", but a digest over nothing never changes, so the stamp\n // would read as current forever. The sentence comes from provenance.ts so\n // that `loam validate` can grade the same state in the same words instead of\n // going green on a document this command refuses.\n return { ok: false, code: \"sources-absent\", message: emptySourcesMessage(label, sources) };\n }\n return { ok: true, path, file, raw, sources, digest, index, skipped };\n}\n\n/**\n * The local calendar day. A vouch is a person saying \"today I read this\", so it\n * is their date, not UTC's — `toISOString` files an evening vouch in the\n * Americas under tomorrow.\n */\nfunction today(now: Date): string {\n const pad = (n: number): string => String(n).padStart(2, \"0\");\n return `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}`;\n}\n\n\nfunction plural(n: number, noun: string): string {\n return `${n} ${noun}${n === 1 ? \"\" : \"s\"}`;\n}\n"]}
1
+ {"version":3,"file":"vouch.js","sourceRoot":"","sources":["../../src/commands/vouch.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `loam vouch` — the human act SCHEMA.md has always described as \"promote draft\n * -> verified\".\n *\n * Everything else loam checks is internal consistency, and an agent writing\n * fluent prose satisfies all of it. This is the one command that records a\n * person: they read the code, they say the document matches it, and loam stamps\n * a digest of what they read — and a digest of the document they read it\n * against, so neither side can move quietly. From then on `loam validate` can\n * tell the difference between a document nobody has checked, one that still\n * matches the code, one the code has moved out from under, and one whose own\n * prose changed after the stamp.\n *\n * \"Records a person\" was, for two releases, a thing this file said and did not\n * do. There was no `vouched_by`, no identity of any kind and no interactive\n * gate, so an agent could run it twice unattended and stamp `verified` both\n * times — while the skill files loam generates pre-approved `Bash(loam:*)`,\n * which meant the agent that wrote the draft was permitted to promote it. Three\n * things close that, and none of them is a signature: the stamp carries git's\n * `user.email` (`vouched_by`), the run refuses without a terminal or an explicit\n * `--yes`, and the generated allowlist names loam's read-only and authoring\n * verbs one by one instead of all of them (core/agent.ts). What that buys is\n * attribution and a deliberate act, not proof — git config is a text file. A\n * reviewer can now ask a named person what they read, which is the question\n * `status: verified` was silently answering with nobody.\n *\n * The stamp is only worth what it claims, so vouch refuses everything it cannot\n * actually verify — a frontmatter block that will not parse (whose fields\n * nobody can read, and whose rewrite would lose the author's lines), a spec\n * with no sources, a glob pattern (no longer supported), a source that is\n * gone, a directory holding no files, or a repo that is not this service's —\n * and refuses without writing anything.\n *\n * \"The document\" is every spec-axis file the service has: spec.md always, and\n * arch.spec.md beside it when present — same frontmatter conventions, same\n * stamp, one vouch. The refusal discipline is per file but the vouch is\n * all-or-nothing per service: one unverifiable file refuses the whole run, so\n * a `verified` service never means \"half-stamped\".\n */\nimport type { Command } from \"commander\";\nimport { existsSync } from \"node:fs\";\nimport { createInterface } from \"node:readline/promises\";\nimport { loadConfig } from \"../core/config.js\";\nimport { emitJson, fail, NO_SERVICE_MESSAGE, repoPath, reportNoConfig, type ErrorCode } from \"../core/json.js\";\nimport { listField, parseFrontmatter, withFrontmatterFields } from \"../core/frontmatter.js\";\nimport {\n contentDigest,\n emptySourcesMessage,\n encodeSourceIndex,\n gitIdentity,\n missingSources,\n patternSources,\n sourcesDigest,\n today,\n unsafeSources,\n type SkippedSource,\n type SourceIndexEntry,\n} from \"../core/provenance.js\";\nimport { SPEC_AXES, servicePaths } from \"../core/repo.js\";\nimport { plural } from \"./format.js\";\nimport {\n message,\n NotUtf8Error,\n readUtf8,\n rollbackStaged,\n stageWrites,\n swapStaged,\n type PlannedWrite,\n} from \"../core/staging.js\";\n\ninterface VouchOptions {\n service?: string;\n yes?: boolean;\n json?: boolean;\n}\n\nexport interface VouchRequest {\n docsDir: string;\n service: string;\n /** The service's own repo — what `sources` resolve against. */\n repoDir: string;\n /** The date to stamp. Injected rather than read off the clock, so it can be pinned. */\n today: string;\n /**\n * Who is vouching, as `vouched_by` is stamped — git's identity for `repoDir`.\n * Injected for the same reason `today` is: this function must be reproducible\n * from its arguments, and the resolution (and its refusal) belongs to the\n * command that can talk to a person about it.\n */\n vouchedBy: string;\n}\n\n/** One spec-axis file's share of a successful vouch. */\nexport interface StampedSpec {\n /** Absolute path of the file that was stamped. */\n path: string;\n /** The axis's filename — \"spec.md\" or \"arch.spec.md\". */\n file: string;\n /** Digest of the sources, as stamped into `sources_digest`. */\n digest: string;\n /** Digest of the document's own body, as stamped into `content_digest`. */\n contentDigest: string;\n /** The `sources` entries, as written in the frontmatter. */\n sources: string[];\n /** How many files those entries expanded to. */\n files: number;\n /**\n * Paths under those entries the digest would not hash. A vouch over a spec\n * with a skipped path is still a vouch — the person read what they read — but\n * they are told, because the stamp cannot go stale over bytes it never saw.\n */\n skipped: SkippedSource[];\n}\n\nexport type VouchOutcome =\n | {\n ok: true;\n status: \"verified\";\n lastVerified: string;\n /** The identity stamped into every file this vouch touched. */\n vouchedBy: string;\n /**\n * Every spec-axis file stamped. Named, not ordered: spec.md is required\n * for a vouch to happen at all, and arch.spec.md is the only other file\n * one can touch, so the type is what says which is which. An array said it\n * in a comment instead, and every reader had to assert non-emptiness to\n * reach the file that comment promised was there.\n */\n stamped: { spec: StampedSpec; archSpec: StampedSpec | null };\n }\n | {\n ok: false;\n // The last three are the commit phase failing: `vouch-raced` says the\n // document moved under the run and nothing was written at all,\n // `merge-failed` says the rollback held (nothing was stamped, re-running\n // can work), `rollback-incomplete` says it did not and the message lists\n // the files.\n code: Extract<\n ErrorCode,\n | \"unknown-target\"\n | \"sources-absent\"\n | \"sources-path-missing\"\n // The spec is there but is not UTF-8 text: a diagnosable state, not the\n // unexpected throw `internal` is for.\n | \"repository-unavailable\"\n | \"vouch-raced\"\n | \"merge-failed\"\n | \"rollback-incomplete\"\n >;\n message: string;\n };\n\nexport function registerVouch(program: Command): void {\n program\n .command(\"vouch\")\n .description(\n \"Vouch for a service's living specs: stamp spec.md, and arch.spec.md when present, verified against the code they describe\",\n )\n .option(\"--service <id>\", \"service to vouch for (defaults to the configured service)\")\n .option(\n \"--yes\",\n \"skip the confirmation — required when stdin is not a terminal, and still records `vouched_by`\",\n )\n .option(\"--json\", \"emit the machine contract instead of the human view\")\n .action(async (opts: VouchOptions) => {\n const json = opts.json === true;\n\n const config = await loadConfig();\n if (!config) {\n reportNoConfig(json);\n return;\n }\n\n const service = opts.service ?? config.service;\n if (service === undefined) {\n return fail(json, \"invalid-option\", NO_SERVICE_MESSAGE);\n }\n // Vouching hashes the code the doc was written from, so it can only run\n // where that code is. From anywhere else the paths are someone else's.\n if (config.service !== service) {\n const here =\n config.service === undefined ? \"this is not a service repo\" : `this repo is '${config.service}'`;\n return fail(\n json,\n \"invalid-option\",\n `Cannot vouch for '${service}' from here: ${here}. \\`sources\\` resolve against the service's own repo — run it there.`,\n );\n }\n\n // The repo root, not the cwd — see the comment on `repoDir` below; the\n // identity has to be asked of the same directory the sources resolve in,\n // or a subdirectory with its own git config would answer for it.\n const repoDir = config.root ?? process.cwd();\n\n // Who. Before any reading, because a run that cannot name a person has\n // nothing to offer at the end of it and should not spend a digest finding\n // that out.\n const vouchedBy = await gitIdentity(repoDir);\n if (vouchedBy === null) {\n return fail(\n json,\n \"vouch-unattributable\",\n `Cannot vouch for '${service}': git names no \\`user.email\\` in ${repoDir}, so the stamp would ` +\n \"record a claim with nobody behind it — which is the one thing `status: verified` must not mean. \" +\n \"Set it (`git config user.email you@example.com`) and re-run.\",\n );\n }\n\n // Whether a person is actually here. `vouch` is the only command in loam\n // whose output is a claim about a HUMAN act — every other check is\n // internal consistency, which fluent prose satisfies — and it used to run\n // unattended, unattributed, twice in a row, stamping `verified` both\n // times. Meanwhile the generated skill files pre-approved `Bash(loam:*)`,\n // so the same agent that wrote the draft was permitted to promote it.\n // That inverts loam's own argument about test evidence: an agent must not\n // be able to SAY a scenario is tested, and it must not be able to say a\n // spec matches the code either. The allowlist no longer covers this\n // command (core/agent.ts), and nothing but a terminal or an explicit\n // `--yes` gets past here.\n if (opts.yes !== true) {\n if (process.stdin.isTTY !== true) {\n return fail(\n json,\n \"vouch-unattended\",\n `Cannot vouch for '${service}' with nothing on the other end of stdin: this command records that ` +\n \"a PERSON read the code and says the document matches it, and nobody was asked. Run it from a \" +\n \"terminal, or pass --yes to state that you are standing behind it anyway — `vouched_by` records \" +\n `${vouchedBy} either way.`,\n );\n }\n // `--json` and a prompt do not compose: the envelope is one JSON\n // document on stdout, and a question printed beside it is not parseable\n // by the consumer that asked for it. A JSON caller is a program, so it\n // gets the same answer a pipe does — say so with --yes.\n if (json) {\n return fail(\n json,\n \"vouch-unattended\",\n `Cannot vouch for '${service}' in --json mode without --yes: the confirmation is a question for a ` +\n \"person, and it cannot be asked on a stream whose whole contract is one JSON document.\",\n );\n }\n if (!(await confirmVouch(service, vouchedBy, config.docsDir))) {\n return fail(json, \"vouch-declined\", `Nothing was stamped for '${service}'.`);\n }\n }\n\n const outcome = await vouch({\n docsDir: config.docsDir,\n service,\n vouchedBy,\n // The repo root, not the cwd. `sources:` are spelled relative to the\n // repository — that is what they mean in the frontmatter and what\n // `loam validate` resolves them against — so vouching from a\n // subdirectory used to report every real source as missing\n // (`sources-path-missing`) and refuse the stamp. `config.root` is the\n // directory loam.json was found in, which is the only definition of\n // \"this repo\" that does not move with the caller.\n repoDir,\n today: today(new Date()),\n });\n if (!outcome.ok) return fail(json, outcome.code, outcome.message);\n\n const { spec, archSpec: arch } = outcome.stamped;\n if (json) {\n emitJson({\n service,\n path: repoPath(config.docsDir, spec.path),\n status: outcome.status,\n last_verified: outcome.lastVerified,\n vouched_by: outcome.vouchedBy,\n sources: spec.sources,\n sources_digest: spec.digest,\n content_digest: spec.contentDigest,\n files: spec.files,\n skipped: spec.skipped,\n // The architecture axis, same keys: null when the service has no\n // arch.spec.md, so a consumer can tell \"none present\" from an older\n // loam that never reported the axis. status/last_verified are not\n // repeated — the vouch is one act, and they hold for every file in it.\n archSpec:\n arch === null\n ? null\n : {\n path: repoPath(config.docsDir, arch.path),\n sources: arch.sources,\n sources_digest: arch.digest,\n content_digest: arch.contentDigest,\n files: arch.files,\n skipped: arch.skipped,\n },\n });\n return;\n }\n // spec.md first, arch.spec.md behind it when present — the order the\n // person who vouched reads them in, and the order the axes are declared.\n for (const [i, s] of [spec, ...(arch === null ? [] : [arch])].entries()) {\n console.log(`${i > 0 ? \"\\n\" : \"\"}${service} vouched — ${repoPath(config.docsDir, s.path)}\\n`);\n console.log(` status ${outcome.status}`);\n console.log(` last_verified ${outcome.lastVerified}`);\n console.log(` vouched_by ${outcome.vouchedBy}`);\n console.log(\n ` sources_digest ${s.digest} (${plural(s.files, \"file\")} from ${plural(s.sources.length, \"source\")})`,\n );\n console.log(` content_digest ${s.contentDigest}`);\n // Said at the moment of stamping, not only later by `loam validate`:\n // this is the one screen the person who vouched is actually looking at,\n // and what it lists is the part of the tree their promise does not cover.\n if (s.skipped.length > 0) {\n console.log(`\\n ⚠ ${plural(s.skipped.length, \"path\")} under those sources went unhashed:`);\n for (const skip of s.skipped) console.log(` ${skip.path} — ${skip.reason}`);\n }\n }\n console.log(\n `\\n\\`loam validate\\` will now say when that code moves out from under the spec — or when the spec moves under its own stamp.`,\n );\n });\n}\n\n/**\n * Ask, on a terminal, and answer only on an explicit yes.\n *\n * The question states what is about to be claimed rather than asking for\n * assent to a verb: \"vouch?\" invites a reflex, and the whole value of this\n * command is that the reflex is the thing being interrupted. Default is no —\n * a bare Enter, a closed stdin and a Ctrl-C all mean the same thing, because\n * the only answer that may stamp a document is one somebody typed.\n */\nasync function confirmVouch(service: string, vouchedBy: string, docsDir: string): Promise<boolean> {\n const rl = createInterface({ input: process.stdin, output: process.stdout });\n try {\n console.log(\n `\\nVouching for '${service}' records that YOU read the code and say ` +\n `${docsDir}/services/${service}/ describes it.\\n` +\n `It will be stamped \\`status: verified\\`, \\`vouched_by: ${vouchedBy}\\`.\\n` +\n \"loam has not checked this and cannot: every other check it runs is internal \" +\n \"consistency, which well-written prose satisfies on its own.\\n\",\n );\n const answer = await rl.question(\"Have you read the code? [y/N] \");\n return /^y(es)?$/i.test(answer.trim());\n } finally {\n rl.close();\n }\n}\n\n/**\n * Stamp `status`, `last_verified`, `sources_digest` and `content_digest` into\n * a service's living specs — spec.md, and arch.spec.md beside it when the\n * service has one — leaving every body byte-identical.\n *\n * Nothing is written unless all four can be stamped truthfully for EVERY file —\n * a half-stamp (verified, but with no digest behind it; or one file stamped and\n * its sibling not) is exactly the claim this command exists to stop being\n * possible, so every present file is verified before any is written — and the\n * writing itself is staged and swapped in like archive's merge, so a failure\n * between the pair's two writes rolls the first back instead of leaving it.\n * The two digests are the two halves of one promise: `sources_digest` pins the\n * code that was read, `content_digest` pins the words it was read against, so\n * `loam validate` can see either side move.\n */\nexport async function vouch(req: VouchRequest): Promise<VouchOutcome> {\n const paths = servicePaths(req.docsDir, req.service);\n if (!existsSync(paths.spec)) {\n return {\n ok: false,\n code: \"unknown-target\",\n message: `No living spec at ${paths.spec}. Run \\`loam adopt\\` for '${req.service}' first.`,\n };\n }\n\n // Verify first, stamp after: spec.md is required (checked above), arch.spec.md\n // rides when it exists, and one file that cannot be verified refuses the run\n // before anything is written. The two axes are walked by name rather than\n // accumulated in a loop, because everything downstream needs to know WHICH\n // file it is holding — the required one or the optional one — and a loop can\n // only say that positionally.\n const [specAxis, archAxis] = SPEC_AXES;\n const specVerified = await verifySpec(req, paths[specAxis.key], specAxis.file);\n if (!specVerified.ok) return specVerified;\n let archVerified: VerifiedSpec | null = null;\n if (existsSync(paths[archAxis.key])) {\n const outcome = await verifySpec(req, paths[archAxis.key], archAxis.file);\n if (!outcome.ok) return outcome;\n archVerified = outcome;\n }\n const verified: VerifiedSpec[] = [specVerified, ...(archVerified === null ? [] : [archVerified])];\n\n const specPlan = planStamp(specVerified, req);\n const archPlan = archVerified === null ? null : planStamp(archVerified, req);\n const writes: PlannedWrite[] = [specPlan.write, ...(archPlan === null ? [] : [archPlan.write])];\n\n // Commit through archive's stage-and-swap machinery (core/staging.ts) rather\n // than a writeFile per file: every stamp is computed above in memory, so a\n // plain sequential write could only die BETWEEN the pair's writes — spec.md\n // verified, arch.spec.md still carrying the old stamp — the exact half-stamped\n // state the all-or-nothing verification exists to rule out, lost at the last\n // step to a full disk. Staging parks each file's new bytes beside it, swaps by\n // rename(2), and on failure restores what already swapped from its pre-image.\n const staged = await stageWrites(writes);\n\n // One shared docs repo, ten service repos, and nothing stopping two of them\n // from vouching at once. Between reading a spec and swapping the stamp in\n // there is a window in which somebody else's vouch (or an editor, or a merge)\n // can land in the same file — and because the new bytes were computed from\n // the OLD ones, swapping them in would take that stamp back out without a\n // word. `stageWrites` has already read what is on disk right now, so the\n // check is a comparison, not another read: if the file is not what was\n // verified, this run is describing a document that no longer exists.\n // Bytes, not text: `stageWrites` reads the file as a Buffer (a string\n // comparison against it is never equal, which made every vouch report\n // `vouch-raced` and stamp nothing). A file that is GONE is still a race —\n // `null` is not \"empty\", it is \"the document this run described is not\n // there any more\".\n // Joined by path, not by index: pairing the two lists positionally assumed\n // `stageWrites` returns one entry per planned write in order — true today,\n // promised nowhere, and a compare-and-set that silently compares the wrong\n // pair would stamp over somebody else's vouch. A staged write no verified\n // file claims has nothing to be compared against, so it counts as raced;\n // fail-closed is the only safe direction here.\n const verifiedByPath = new Map(verified.map((v) => [v.path, v] as const));\n const raced = staged.filter((s) => {\n const v = verifiedByPath.get(s.write.path);\n return v === undefined || s.before === null || !s.before.equals(Buffer.from(v.raw, \"utf8\"));\n });\n if (raced.length > 0) {\n // Nothing has swapped yet, so the rollback is only the temp files going\n // away — the other writer's stamp is left exactly as it landed.\n await rollbackStaged(staged);\n return {\n ok: false,\n code: \"vouch-raced\",\n message:\n `${req.service}: ${raced.map((s) => s.write.path).join(\", \")} changed while this vouch was running — ` +\n `another vouch or an edit landed first. Nothing was stamped: re-read the document and re-run.`,\n };\n }\n\n try {\n await swapStaged(staged);\n } catch (err) {\n const failures = await rollbackStaged(staged);\n // Archive's two answers to \"can I trust the repo?\", reused rather than\n // minting vouch-only codes — a caller branches on the same fact either way:\n // rolled back → nothing changed, re-running can work; incomplete → the\n // files listed need a human. Only the prose is vouch's own.\n return failures.length > 0\n ? {\n ok: false,\n code: \"rollback-incomplete\",\n message: `${message(err)} — ROLLBACK INCOMPLETE, these files may be half-stamped and need checking by hand: ${failures.join(\", \")}`,\n }\n : {\n ok: false,\n code: \"merge-failed\",\n message: `${message(err)} — the vouch was rolled back, no spec was stamped`,\n };\n }\n return {\n ok: true,\n status: \"verified\",\n lastVerified: req.today,\n vouchedBy: req.vouchedBy,\n stamped: { spec: specPlan.stamped, archSpec: archPlan === null ? null : archPlan.stamped },\n };\n}\n\n/**\n * One verified file's share of the commit: the bytes to write, and the record\n * of what was stamped into them. Kept beside each other because they are the\n * same computation — the report must describe the file that was actually\n * written, not a second guess at it.\n */\nfunction planStamp(v: VerifiedSpec, req: VouchRequest): { write: PlannedWrite; stamped: StampedSpec } {\n // Two passes on purpose: `content_digest` hashes the body BELOW the\n // frontmatter, and withFrontmatterFields promises that body byte-identical —\n // so hashing after the first stamp and writing the hash in a second one\n // yields a digest that is true of the file exactly as written. A re-vouch\n // takes the same road and refreshes every field, this one included.\n const restamped = withFrontmatterFields(v.raw, {\n status: \"verified\",\n last_verified: req.today,\n // WHO, beside when. Without it `status: verified` recorded that the word\n // had been written and nothing about who wrote it, so a vouch and an\n // agent's own draft left the same trace in the document — the one\n // distinction this command exists to make.\n vouched_by: req.vouchedBy,\n sources_digest: v.digest,\n // Beside the digest, what it was taken over. `sources_digest` alone can\n // only ever say THAT the code moved; the next `loam validate` reads this\n // back to say which files did.\n sources_files: encodeSourceIndex(v.index),\n });\n const bodyDigest = contentDigest(restamped);\n return {\n write: { path: v.path, content: withFrontmatterFields(restamped, { content_digest: bodyDigest }) },\n stamped: {\n path: v.path,\n file: v.file,\n digest: v.digest,\n contentDigest: bodyDigest,\n sources: v.sources,\n files: v.index.length,\n skipped: v.skipped,\n },\n };\n}\n\n/** A spec-axis file whose sources all check out, carrying what the stamp needs. */\ninterface VerifiedSpec {\n ok: true;\n path: string;\n file: string;\n /** The file exactly as read — what the stamp is applied to. */\n raw: string;\n sources: string[];\n digest: string;\n /** Per-file shas, in digest order — what `sources_files` is stamped from. */\n index: SourceIndexEntry[];\n skipped: SkippedSource[];\n}\n\n/**\n * The per-file half of the refusal discipline: everything vouch cannot verify\n * about ONE spec file, checked the same way for spec.md and arch.spec.md.\n */\nasync function verifySpec(\n req: VouchRequest,\n path: string,\n file: string,\n): Promise<VerifiedSpec | Extract<VouchOutcome, { ok: false }>> {\n // Through readUtf8, so the round trip below is exact: the stamp is computed\n // from `raw` and the race check compares `Buffer.from(raw)` against the bytes\n // on disk, which only agree if the file decoded without substitutions. A\n // non-UTF-8 spec is refused by name rather than stamped as verified with\n // every undecodable byte replaced. Refused HERE rather than left to the CLI's\n // top-level handler, which would report a diagnosable state as `internal`.\n // What it replaces is a diagnosis that was worse than none: a UTF-16 spec\n // decoded to a document with no frontmatter loam could find, so vouch refused\n // it as `sources-absent` — \"names no sources\" — over a file whose `sources:`\n // line is right there.\n let raw: string;\n try {\n raw = await readUtf8(path);\n } catch (err) {\n if (!(err instanceof NotUtf8Error)) throw err;\n return {\n ok: false,\n code: \"repository-unavailable\",\n message: `${req.service}: ${file} cannot be vouched for — ${err.message}`,\n };\n }\n const fm = parseFrontmatter(raw);\n // Before any sources reasoning: a header that does not parse hides whatever\n // fields the author wrote, so \"names no sources\" would be a false diagnosis —\n // and if the file ever reached the stamp, withFrontmatterFields' rule for\n // unreadable headers is replace-don't-merge, which would silently discard the\n // author's owner/service/sources lines. Refuse before the write path can see\n // the file. The code stays `sources-absent` — for vouch's purposes the\n // sources ARE unreadable-hence-absent, and the message carries the real\n // diagnosis — rather than minting a new ErrorCode for one refusal.\n if (fm.malformed) {\n return {\n ok: false,\n code: \"sources-absent\",\n message:\n `${req.service}: ${file} has a frontmatter block that cannot be read as YAML — ` +\n `its fields (\\`sources\\` included) are unreadable, and stamping would rewrite the header wholesale, ` +\n `losing what the author wrote. Fix the YAML between the \\`---\\` fences ` +\n `(\\`loam validate\\` reports it as \\`frontmatter.malformed\\`), then re-run.`,\n };\n }\n const sources = listField(fm, \"sources\");\n if (sources.length === 0) {\n return {\n ok: false,\n code: \"sources-absent\",\n message: `${req.service}: ${file} names no sources — there is nothing to vouch it against. Add \\`sources:\\` naming the code it was written from.`,\n };\n }\n\n // spec.md's refusals predate the second axis and keep their exact wording;\n // where they did not name the file, only the newer axis adds it.\n const label = file === \"spec.md\" ? req.service : `${req.service}: ${file}`;\n\n // Before the existence check: a pattern is not a path that \"does not exist\",\n // it is an entry loam no longer reads at all, and the refusal has to say so.\n const patterns = patternSources(sources);\n if (patterns.length > 0) {\n return {\n ok: false,\n code: \"sources-path-missing\",\n message: `${label}: ${patterns.length} source(s) are glob patterns — ${patterns.join(\", \")}. Patterns are no longer supported: name files or directories (a directory already covers everything beneath it). A stamp over a pattern would vouch for a file set nobody can be sure of.`,\n };\n }\n\n const unsafe = unsafeSources(req.repoDir, sources);\n if (unsafe.length > 0) {\n return {\n ok: false,\n code: \"sources-path-missing\",\n message: `${label}: ${unsafe.length} source(s) escape the service repo — ${unsafe.join(\", \")}. Sources must be relative paths contained by this repository, including through symlinks; vouch will not hash files outside it.`,\n };\n }\n\n const missing = missingSources(req.repoDir, sources);\n if (missing.length > 0) {\n return {\n ok: false,\n code: \"sources-path-missing\",\n message: `${label}: ${missing.length} source(s) do not exist — ${missing.join(\", \")}. Vouching now would stamp a claim about code that is not there.`,\n };\n }\n\n const { digest, index, skipped } = await sourcesDigest(req.repoDir, sources);\n if (index.length === 0) {\n // A directory that exists but holds no files — or only ones the walk leaves\n // out: dot-entries, `node_modules`, anything the repository itself ignores.\n // The paths \"resolve\", but a digest over nothing never changes, so the stamp\n // would read as current forever. The sentence comes from provenance.ts so\n // that `loam validate` can grade the same state in the same words instead of\n // going green on a document this command refuses.\n return { ok: false, code: \"sources-absent\", message: emptySourcesMessage(label, sources) };\n }\n return { ok: true, path, file, raw, sources, digest, index, skipped };\n}\n"]}