@ybotok/loam 0.1.0-beta.1 → 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 (115) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/README.md +6 -1
  3. package/dist/cli.js +4 -0
  4. package/dist/cli.js.map +1 -1
  5. package/dist/commands/adopt.js +17 -3
  6. package/dist/commands/adopt.js.map +1 -1
  7. package/dist/commands/archive.js +24 -8
  8. package/dist/commands/archive.js.map +1 -1
  9. package/dist/commands/delta.js +74 -47
  10. package/dist/commands/delta.js.map +1 -1
  11. package/dist/commands/dependencies.js +7 -2
  12. package/dist/commands/dependencies.js.map +1 -1
  13. package/dist/commands/docs-repo-gate.js +104 -0
  14. package/dist/commands/docs-repo-gate.js.map +1 -0
  15. package/dist/commands/explore.js +195 -0
  16. package/dist/commands/explore.js.map +1 -0
  17. package/dist/commands/format.js +17 -0
  18. package/dist/commands/format.js.map +1 -0
  19. package/dist/commands/gherkin.js +87 -48
  20. package/dist/commands/gherkin.js.map +1 -1
  21. package/dist/commands/init.js +3 -3
  22. package/dist/commands/init.js.map +1 -1
  23. package/dist/commands/instructions.js +70 -0
  24. package/dist/commands/instructions.js.map +1 -0
  25. package/dist/commands/list.js +6 -76
  26. package/dist/commands/list.js.map +1 -1
  27. package/dist/commands/migrate-openspec.js +45 -28
  28. package/dist/commands/migrate-openspec.js.map +1 -1
  29. package/dist/commands/new.js +35 -34
  30. package/dist/commands/new.js.map +1 -1
  31. package/dist/commands/rebase.js +2 -4
  32. package/dist/commands/rebase.js.map +1 -1
  33. package/dist/commands/show.js +26 -31
  34. package/dist/commands/show.js.map +1 -1
  35. package/dist/commands/status.js +8 -24
  36. package/dist/commands/status.js.map +1 -1
  37. package/dist/commands/unarchive.js +12 -5
  38. package/dist/commands/unarchive.js.map +1 -1
  39. package/dist/commands/validate.js +115 -133
  40. package/dist/commands/validate.js.map +1 -1
  41. package/dist/commands/verify.js +33 -17
  42. package/dist/commands/verify.js.map +1 -1
  43. package/dist/commands/vouch.js +77 -59
  44. package/dist/commands/vouch.js.map +1 -1
  45. package/dist/core/agent.js +273 -31
  46. package/dist/core/agent.js.map +1 -1
  47. package/dist/core/arch.js +28 -2
  48. package/dist/core/arch.js.map +1 -1
  49. package/dist/core/brief.js +3 -3
  50. package/dist/core/brief.js.map +1 -1
  51. package/dist/core/coherence.js +37 -13
  52. package/dist/core/coherence.js.map +1 -1
  53. package/dist/core/concurrency.js +51 -0
  54. package/dist/core/concurrency.js.map +1 -0
  55. package/dist/core/config.js +10 -2
  56. package/dist/core/config.js.map +1 -1
  57. package/dist/core/delta.js +21 -2
  58. package/dist/core/delta.js.map +1 -1
  59. package/dist/core/dependencies.js +46 -42
  60. package/dist/core/dependencies.js.map +1 -1
  61. package/dist/core/docs.js +2 -1
  62. package/dist/core/docs.js.map +1 -1
  63. package/dist/core/doctor.js +41 -26
  64. package/dist/core/doctor.js.map +1 -1
  65. package/dist/core/document-bytes.js +59 -0
  66. package/dist/core/document-bytes.js.map +1 -0
  67. package/dist/core/explore.js +225 -0
  68. package/dist/core/explore.js.map +1 -0
  69. package/dist/core/fleet-context.js +16 -63
  70. package/dist/core/fleet-context.js.map +1 -1
  71. package/dist/core/frontmatter.js +1 -5
  72. package/dist/core/frontmatter.js.map +1 -1
  73. package/dist/core/gherkin.js +3 -61
  74. package/dist/core/gherkin.js.map +1 -1
  75. package/dist/core/health.js +1 -5
  76. package/dist/core/health.js.map +1 -1
  77. package/dist/core/ids.js +34 -5
  78. package/dist/core/ids.js.map +1 -1
  79. package/dist/core/json.js +8 -1
  80. package/dist/core/json.js.map +1 -1
  81. package/dist/core/likec4.js +6 -1
  82. package/dist/core/likec4.js.map +1 -1
  83. package/dist/core/maturity.js +66 -0
  84. package/dist/core/maturity.js.map +1 -0
  85. package/dist/core/openapi-merge.js +39 -6
  86. package/dist/core/openapi-merge.js.map +1 -1
  87. package/dist/core/openapi.js +17 -5
  88. package/dist/core/openapi.js.map +1 -1
  89. package/dist/core/openspec-inventory.js +64 -27
  90. package/dist/core/openspec-inventory.js.map +1 -1
  91. package/dist/core/path-safety.js +59 -0
  92. package/dist/core/path-safety.js.map +1 -1
  93. package/dist/core/provenance.js +35 -10
  94. package/dist/core/provenance.js.map +1 -1
  95. package/dist/core/records.js +33 -0
  96. package/dist/core/records.js.map +1 -0
  97. package/dist/core/repo.js +75 -1
  98. package/dist/core/repo.js.map +1 -1
  99. package/dist/core/report.js +9 -0
  100. package/dist/core/report.js.map +1 -1
  101. package/dist/core/results.js +1 -3
  102. package/dist/core/results.js.map +1 -1
  103. package/dist/core/spec.js +3 -3
  104. package/dist/core/spec.js.map +1 -1
  105. package/dist/core/staging.js +124 -17
  106. package/dist/core/staging.js.map +1 -1
  107. package/dist/core/status.js +122 -33
  108. package/dist/core/status.js.map +1 -1
  109. package/dist/core/steps.js +72 -0
  110. package/dist/core/steps.js.map +1 -0
  111. package/dist/core/verify.js +18 -4
  112. package/dist/core/verify.js.map +1 -1
  113. package/dist/core/version.js +17 -1
  114. package/dist/core/version.js.map +1 -1
  115. package/package.json +1 -1
@@ -1,9 +1,10 @@
1
1
  import { existsSync } from "node:fs";
2
2
  import { loadConfig } from "../core/config.js";
3
- import { emitJson, fail, repoPath, reportNoConfig } from "../core/json.js";
3
+ import { emitJson, fail, NO_SERVICE_MESSAGE, repoPath, reportNoConfig } from "../core/json.js";
4
4
  import { listField, parseFrontmatter, withFrontmatterFields } from "../core/frontmatter.js";
5
- import { contentDigest, emptySourcesMessage, encodeSourceIndex, missingSources, patternSources, sourcesDigest, unsafeSources, } from "../core/provenance.js";
5
+ import { contentDigest, emptySourcesMessage, encodeSourceIndex, missingSources, patternSources, sourcesDigest, today, unsafeSources, } from "../core/provenance.js";
6
6
  import { SPEC_AXES, servicePaths } from "../core/repo.js";
7
+ import { plural } from "./format.js";
7
8
  import { message, NotUtf8Error, readUtf8, rollbackStaged, stageWrites, swapStaged, } from "../core/staging.js";
8
9
  export function registerVouch(program) {
9
10
  program
@@ -20,7 +21,7 @@ export function registerVouch(program) {
20
21
  }
21
22
  const service = opts.service ?? config.service;
22
23
  if (service === undefined) {
23
- return fail(json, "invalid-option", "No service. Pass --service <id> or set it in loam.json.");
24
+ return fail(json, "invalid-option", NO_SERVICE_MESSAGE);
24
25
  }
25
26
  // Vouching hashes the code the doc was written from, so it can only run
26
27
  // where that code is. From anywhere else the paths are someone else's.
@@ -43,9 +44,7 @@ export function registerVouch(program) {
43
44
  });
44
45
  if (!outcome.ok)
45
46
  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;
47
+ const { spec, archSpec: arch } = outcome.stamped;
49
48
  if (json) {
50
49
  emitJson({
51
50
  service,
@@ -61,7 +60,7 @@ export function registerVouch(program) {
61
60
  // arch.spec.md, so a consumer can tell "none present" from an older
62
61
  // loam that never reported the axis. status/last_verified are not
63
62
  // repeated — the vouch is one act, and they hold for every file in it.
64
- archSpec: arch === undefined
63
+ archSpec: arch === null
65
64
  ? null
66
65
  : {
67
66
  path: repoPath(config.docsDir, arch.path),
@@ -74,7 +73,9 @@ export function registerVouch(program) {
74
73
  });
75
74
  return;
76
75
  }
77
- for (const [i, s] of outcome.stamped.entries()) {
76
+ // spec.md first, arch.spec.md behind it when present — the order the
77
+ // person who vouched reads them in, and the order the axes are declared.
78
+ for (const [i, s] of [spec, ...(arch === null ? [] : [arch])].entries()) {
78
79
  console.log(`${i > 0 ? "\n" : ""}${service} vouched — ${repoPath(config.docsDir, s.path)}\n`);
79
80
  console.log(` status ${outcome.status}`);
80
81
  console.log(` last_verified ${outcome.lastVerified}`);
@@ -118,46 +119,25 @@ export async function vouch(req) {
118
119
  }
119
120
  // Verify first, stamp after: spec.md is required (checked above), arch.spec.md
120
121
  // 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);
122
+ // before anything is written. The two axes are walked by name rather than
123
+ // accumulated in a loop, because everything downstream needs to know WHICH
124
+ // file it is holding — the required one or the optional one — and a loop can
125
+ // only say that positionally.
126
+ const [specAxis, archAxis] = SPEC_AXES;
127
+ const specVerified = await verifySpec(req, paths[specAxis.key], specAxis.file);
128
+ if (!specVerified.ok)
129
+ return specVerified;
130
+ let archVerified = null;
131
+ if (existsSync(paths[archAxis.key])) {
132
+ const outcome = await verifySpec(req, paths[archAxis.key], archAxis.file);
128
133
  if (!outcome.ok)
129
134
  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
- });
135
+ archVerified = outcome;
160
136
  }
137
+ const verified = [specVerified, ...(archVerified === null ? [] : [archVerified])];
138
+ const specPlan = planStamp(specVerified, req.today);
139
+ const archPlan = archVerified === null ? null : planStamp(archVerified, req.today);
140
+ const writes = [specPlan.write, ...(archPlan === null ? [] : [archPlan.write])];
161
141
  // Commit through archive's stage-and-swap machinery (core/staging.ts) rather
162
142
  // than a writeFile per file: every stamp is computed above in memory, so a
163
143
  // plain sequential write could only die BETWEEN the pair's writes — spec.md
@@ -179,7 +159,17 @@ export async function vouch(req) {
179
159
  // `vouch-raced` and stamp nothing). A file that is GONE is still a race —
180
160
  // `null` is not "empty", it is "the document this run described is not
181
161
  // there any more".
182
- const raced = staged.filter((s, i) => s.before === null || !s.before.equals(Buffer.from(verified[i].raw, "utf8")));
162
+ // Joined by path, not by index: pairing the two lists positionally assumed
163
+ // `stageWrites` returns one entry per planned write in order — true today,
164
+ // promised nowhere, and a compare-and-set that silently compares the wrong
165
+ // pair would stamp over somebody else's vouch. A staged write no verified
166
+ // file claims has nothing to be compared against, so it counts as raced;
167
+ // fail-closed is the only safe direction here.
168
+ const verifiedByPath = new Map(verified.map((v) => [v.path, v]));
169
+ const raced = staged.filter((s) => {
170
+ const v = verifiedByPath.get(s.write.path);
171
+ return v === undefined || s.before === null || !s.before.equals(Buffer.from(v.raw, "utf8"));
172
+ });
183
173
  if (raced.length > 0) {
184
174
  // Nothing has swapped yet, so the rollback is only the temp files going
185
175
  // away — the other writer's stamp is left exactly as it landed.
@@ -212,7 +202,47 @@ export async function vouch(req) {
212
202
  message: `${message(err)} — the vouch was rolled back, no spec was stamped`,
213
203
  };
214
204
  }
215
- return { ok: true, status: "verified", lastVerified: req.today, stamped };
205
+ return {
206
+ ok: true,
207
+ status: "verified",
208
+ lastVerified: req.today,
209
+ stamped: { spec: specPlan.stamped, archSpec: archPlan === null ? null : archPlan.stamped },
210
+ };
211
+ }
212
+ /**
213
+ * One verified file's share of the commit: the bytes to write, and the record
214
+ * of what was stamped into them. Kept beside each other because they are the
215
+ * same computation — the report must describe the file that was actually
216
+ * written, not a second guess at it.
217
+ */
218
+ function planStamp(v, lastVerified) {
219
+ // Two passes on purpose: `content_digest` hashes the body BELOW the
220
+ // frontmatter, and withFrontmatterFields promises that body byte-identical —
221
+ // so hashing after the first stamp and writing the hash in a second one
222
+ // yields a digest that is true of the file exactly as written. A re-vouch
223
+ // takes the same road and refreshes every field, this one included.
224
+ const restamped = withFrontmatterFields(v.raw, {
225
+ status: "verified",
226
+ last_verified: lastVerified,
227
+ sources_digest: v.digest,
228
+ // Beside the digest, what it was taken over. `sources_digest` alone can
229
+ // only ever say THAT the code moved; the next `loam validate` reads this
230
+ // back to say which files did.
231
+ sources_files: encodeSourceIndex(v.index),
232
+ });
233
+ const bodyDigest = contentDigest(restamped);
234
+ return {
235
+ write: { path: v.path, content: withFrontmatterFields(restamped, { content_digest: bodyDigest }) },
236
+ stamped: {
237
+ path: v.path,
238
+ file: v.file,
239
+ digest: v.digest,
240
+ contentDigest: bodyDigest,
241
+ sources: v.sources,
242
+ files: v.index.length,
243
+ skipped: v.skipped,
244
+ },
245
+ };
216
246
  }
217
247
  /**
218
248
  * The per-file half of the refusal discipline: everything vouch cannot verify
@@ -310,16 +340,4 @@ async function verifySpec(req, path, file) {
310
340
  }
311
341
  return { ok: true, path, file, raw, sources, digest, index, skipped };
312
342
  }
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
343
  //# sourceMappingURL=vouch.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"vouch.js","sourceRoot":"","sources":["../../src/commands/vouch.ts"],"names":[],"mappings":"AA2BA,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAC/C,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,QAAQ,EAAE,cAAc,EAAkB,MAAM,iBAAiB,CAAC;AAC3F,OAAO,EAAE,SAAS,EAAE,gBAAgB,EAAE,qBAAqB,EAAE,MAAM,wBAAwB,CAAC;AAC5F,OAAO,EACL,aAAa,EACb,mBAAmB,EACnB,iBAAiB,EACjB,cAAc,EACd,cAAc,EACd,aAAa,EACb,aAAa,GAGd,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAC1D,OAAO,EACL,OAAO,EACP,YAAY,EACZ,QAAQ,EACR,cAAc,EACd,WAAW,EACX,UAAU,GAEX,MAAM,oBAAoB,CAAC;AAoE5B,MAAM,UAAU,aAAa,CAAC,OAAgB;IAC5C,OAAO;SACJ,OAAO,CAAC,OAAO,CAAC;SAChB,WAAW,CACV,2HAA2H,CAC5H;SACA,MAAM,CAAC,gBAAgB,EAAE,2DAA2D,CAAC;SACrF,MAAM,CAAC,QAAQ,EAAE,qDAAqD,CAAC;SACvE,MAAM,CAAC,KAAK,EAAE,IAAkB,EAAE,EAAE;QACnC,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,KAAK,IAAI,CAAC;QAEhC,MAAM,MAAM,GAAG,MAAM,UAAU,EAAE,CAAC;QAClC,IAAI,CAAC,MAAM,EAAE,CAAC;YACZ,cAAc,CAAC,IAAI,CAAC,CAAC;YACrB,OAAO;QACT,CAAC;QAED,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,IAAI,MAAM,CAAC,OAAO,CAAC;QAC/C,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;YAC1B,OAAO,IAAI,CAAC,IAAI,EAAE,gBAAgB,EAAE,yDAAyD,CAAC,CAAC;QACjG,CAAC;QACD,wEAAwE;QACxE,uEAAuE;QACvE,IAAI,MAAM,CAAC,OAAO,KAAK,OAAO,EAAE,CAAC;YAC/B,MAAM,IAAI,GACR,MAAM,CAAC,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,4BAA4B,CAAC,CAAC,CAAC,iBAAiB,MAAM,CAAC,OAAO,GAAG,CAAC;YACnG,OAAO,IAAI,CACT,IAAI,EACJ,gBAAgB,EAChB,qBAAqB,OAAO,gBAAgB,IAAI,sEAAsE,CACvH,CAAC;QACJ,CAAC;QAED,MAAM,OAAO,GAAG,MAAM,KAAK,CAAC;YAC1B,OAAO,EAAE,MAAM,CAAC,OAAO;YACvB,OAAO;YACP,qEAAqE;YACrE,kEAAkE;YAClE,6DAA6D;YAC7D,2DAA2D;YAC3D,sEAAsE;YACtE,oEAAoE;YACpE,kDAAkD;YAClD,OAAO,EAAE,MAAM,CAAC,IAAI,IAAI,OAAO,CAAC,GAAG,EAAE;YACrC,KAAK,EAAE,KAAK,CAAC,IAAI,IAAI,EAAE,CAAC;SACzB,CAAC,CAAC;QACH,IAAI,CAAC,OAAO,CAAC,EAAE;YAAE,OAAO,IAAI,CAAC,IAAI,EAAE,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC,OAAO,CAAC,CAAC;QAElE,yEAAyE;QACzE,8BAA8B;QAC9B,MAAM,CAAC,IAAI,EAAE,IAAI,CAAC,GAAG,OAAO,CAAC,OAAO,CAAC;QACrC,IAAI,IAAI,EAAE,CAAC;YACT,QAAQ,CAAC;gBACP,OAAO;gBACP,IAAI,EAAE,QAAQ,CAAC,MAAM,CAAC,OAAO,EAAE,IAAK,CAAC,IAAI,CAAC;gBAC1C,MAAM,EAAE,OAAO,CAAC,MAAM;gBACtB,aAAa,EAAE,OAAO,CAAC,YAAY;gBACnC,OAAO,EAAE,IAAK,CAAC,OAAO;gBACtB,cAAc,EAAE,IAAK,CAAC,MAAM;gBAC5B,cAAc,EAAE,IAAK,CAAC,aAAa;gBACnC,KAAK,EAAE,IAAK,CAAC,KAAK;gBAClB,OAAO,EAAE,IAAK,CAAC,OAAO;gBACtB,iEAAiE;gBACjE,oEAAoE;gBACpE,kEAAkE;gBAClE,uEAAuE;gBACvE,QAAQ,EACN,IAAI,KAAK,SAAS;oBAChB,CAAC,CAAC,IAAI;oBACN,CAAC,CAAC;wBACE,IAAI,EAAE,QAAQ,CAAC,MAAM,CAAC,OAAO,EAAE,IAAI,CAAC,IAAI,CAAC;wBACzC,OAAO,EAAE,IAAI,CAAC,OAAO;wBACrB,cAAc,EAAE,IAAI,CAAC,MAAM;wBAC3B,cAAc,EAAE,IAAI,CAAC,aAAa;wBAClC,KAAK,EAAE,IAAI,CAAC,KAAK;wBACjB,OAAO,EAAE,IAAI,CAAC,OAAO;qBACtB;aACR,CAAC,CAAC;YACH,OAAO;QACT,CAAC;QACD,KAAK,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,OAAO,CAAC,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC;YAC/C,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,GAAG,OAAO,cAAc,QAAQ,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YAC9F,OAAO,CAAC,GAAG,CAAC,qBAAqB,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;YACnD,OAAO,CAAC,GAAG,CAAC,qBAAqB,OAAO,CAAC,YAAY,EAAE,CAAC,CAAC;YACzD,OAAO,CAAC,GAAG,CACT,qBAAqB,CAAC,CAAC,MAAM,MAAM,MAAM,CAAC,CAAC,CAAC,KAAK,EAAE,MAAM,CAAC,SAAS,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,QAAQ,CAAC,GAAG,CACzG,CAAC;YACF,OAAO,CAAC,GAAG,CAAC,qBAAqB,CAAC,CAAC,aAAa,EAAE,CAAC,CAAC;YACpD,qEAAqE;YACrE,wEAAwE;YACxE,0EAA0E;YAC1E,IAAI,CAAC,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBACzB,OAAO,CAAC,GAAG,CAAC,SAAS,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,MAAM,CAAC,qCAAqC,CAAC,CAAC;gBAC5F,KAAK,MAAM,IAAI,IAAI,CAAC,CAAC,OAAO;oBAAE,OAAO,CAAC,GAAG,CAAC,SAAS,IAAI,CAAC,IAAI,MAAM,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC;YACnF,CAAC;QACH,CAAC;QACD,OAAO,CAAC,GAAG,CACT,6HAA6H,CAC9H,CAAC;IACJ,CAAC,CAAC,CAAC;AACP,CAAC;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,CAAC,KAAK,UAAU,KAAK,CAAC,GAAiB;IAC3C,MAAM,KAAK,GAAG,YAAY,CAAC,GAAG,CAAC,OAAO,EAAE,GAAG,CAAC,OAAO,CAAC,CAAC;IACrD,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;QAC5B,OAAO;YACL,EAAE,EAAE,KAAK;YACT,IAAI,EAAE,gBAAgB;YACtB,OAAO,EAAE,qBAAqB,KAAK,CAAC,IAAI,6BAA6B,GAAG,CAAC,OAAO,UAAU;SAC3F,CAAC;IACJ,CAAC;IAED,+EAA+E;IAC/E,6EAA6E;IAC7E,8BAA8B;IAC9B,MAAM,QAAQ,GAAmB,EAAE,CAAC;IACpC,KAAK,MAAM,IAAI,IAAI,SAAS,EAAE,CAAC;QAC7B,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QAC7B,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC;YAAE,SAAS;QAChC,MAAM,OAAO,GAAG,MAAM,UAAU,CAAC,GAAG,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC;QACvD,IAAI,CAAC,OAAO,CAAC,EAAE;YAAE,OAAO,OAAO,CAAC;QAChC,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IACzB,CAAC;IAED,MAAM,OAAO,GAAkB,EAAE,CAAC;IAClC,MAAM,MAAM,GAAmB,EAAE,CAAC;IAClC,KAAK,MAAM,CAAC,IAAI,QAAQ,EAAE,CAAC;QACzB,oEAAoE;QACpE,6EAA6E;QAC7E,wEAAwE;QACxE,0EAA0E;QAC1E,oEAAoE;QACpE,MAAM,SAAS,GAAG,qBAAqB,CAAC,CAAC,CAAC,GAAG,EAAE;YAC7C,MAAM,EAAE,UAAU;YAClB,aAAa,EAAE,GAAG,CAAC,KAAK;YACxB,cAAc,EAAE,CAAC,CAAC,MAAM;YACxB,wEAAwE;YACxE,yEAAyE;YACzE,+BAA+B;YAC/B,aAAa,EAAE,iBAAiB,CAAC,CAAC,CAAC,KAAK,CAAC;SAC1C,CAAC,CAAC;QACH,MAAM,UAAU,GAAG,aAAa,CAAC,SAAS,CAAC,CAAC;QAC5C,MAAM,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,OAAO,EAAE,qBAAqB,CAAC,SAAS,EAAE,EAAE,cAAc,EAAE,UAAU,EAAE,CAAC,EAAE,CAAC,CAAC;QACzG,OAAO,CAAC,IAAI,CAAC;YACX,IAAI,EAAE,CAAC,CAAC,IAAI;YACZ,IAAI,EAAE,CAAC,CAAC,IAAI;YACZ,MAAM,EAAE,CAAC,CAAC,MAAM;YAChB,aAAa,EAAE,UAAU;YACzB,OAAO,EAAE,CAAC,CAAC,OAAO;YAClB,KAAK,EAAE,CAAC,CAAC,KAAK,CAAC,MAAM;YACrB,OAAO,EAAE,CAAC,CAAC,OAAO;SACnB,CAAC,CAAC;IACL,CAAC;IAED,6EAA6E;IAC7E,2EAA2E;IAC3E,4EAA4E;IAC5E,+EAA+E;IAC/E,6EAA6E;IAC7E,+EAA+E;IAC/E,8EAA8E;IAC9E,MAAM,MAAM,GAAG,MAAM,WAAW,CAAC,MAAM,CAAC,CAAC;IAEzC,4EAA4E;IAC5E,0EAA0E;IAC1E,8EAA8E;IAC9E,2EAA2E;IAC3E,0EAA0E;IAC1E,yEAAyE;IACzE,uEAAuE;IACvE,qEAAqE;IACrE,sEAAsE;IACtE,sEAAsE;IACtE,0EAA0E;IAC1E,uEAAuE;IACvE,mBAAmB;IACnB,MAAM,KAAK,GAAG,MAAM,CAAC,MAAM,CACzB,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,KAAK,IAAI,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAE,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC,CACvF,CAAC;IACF,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACrB,wEAAwE;QACxE,gEAAgE;QAChE,MAAM,cAAc,CAAC,MAAM,CAAC,CAAC;QAC7B,OAAO;YACL,EAAE,EAAE,KAAK;YACT,IAAI,EAAE,aAAa;YACnB,OAAO,EACL,GAAG,GAAG,CAAC,OAAO,KAAK,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,0CAA0C;gBACtG,8FAA8F;SACjG,CAAC;IACJ,CAAC;IAED,IAAI,CAAC;QACH,MAAM,UAAU,CAAC,MAAM,CAAC,CAAC;IAC3B,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,QAAQ,GAAG,MAAM,cAAc,CAAC,MAAM,CAAC,CAAC;QAC9C,uEAAuE;QACvE,4EAA4E;QAC5E,uEAAuE;QACvE,4DAA4D;QAC5D,OAAO,QAAQ,CAAC,MAAM,GAAG,CAAC;YACxB,CAAC,CAAC;gBACE,EAAE,EAAE,KAAK;gBACT,IAAI,EAAE,qBAAqB;gBAC3B,OAAO,EAAE,GAAG,OAAO,CAAC,GAAG,CAAC,sFAAsF,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE;aACpI;YACH,CAAC,CAAC;gBACE,EAAE,EAAE,KAAK;gBACT,IAAI,EAAE,cAAc;gBACpB,OAAO,EAAE,GAAG,OAAO,CAAC,GAAG,CAAC,mDAAmD;aAC5E,CAAC;IACR,CAAC;IACD,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,YAAY,EAAE,GAAG,CAAC,KAAK,EAAE,OAAO,EAAE,CAAC;AAC5E,CAAC;AAgBD;;;GAGG;AACH,KAAK,UAAU,UAAU,CACvB,GAAiB,EACjB,IAAY,EACZ,IAAY;IAEZ,4EAA4E;IAC5E,8EAA8E;IAC9E,yEAAyE;IACzE,yEAAyE;IACzE,8EAA8E;IAC9E,2EAA2E;IAC3E,0EAA0E;IAC1E,8EAA8E;IAC9E,6EAA6E;IAC7E,uBAAuB;IACvB,IAAI,GAAW,CAAC;IAChB,IAAI,CAAC;QACH,GAAG,GAAG,MAAM,QAAQ,CAAC,IAAI,CAAC,CAAC;IAC7B,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,IAAI,CAAC,CAAC,GAAG,YAAY,YAAY,CAAC;YAAE,MAAM,GAAG,CAAC;QAC9C,OAAO;YACL,EAAE,EAAE,KAAK;YACT,IAAI,EAAE,wBAAwB;YAC9B,OAAO,EAAE,GAAG,GAAG,CAAC,OAAO,KAAK,IAAI,4BAA4B,GAAG,CAAC,OAAO,EAAE;SAC1E,CAAC;IACJ,CAAC;IACD,MAAM,EAAE,GAAG,gBAAgB,CAAC,GAAG,CAAC,CAAC;IACjC,4EAA4E;IAC5E,8EAA8E;IAC9E,0EAA0E;IAC1E,8EAA8E;IAC9E,6EAA6E;IAC7E,uEAAuE;IACvE,wEAAwE;IACxE,mEAAmE;IACnE,IAAI,EAAE,CAAC,SAAS,EAAE,CAAC;QACjB,OAAO;YACL,EAAE,EAAE,KAAK;YACT,IAAI,EAAE,gBAAgB;YACtB,OAAO,EACL,GAAG,GAAG,CAAC,OAAO,KAAK,IAAI,yDAAyD;gBAChF,qGAAqG;gBACrG,wEAAwE;gBACxE,2EAA2E;SAC9E,CAAC;IACJ,CAAC;IACD,MAAM,OAAO,GAAG,SAAS,CAAC,EAAE,EAAE,SAAS,CAAC,CAAC;IACzC,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACzB,OAAO;YACL,EAAE,EAAE,KAAK;YACT,IAAI,EAAE,gBAAgB;YACtB,OAAO,EAAE,GAAG,GAAG,CAAC,OAAO,KAAK,IAAI,iHAAiH;SAClJ,CAAC;IACJ,CAAC;IAED,2EAA2E;IAC3E,iEAAiE;IACjE,MAAM,KAAK,GAAG,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,GAAG,CAAC,OAAO,KAAK,IAAI,EAAE,CAAC;IAE3E,6EAA6E;IAC7E,6EAA6E;IAC7E,MAAM,QAAQ,GAAG,cAAc,CAAC,OAAO,CAAC,CAAC;IACzC,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACxB,OAAO;YACL,EAAE,EAAE,KAAK;YACT,IAAI,EAAE,sBAAsB;YAC5B,OAAO,EAAE,GAAG,KAAK,KAAK,QAAQ,CAAC,MAAM,kCAAkC,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,4LAA4L;SACvR,CAAC;IACJ,CAAC;IAED,MAAM,MAAM,GAAG,aAAa,CAAC,GAAG,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;IACnD,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACtB,OAAO;YACL,EAAE,EAAE,KAAK;YACT,IAAI,EAAE,sBAAsB;YAC5B,OAAO,EAAE,GAAG,KAAK,KAAK,MAAM,CAAC,MAAM,wCAAwC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,kIAAkI;SAC/N,CAAC;IACJ,CAAC;IAED,MAAM,OAAO,GAAG,cAAc,CAAC,GAAG,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;IACrD,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACvB,OAAO;YACL,EAAE,EAAE,KAAK;YACT,IAAI,EAAE,sBAAsB;YAC5B,OAAO,EAAE,GAAG,KAAK,KAAK,OAAO,CAAC,MAAM,6BAA6B,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,kEAAkE;SACtJ,CAAC;IACJ,CAAC;IAED,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,GAAG,MAAM,aAAa,CAAC,GAAG,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;IAC7E,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACvB,4EAA4E;QAC5E,4EAA4E;QAC5E,6EAA6E;QAC7E,0EAA0E;QAC1E,6EAA6E;QAC7E,kDAAkD;QAClD,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,gBAAgB,EAAE,OAAO,EAAE,mBAAmB,CAAC,KAAK,EAAE,OAAO,CAAC,EAAE,CAAC;IAC7F,CAAC;IACD,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,CAAC;AACxE,CAAC;AAED;;;;GAIG;AACH,SAAS,KAAK,CAAC,GAAS;IACtB,MAAM,GAAG,GAAG,CAAC,CAAS,EAAU,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;IAC9D,OAAO,GAAG,GAAG,CAAC,WAAW,EAAE,IAAI,GAAG,CAAC,GAAG,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC,IAAI,GAAG,CAAC,GAAG,CAAC,OAAO,EAAE,CAAC,EAAE,CAAC;AACjF,CAAC;AAGD,SAAS,MAAM,CAAC,CAAS,EAAE,IAAY;IACrC,OAAO,GAAG,CAAC,IAAI,IAAI,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC;AAC7C,CAAC","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 * 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, 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 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 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 /**\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(\"--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 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 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 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(\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. 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.today);\n const archPlan = archVerified === null ? null : planStamp(archVerified, req.today);\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 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, lastVerified: string): { 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: lastVerified,\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"]}