@clossys/writer 0.4.0 → 0.5.0

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 (109) hide show
  1. package/README.md +587 -2
  2. package/dist/approval-bypass.d.ts +120 -0
  3. package/dist/approval-bypass.d.ts.map +1 -0
  4. package/dist/approval-bypass.js +2318 -0
  5. package/dist/approval-bypass.js.map +1 -0
  6. package/dist/approval.d.ts +62 -0
  7. package/dist/approval.d.ts.map +1 -0
  8. package/dist/approval.js +421 -0
  9. package/dist/approval.js.map +1 -0
  10. package/dist/approve-cli.d.ts +82 -0
  11. package/dist/approve-cli.d.ts.map +1 -0
  12. package/dist/approve-cli.js +430 -0
  13. package/dist/approve-cli.js.map +1 -0
  14. package/dist/cli.d.ts +15 -0
  15. package/dist/cli.d.ts.map +1 -1
  16. package/dist/cli.js +48 -13
  17. package/dist/cli.js.map +1 -1
  18. package/dist/field-options.d.ts +59 -0
  19. package/dist/field-options.d.ts.map +1 -0
  20. package/dist/field-options.js +109 -0
  21. package/dist/field-options.js.map +1 -0
  22. package/dist/front-door.check.d.ts +2 -0
  23. package/dist/front-door.check.d.ts.map +1 -0
  24. package/dist/front-door.check.js +8 -0
  25. package/dist/front-door.check.js.map +1 -0
  26. package/dist/front-door.d.ts +82 -0
  27. package/dist/front-door.d.ts.map +1 -0
  28. package/dist/front-door.js +271 -0
  29. package/dist/front-door.js.map +1 -0
  30. package/dist/generated/advisor-plan-contract.generated.d.ts +3 -0
  31. package/dist/generated/advisor-plan-contract.generated.d.ts.map +1 -0
  32. package/dist/generated/advisor-plan-contract.generated.js +430 -0
  33. package/dist/generated/advisor-plan-contract.generated.js.map +1 -0
  34. package/dist/generated/contract-schema.generated.d.ts +97 -0
  35. package/dist/generated/contract-schema.generated.d.ts.map +1 -0
  36. package/dist/generated/contract-schema.generated.js +496 -0
  37. package/dist/generated/contract-schema.generated.js.map +1 -0
  38. package/dist/index.d.ts +14 -2
  39. package/dist/index.d.ts.map +1 -1
  40. package/dist/index.js +24 -0
  41. package/dist/index.js.map +1 -1
  42. package/dist/messaging-kit.d.ts +113 -0
  43. package/dist/messaging-kit.d.ts.map +1 -0
  44. package/dist/messaging-kit.js +121 -0
  45. package/dist/messaging-kit.js.map +1 -0
  46. package/dist/passage.d.ts +2 -1
  47. package/dist/passage.d.ts.map +1 -1
  48. package/dist/passage.js +2 -1
  49. package/dist/passage.js.map +1 -1
  50. package/dist/plan-authority.d.ts +73 -0
  51. package/dist/plan-authority.d.ts.map +1 -0
  52. package/dist/plan-authority.js +126 -0
  53. package/dist/plan-authority.js.map +1 -0
  54. package/dist/plan-contract.d.ts +16 -0
  55. package/dist/plan-contract.d.ts.map +1 -0
  56. package/dist/plan-contract.js +33 -0
  57. package/dist/plan-contract.js.map +1 -0
  58. package/dist/plan-digest.d.ts +23 -0
  59. package/dist/plan-digest.d.ts.map +1 -0
  60. package/dist/plan-digest.js +82 -0
  61. package/dist/plan-digest.js.map +1 -0
  62. package/dist/plan-rules.d.ts +54 -0
  63. package/dist/plan-rules.d.ts.map +1 -0
  64. package/dist/plan-rules.js +145 -0
  65. package/dist/plan-rules.js.map +1 -0
  66. package/dist/resolve.d.ts +40 -6
  67. package/dist/resolve.d.ts.map +1 -1
  68. package/dist/resolve.js +144 -18
  69. package/dist/resolve.js.map +1 -1
  70. package/dist/scan.d.ts +32 -0
  71. package/dist/scan.d.ts.map +1 -1
  72. package/dist/scan.js +67 -6
  73. package/dist/scan.js.map +1 -1
  74. package/dist/schema.d.ts +1 -0
  75. package/dist/schema.d.ts.map +1 -1
  76. package/dist/schema.js +209 -5
  77. package/dist/schema.js.map +1 -1
  78. package/dist/site-identity.d.ts +88 -0
  79. package/dist/site-identity.d.ts.map +1 -0
  80. package/dist/site-identity.js +82 -0
  81. package/dist/site-identity.js.map +1 -0
  82. package/dist/types.d.ts +65 -7
  83. package/dist/types.d.ts.map +1 -1
  84. package/dist/types.js +9 -7
  85. package/dist/types.js.map +1 -1
  86. package/package.json +7 -6
  87. package/skill/SKILL.md +2 -2
  88. package/src/approval-bypass.ts +2263 -0
  89. package/src/approval.ts +491 -0
  90. package/src/approve-cli.ts +474 -0
  91. package/src/cli.ts +46 -13
  92. package/src/field-options.ts +134 -0
  93. package/src/front-door.check.ts +10 -0
  94. package/src/front-door.ts +312 -0
  95. package/src/generated/advisor-plan-contract.generated.ts +430 -0
  96. package/src/generated/contract-schema.generated.ts +520 -0
  97. package/src/index.ts +97 -1
  98. package/src/messaging-kit.ts +195 -0
  99. package/src/passage.ts +2 -1
  100. package/src/plan-authority.ts +160 -0
  101. package/src/plan-contract.ts +45 -0
  102. package/src/plan-digest.ts +81 -0
  103. package/src/plan-rules.ts +179 -0
  104. package/src/resolve.ts +185 -19
  105. package/src/scan.ts +97 -6
  106. package/src/schema.ts +223 -5
  107. package/src/site-identity.ts +125 -0
  108. package/src/types.ts +72 -7
  109. package/templates/front-door.en.json +426 -0
@@ -0,0 +1,430 @@
1
+ /**
2
+ * `writer-check approve` — writes a `CopyRegistryEntry.approval` record so
3
+ * nobody has to hand-edit one. It exists as a command, not a hand-edited
4
+ * JSON field, for the same reason `textFingerprint` (`fingerprint.ts`) is a
5
+ * content hash rather than a human-maintained counter: a hand-typed record
6
+ * is easy to get wrong — copying yesterday's `approvedAt`, pinning `textFingerprint` to
7
+ * some other entry's text, or leaving `pendingOwnerReview` set after an
8
+ * owner actually reviewed it. Every field this command writes is either
9
+ * computed (`approvedAt` from the clock, `textFingerprint` from the
10
+ * entry's own current `text` via `computeCopyFingerprint`) or copied
11
+ * verbatim from an argument this command itself validated (`--delegate`,
12
+ * `--scope`, `--expires`) — there is no field here a human types free-form
13
+ * into the JSON.
14
+ *
15
+ * This module owns argument parsing, registry-file I/O, and the mutate-
16
+ * then-atomically-rename write; `./approval.ts`'s `isDelegateScopeItem` and
17
+ * `isEntryInDelegateScope` decide whether a requested id is in a delegate's
18
+ * declared scope, and `./schema.ts`'s `validateCopyRegistryShape` decides
19
+ * whether the registry file is trustworthy enough to mutate at all — this
20
+ * file calls both rather than re-deriving either.
21
+ *
22
+ * `main`-equivalent shape is `mainApproveCommand`, exported (unlike a
23
+ * typical CLI `main`) so this package's tests (which do not ship) can exercise the whole
24
+ * argv-to-exit-code contract directly against real `mkdtemp` fixture
25
+ * files, without spawning a subprocess — the same discipline every other
26
+ * subcommand in this package's `cli.ts` already holds to. `argv` excludes
27
+ * the leading `"approve"` token: `cli.ts`'s top-level dispatch calls
28
+ * `mainApproveCommand(argv.slice(1))` for `writer-check approve ...`, the same way `"passages"` and
29
+ * `"addressability"` are already dispatched there.
30
+ *
31
+ * Exit codes — a three-state contract matching every other gate/command in
32
+ * this package:
33
+ *
34
+ * 0 — written: every named entry was approved or revoked, and the
35
+ * registry file on disk now reflects it.
36
+ * 1 — refused: at least one named entry is unknown, is `retired`, or —
37
+ * for a delegate approval — falls outside `--scope`. This is
38
+ * ALL-OR-NOTHING: when any requested id has a problem, EVERY problem
39
+ * is printed and NOTHING is written, even for the other ids that
40
+ * would otherwise have succeeded. A partial write here would leave a
41
+ * consumer's own audit trail unable to tell whether a whole
42
+ * requested batch was actually reviewed together.
43
+ * 2 — could not run: bad arguments (see below), or the registry file is
44
+ * missing/unreadable, not valid JSON, or fails
45
+ * `validateCopyRegistryShape`. Nothing is written in this state
46
+ * either — this command never mutates a registry it does not first
47
+ * fully trust.
48
+ *
49
+ * Bad-argument cases that produce exit 2, all before the registry file is
50
+ * ever opened: no registry-file argument, zero entry ids, neither `--by`
51
+ * nor `--revoke` (or both), `--by` other than `owner`/`delegate`, `--by
52
+ * owner` combined with `--delegate`/`--scope`/`--expires`, `--by delegate`
53
+ * missing `--delegate` or `--scope`, an empty `--delegate` value, a
54
+ * `--scope` item that fails `isDelegateScopeItem` or an empty scope list,
55
+ * `--revoke` combined with any of `--by`/`--delegate`/`--scope`/
56
+ * `--expires`, an `--expires` value that is not a well-formed ISO 8601 UTC
57
+ * timestamp or is not strictly after the current time, or an unknown flag.
58
+ * `--help`/`-h` always prints usage and returns 0, taking precedence over
59
+ * every other check reachable in the same parse. Any other unexpected
60
+ * exception is caught and mapped to exit 2 as well — this command never
61
+ * lets an uncaught error double as a "something went wrong" report.
62
+ */
63
+ import { randomBytes } from "node:crypto";
64
+ import { existsSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
65
+ import { basename, dirname, join, resolve } from "node:path";
66
+ import { isDelegateScopeItem, isEntryInDelegateScope } from "./approval.js";
67
+ import { COPY_FINGERPRINT_ALGORITHM, computeCopyFingerprint } from "./fingerprint.js";
68
+ import { isIsoUtcTimestamp, validateCopyRegistryShape } from "./schema.js";
69
+ export const APPROVE_USAGE = `Usage: writer-check approve <registry-file> <entry-id>... --by owner
70
+ or: writer-check approve <registry-file> <entry-id>... --by delegate --delegate <id> --scope <ns>[,<ns>...] [--expires <ISO>]
71
+ or: writer-check approve <registry-file> <entry-id>... --revoke
72
+
73
+ registry-file Path to a CopyRegistry JSON file. Required.
74
+ entry-id One or more entry ids to approve or revoke (repeatable positional argument; duplicates are deduplicated). Required.
75
+
76
+ Options:
77
+ --help Print this message and exit 0.
78
+ --by <owner|delegate> Who is recording this approval. Exactly one of --by or --revoke is required.
79
+ --delegate <id> Opaque delegate identifier, never a personal name. Required (and only allowed) with --by delegate.
80
+ --scope <ns>[,<ns>...] One or more dot-separated entry-id namespaces the delegate may approve (repeatable; a comma-separated value and repeated flags union). Required (and only allowed) with --by delegate.
81
+ --expires <ISO> ISO 8601 UTC timestamp (YYYY-MM-DDTHH:MM:SSZ, optionally with milliseconds) after which the delegate approval expires. Must be strictly after the current time. Only allowed with --by delegate.
82
+ --revoke Return every named entry to "draft" and delete its approval record. Cannot be combined with --by, --delegate, --scope, or --expires.
83
+
84
+ Approving an entry that already carries a delegate's approval record with
85
+ --by owner replaces that record with the owner's own — this is how a
86
+ pending delegate approval is durably confirmed.
87
+
88
+ Exit codes: 0 = written (every named entry approved or revoked; the
89
+ registry file was updated), 1 = refused (an unknown, retired, or — for a
90
+ delegate approval — out-of-scope entry id was named; every problem is
91
+ printed and nothing is written), 2 = could not run (bad arguments, or the
92
+ registry file is missing/unreadable/not valid JSON/fails schema
93
+ validation; nothing is written).
94
+ `;
95
+ /** Bad arguments only — never thrown once the registry file has been read. */
96
+ class ApproveInputError extends Error {
97
+ }
98
+ function parseRawArgs(argv) {
99
+ let registryFile;
100
+ const entryIds = [];
101
+ let by;
102
+ let delegate;
103
+ let scope;
104
+ let expires;
105
+ let revoke = false;
106
+ let help = false;
107
+ for (let i = 0; i < argv.length; i++) {
108
+ const arg = argv[i];
109
+ if (arg === "--help" || arg === "-h") {
110
+ help = true;
111
+ continue;
112
+ }
113
+ if (arg === "--revoke") {
114
+ revoke = true;
115
+ continue;
116
+ }
117
+ if (arg === "--by") {
118
+ const value = argv[++i];
119
+ if (value === undefined || value.startsWith("-"))
120
+ throw new ApproveInputError("--by requires a value");
121
+ by = value;
122
+ continue;
123
+ }
124
+ if (arg === "--delegate") {
125
+ const value = argv[++i];
126
+ if (value === undefined || value.startsWith("-"))
127
+ throw new ApproveInputError("--delegate requires a value");
128
+ delegate = value;
129
+ continue;
130
+ }
131
+ if (arg === "--scope") {
132
+ const value = argv[++i];
133
+ if (value === undefined || value.startsWith("-"))
134
+ throw new ApproveInputError("--scope requires a value");
135
+ const tokens = value.split(",").map((token) => token.trim());
136
+ scope = [...(scope ?? []), ...tokens];
137
+ continue;
138
+ }
139
+ if (arg === "--expires") {
140
+ const value = argv[++i];
141
+ if (value === undefined || value.startsWith("-")) {
142
+ throw new ApproveInputError("--expires requires an ISO 8601 UTC timestamp");
143
+ }
144
+ expires = value;
145
+ continue;
146
+ }
147
+ if (arg.startsWith("-")) {
148
+ throw new ApproveInputError(`unknown flag "${arg}"`);
149
+ }
150
+ if (registryFile === undefined) {
151
+ registryFile = arg;
152
+ }
153
+ else {
154
+ entryIds.push(arg);
155
+ }
156
+ }
157
+ return { registryFile, entryIds, by, delegate, scope, expires, revoke, help };
158
+ }
159
+ /**
160
+ * `YYYY-MM-DDTHH:MM:SS(.sss)?Z` — deliberately the same shape
161
+ * `schema.ts`'s own (unexported) `ISO_UTC_TIMESTAMP_RE` accepts for
162
+ * `approvedAt`/`expiresAt`, so a value this command accepts on argv is
163
+ * never one `validateCopyRegistryShape` would then reject from the file
164
+ * this command just wrote. `Date.parse` must also actually accept it, so a
165
+ * string that merely matches the pattern but names an impossible date
166
+ * (e.g. a bad month) is still rejected.
167
+ */
168
+ function validateExpires(raw, now) {
169
+ if (!isIsoUtcTimestamp(raw)) {
170
+ throw new ApproveInputError(`--expires must be an ISO 8601 UTC timestamp (e.g. 2026-01-01T00:00:00Z), got ${JSON.stringify(raw)}`);
171
+ }
172
+ if (Date.parse(raw) <= now.getTime()) {
173
+ throw new ApproveInputError(`--expires ${JSON.stringify(raw)} must be strictly after the current time`);
174
+ }
175
+ return raw;
176
+ }
177
+ /**
178
+ * Turns the raw, syntactically-parsed argv into a fully validated action —
179
+ * every semantic rule this command enforces before ever touching the
180
+ * registry file lives here. Throws `ApproveInputError` on the first
181
+ * violation, matching every other subcommand's `parseArgs` in this
182
+ * package.
183
+ */
184
+ function validateArgs(raw, now) {
185
+ if (!raw.registryFile) {
186
+ throw new ApproveInputError("registry-file is required");
187
+ }
188
+ if (raw.entryIds.length === 0) {
189
+ throw new ApproveInputError("at least one entry-id is required");
190
+ }
191
+ if (!raw.revoke && raw.by === undefined) {
192
+ throw new ApproveInputError("exactly one of --by or --revoke is required");
193
+ }
194
+ let action;
195
+ if (raw.revoke) {
196
+ if (raw.by !== undefined || raw.delegate !== undefined || raw.scope !== undefined || raw.expires !== undefined) {
197
+ throw new ApproveInputError("--revoke cannot be combined with --by, --delegate, --scope, or --expires");
198
+ }
199
+ action = { kind: "revoke" };
200
+ }
201
+ else if (raw.by === "owner") {
202
+ if (raw.delegate !== undefined || raw.scope !== undefined || raw.expires !== undefined) {
203
+ throw new ApproveInputError("--by owner cannot be combined with --delegate, --scope, or --expires");
204
+ }
205
+ action = { kind: "approve-owner" };
206
+ }
207
+ else if (raw.by === "delegate") {
208
+ if (raw.delegate === undefined) {
209
+ throw new ApproveInputError("--by delegate requires --delegate <id>");
210
+ }
211
+ if (raw.delegate.trim().length === 0) {
212
+ throw new ApproveInputError("--delegate must not be empty");
213
+ }
214
+ const delegateId = raw.delegate;
215
+ if (raw.scope === undefined || raw.scope.length === 0) {
216
+ throw new ApproveInputError("--by delegate requires --scope <ns>[,<ns>...]");
217
+ }
218
+ for (const item of raw.scope) {
219
+ if (!isDelegateScopeItem(item)) {
220
+ throw new ApproveInputError(`invalid --scope item ${JSON.stringify(item)}`);
221
+ }
222
+ }
223
+ const expiresAt = raw.expires !== undefined ? validateExpires(raw.expires, now) : undefined;
224
+ action = { kind: "approve-delegate", delegate: delegateId, scope: raw.scope, expiresAt };
225
+ }
226
+ else {
227
+ throw new ApproveInputError(`--by must be "owner" or "delegate", got ${JSON.stringify(raw.by)}`);
228
+ }
229
+ return { registryFile: raw.registryFile, entryIds: [...new Set(raw.entryIds)], action };
230
+ }
231
+ /**
232
+ * Reads, parses, and shape-validates the registry file — never throws.
233
+ * Deliberately does NOT go through `parseCopyRegistry`/`buildCopyRegistry`
234
+ * (`schema.ts`): those rebuild a fresh object from validated fields, which
235
+ * would lose the original file's key order and any field this package
236
+ * does not itself model. This command mutates the raw parsed JSON value in
237
+ * place instead (see `mainApproveCommand`), so it only needs
238
+ * `validateCopyRegistryShape`'s findings, never a rebuilt object.
239
+ */
240
+ function readRegistry(path) {
241
+ let raw;
242
+ try {
243
+ raw = readFileSync(path, "utf8");
244
+ }
245
+ catch (error) {
246
+ return {
247
+ ok: false,
248
+ messages: [`registry file "${path}" could not be read: ${error instanceof Error ? error.message : String(error)}`],
249
+ };
250
+ }
251
+ let parsed;
252
+ try {
253
+ parsed = JSON.parse(raw);
254
+ }
255
+ catch (error) {
256
+ return {
257
+ ok: false,
258
+ messages: [`registry file "${path}" is not valid JSON: ${error instanceof Error ? error.message : String(error)}`],
259
+ };
260
+ }
261
+ const findings = validateCopyRegistryShape(parsed);
262
+ if (findings.length > 0) {
263
+ return {
264
+ ok: false,
265
+ messages: findings.map((f) => `[${f.rule}]${f.path ? ` ${f.path}:` : ""} ${f.message}`),
266
+ };
267
+ }
268
+ return { ok: true, parsed: parsed, messages: [] };
269
+ }
270
+ function indexEntries(parsed) {
271
+ const entries = parsed.entries;
272
+ const byId = new Map();
273
+ for (const raw of entries) {
274
+ const entry = raw;
275
+ byId.set(entry.id, entry);
276
+ }
277
+ return byId;
278
+ }
279
+ /**
280
+ * All-or-nothing pre-flight: every requested id that is unknown, retired,
281
+ * or (for a delegate approval) outside `--scope` is collected here, so
282
+ * `mainApproveCommand` can print every problem and refuse the WHOLE batch
283
+ * — never write some of the requested entries and skip others.
284
+ */
285
+ function findProblems(entriesById, entryIds, action) {
286
+ const problems = [];
287
+ for (const id of entryIds) {
288
+ const entry = entriesById.get(id);
289
+ if (!entry) {
290
+ problems.push(`unknown entry id "${id}"`);
291
+ continue;
292
+ }
293
+ if (entry.status === "retired") {
294
+ problems.push(`entry "${id}" is retired and cannot be approved or revoked`);
295
+ continue;
296
+ }
297
+ if (action.kind === "approve-delegate" && !isEntryInDelegateScope(id, action.scope)) {
298
+ problems.push(`entry "${id}" is outside delegate scope [${action.scope.join(", ")}]`);
299
+ }
300
+ }
301
+ return problems;
302
+ }
303
+ /**
304
+ * Builds a fresh `CopyApproval`-shaped record with keys in the exact order
305
+ * this package's contract fixes: `approvedBy`, `approvedAt`,
306
+ * `textFingerprint`, `fingerprintAlgorithm`, then — delegate records only —
307
+ * `delegate`, `pendingOwnerReview`, `expiresAt`. Assigning this whole
308
+ * object to `entry.approval` either appends a new `approval` key at the
309
+ * end of the entry (it did not exist before) or replaces an existing one
310
+ * in place (its position in the entry is unchanged, because reassigning an
311
+ * existing object key never moves it) — see this file's top doc comment
312
+ * and `mainApproveCommand` for why that is exactly the key-order contract
313
+ * this command must preserve.
314
+ */
315
+ function buildApprovalRecord(action, entry, now) {
316
+ const record = {
317
+ approvedBy: action.kind === "approve-delegate" ? "delegate" : "owner",
318
+ approvedAt: now.toISOString(),
319
+ textFingerprint: computeCopyFingerprint(entry.text),
320
+ fingerprintAlgorithm: COPY_FINGERPRINT_ALGORITHM,
321
+ };
322
+ if (action.kind === "approve-delegate") {
323
+ record.delegate = { id: action.delegate, scope: [...action.scope] };
324
+ record.pendingOwnerReview = true;
325
+ if (action.expiresAt !== undefined) {
326
+ record.expiresAt = action.expiresAt;
327
+ }
328
+ }
329
+ return record;
330
+ }
331
+ /**
332
+ * Writes `data` to `path` by writing a temp file in the SAME directory,
333
+ * then `renameSync`-ing it into place, so the registry file is replaced by
334
+ * one rename rather than rewritten in place. The temp file is cleaned up
335
+ * on any failure; a failure after a successful `renameSync` cannot happen,
336
+ * since nothing runs after it.
337
+ */
338
+ function writeRegistryFile(path, data) {
339
+ const dir = dirname(path);
340
+ const base = basename(path);
341
+ const tempPath = join(dir, `.${base}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`);
342
+ const content = `${JSON.stringify(data, null, 2)}\n`;
343
+ try {
344
+ writeFileSync(tempPath, content, "utf8");
345
+ renameSync(tempPath, path);
346
+ }
347
+ catch (error) {
348
+ try {
349
+ if (existsSync(tempPath))
350
+ unlinkSync(tempPath);
351
+ }
352
+ catch {
353
+ // Best-effort cleanup only — the original error below is what matters.
354
+ }
355
+ throw error;
356
+ }
357
+ }
358
+ /**
359
+ * Exported (unlike a typical CLI `main`) so this package's tests (which do
360
+ * not ship) can exercise the whole argv-to-exit-code contract directly against real
361
+ * `mkdtemp` fixture files, without spawning a subprocess — see this file's
362
+ * top doc comment for the full exit-code contract. `argv` excludes the
363
+ * leading `"approve"` subcommand token. `io.now` lets a test pin the clock
364
+ * that `approvedAt` and `--expires` validation are measured against;
365
+ * omitted, it defaults to `new Date()`.
366
+ *
367
+ * Never throws: every failure, expected or not, is caught here and mapped
368
+ * to an exit code — bad arguments and an untrustworthy registry file both
369
+ * become 2 with nothing written, a refused (but well-formed) request
370
+ * becomes 1 with nothing written, and a clean run becomes 0 with the
371
+ * registry file updated on disk.
372
+ */
373
+ export function mainApproveCommand(argv, io = {}) {
374
+ try {
375
+ const now = io.now ?? new Date();
376
+ const raw = parseRawArgs(argv);
377
+ if (raw.help) {
378
+ console.log(APPROVE_USAGE);
379
+ return 0;
380
+ }
381
+ const validated = validateArgs(raw, now);
382
+ const registryFile = resolve(validated.registryFile);
383
+ const read = readRegistry(registryFile);
384
+ if (!read.ok || !read.parsed) {
385
+ console.error(`[approve] registry could not be loaded:`);
386
+ for (const message of read.messages)
387
+ console.error(` ${message}`);
388
+ console.error("[approve] Refusing to modify a registry that could not be fully validated.");
389
+ return 2;
390
+ }
391
+ const entriesById = indexEntries(read.parsed);
392
+ const problems = findProblems(entriesById, validated.entryIds, validated.action);
393
+ if (problems.length > 0) {
394
+ console.error(`[approve] refusing to write — ${problems.length} problem(s) with the requested entries:`);
395
+ for (const problem of problems)
396
+ console.error(` ${problem}`);
397
+ return 1;
398
+ }
399
+ const lines = [];
400
+ for (const id of validated.entryIds) {
401
+ const entry = entriesById.get(id);
402
+ if (validated.action.kind === "revoke") {
403
+ entry.status = "draft";
404
+ delete entry.approval;
405
+ lines.push(`[approve] ${id}: revoked (now draft)`);
406
+ }
407
+ else {
408
+ entry.status = "approved";
409
+ entry.approval = buildApprovalRecord(validated.action, entry, now);
410
+ lines.push(validated.action.kind === "approve-delegate"
411
+ ? `[approve] ${id}: approved by delegate ${validated.action.delegate} (pending owner review)`
412
+ : `[approve] ${id}: approved by owner`);
413
+ }
414
+ }
415
+ writeRegistryFile(registryFile, read.parsed);
416
+ for (const line of lines)
417
+ console.log(line);
418
+ return 0;
419
+ }
420
+ catch (error) {
421
+ if (error instanceof ApproveInputError) {
422
+ console.error(`writer-check approve: ${error.message}`);
423
+ console.error(`\n${APPROVE_USAGE}`);
424
+ return 2;
425
+ }
426
+ console.error(`writer-check approve: unexpected error: ${error instanceof Error ? (error.stack ?? error.message) : String(error)}`);
427
+ return 2;
428
+ }
429
+ }
430
+ //# sourceMappingURL=approve-cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"approve-cli.js","sourceRoot":"","sources":["../src/approve-cli.ts"],"names":[],"mappings":"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"}
package/dist/cli.d.ts CHANGED
@@ -109,6 +109,21 @@
109
109
  * all. This is the explicit third state this gate is built around:
110
110
  * "could not check" must never be reported as a pass.
111
111
  *
112
+ * A FIFTH and SIXTH subcommand, `approve` and `approval-state`, are
113
+ * dispatched the same way `addressability` and `passages` are — a fully
114
+ * separate top-level branch at the very bottom of this file, never folded
115
+ * through `main()`. `writer-check approve` writes a
116
+ * `CopyRegistryEntry.approval` record so nobody hand-edits one; its whole
117
+ * argument-parsing, validation, and atomic-write contract lives in
118
+ * `mainApproveCommand` (`./approve-cli.ts`) — this file only wires its
119
+ * exit code through. `writer-check approval-state` reports the current
120
+ * approval state of a registry from two angles at once: whether every
121
+ * approved entry's own record is current (`assessCopyApprovals`) and
122
+ * whether consumer source actually routes copy through the registry's
123
+ * approval lifecycle rather than around it (`scanApprovalBypass`/
124
+ * `checkApprovalBypass`); its own contract lives in
125
+ * `mainApprovalStateCommand` (`./approval.ts`).
126
+ *
112
127
  * A malformed `pathExclusions` entry (see `scan.ts`/`path-exclusions.ts`)
113
128
  * gets the same `2` treatment as `unchecked`, for the identical reason: an
114
129
  * exclusion list this run cannot trust means this run cannot say which
package/dist/cli.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwHG;AAWH,OAAO,EAAyB,KAAK,eAAe,EAAE,KAAK,eAAe,EAAuB,MAAM,gBAAgB,CAAC;AAMxH,OAAO,EAAuC,KAAK,aAAa,EAAE,MAAM,WAAW,CAAC;AAsEpF,qHAAqH;AACrH,qBAAa,aAAc,SAAQ,KAAK;CAAG;AAojB3C;;;;;;;GAOG;AACH;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,MAAM,WAAW,sBAAsB;IACrC,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,OAAO,GAAG,UAAU,GAAG,eAAe,CAAC;IAChD,QAAQ,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,MAAM,CAAC;IACrB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,eAAe,EAAE,CAAC;IAC5B,OAAO,EAAE,eAAe,EAAE,CAAC;IAC3B,SAAS,EAAE,aAAa,EAAE,CAAC;IAC3B,aAAa,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;CACnD;AAmBD,wBAAgB,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,MAAM,CA2R3C;AAsGD;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,MAAM,CAqCxD;AA4ID;;;;;GAKG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,MAAM,CAwC9D"}
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuIG;AAWH,OAAO,EAAyB,KAAK,eAAe,EAAE,KAAK,eAAe,EAAuB,MAAM,gBAAgB,CAAC;AAQxH,OAAO,EAAuC,KAAK,aAAa,EAAE,MAAM,WAAW,CAAC;AA6EpF,qHAAqH;AACrH,qBAAa,aAAc,SAAQ,KAAK;CAAG;AAojB3C;;;;;;;GAOG;AACH;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,MAAM,WAAW,sBAAsB;IACrC,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,OAAO,GAAG,UAAU,GAAG,eAAe,CAAC;IAChD,QAAQ,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,MAAM,CAAC;IACrB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,eAAe,EAAE,CAAC;IAC5B,OAAO,EAAE,eAAe,EAAE,CAAC;IAC3B,SAAS,EAAE,aAAa,EAAE,CAAC;IAC3B,aAAa,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;CACnD;AAmBD,wBAAgB,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,MAAM,CA2R3C;AAsGD;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,MAAM,CAqCxD;AA4ID;;;;;GAKG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,MAAM,CAwC9D"}
package/dist/cli.js CHANGED
@@ -109,6 +109,21 @@
109
109
  * all. This is the explicit third state this gate is built around:
110
110
  * "could not check" must never be reported as a pass.
111
111
  *
112
+ * A FIFTH and SIXTH subcommand, `approve` and `approval-state`, are
113
+ * dispatched the same way `addressability` and `passages` are — a fully
114
+ * separate top-level branch at the very bottom of this file, never folded
115
+ * through `main()`. `writer-check approve` writes a
116
+ * `CopyRegistryEntry.approval` record so nobody hand-edits one; its whole
117
+ * argument-parsing, validation, and atomic-write contract lives in
118
+ * `mainApproveCommand` (`./approve-cli.ts`) — this file only wires its
119
+ * exit code through. `writer-check approval-state` reports the current
120
+ * approval state of a registry from two angles at once: whether every
121
+ * approved entry's own record is current (`assessCopyApprovals`) and
122
+ * whether consumer source actually routes copy through the registry's
123
+ * approval lifecycle rather than around it (`scanApprovalBypass`/
124
+ * `checkApprovalBypass`); its own contract lives in
125
+ * `mainApprovalStateCommand` (`./approval.ts`).
126
+ *
112
127
  * A malformed `pathExclusions` entry (see `scan.ts`/`path-exclusions.ts`)
113
128
  * gets the same `2` treatment as `unchecked`, for the identical reason: an
114
129
  * exclusion list this run cannot trust means this run cannot say which
@@ -128,6 +143,8 @@ import { checkCopyTraceability } from "./copy-gate.js";
128
143
  import { checkLocaleCoverage } from "./locale-coverage.js";
129
144
  import { liveTreeExists, scanLiveCopyTrees } from "./live-copy-gate.js";
130
145
  import { checkPassageComposition, readPassageRecord } from "./passage.js";
146
+ import { mainApprovalStateCommand } from "./approval.js";
147
+ import { mainApproveCommand } from "./approve-cli.js";
131
148
  import { readCopyRecord } from "./registry.js";
132
149
  import { checkRenderRegistryParity } from "./render-registry-parity.js";
133
150
  import { scanCopySourceTree } from "./scan.js";
@@ -140,13 +157,18 @@ const USAGE = `Usage: writer-check <record-file> [scan-dir] [options]
140
157
  record-file Path to a CopyRecord JSON file (see @clossys/writer's README). Required.
141
158
  scan-dir Directory to scan for user-facing string/template literals. Defaults to the current working directory.
142
159
 
143
- See also "writer-check addressability [scan-dir]" and "writer-check passages
144
- <registry-file>" (this same file's other subcommands, both dispatched on
145
- argv[0] before anything below) — addressability asks whether user-facing
146
- prose is resolved from the copy registry by id, rather than typed inline;
147
- passages asks whether a passage composes registered entries/terms rather
148
- than inlining a literal or reaching into another passage's internals. Both
149
- are separate, stricter gates; neither runs as part of this default command.
160
+ See also "writer-check addressability [scan-dir]", "writer-check passages
161
+ <registry-file>", "writer-check approve <registry-file> <entry-id>...", and
162
+ "writer-check approval-state <registry-file> <scan-dir>" (this same file's
163
+ other subcommands, all dispatched on argv[0] before anything below) —
164
+ addressability asks whether user-facing prose is resolved from the copy
165
+ registry by id, rather than typed inline; passages asks whether a passage
166
+ composes registered entries/terms rather than inlining a literal or
167
+ reaching into another passage's internals; approve writes an entry's
168
+ approval record; approval-state reports whether a
169
+ registry's approval records are current and whether consumer source
170
+ actually routes copy through them. All four are separate gates/commands;
171
+ none runs as part of this default command.
150
172
 
151
173
  Options:
152
174
  --help Print this message and exit 0.
@@ -170,6 +192,8 @@ Run "writer-check voice-derivation-coverage --help" for the second subcommand's
170
192
  Run "writer-check locale-coverage --help" for the third subcommand's own usage.
171
193
  Run "writer-check addressability --help" for that subcommand's own usage.
172
194
  Run "writer-check passages --help" for the passage-composition subcommand's own usage.
195
+ Run "writer-check approve --help" for the approval-recording subcommand's own usage.
196
+ Run "writer-check approval-state --help" for the approval-state subcommand's own usage.
173
197
  `;
174
198
  const VOICE_DERIVATION_COVERAGE_USAGE = `Usage: writer-check voice-derivation-coverage <obligations-file> <brand-derived-rule-ids-file> [options]
175
199
 
@@ -1301,11 +1325,12 @@ function detectMainModule() {
1301
1325
  }
1302
1326
  }
1303
1327
  /**
1304
- * `writer-check`, `writer-check addressability`, and `writer-check passages`
1305
- * are ONE `bin` entry, one compiled file, dispatched by an explicit FIRST
1306
- * ARGUMENT — never by how this file was invoked (its own path, a symlink
1307
- * name, an installed `bin` shim, ...). That is deliberate: this
1308
- * repository's own root `package.json` invokes every gate by compiled path
1328
+ * `writer-check`, `writer-check addressability`, `writer-check passages`,
1329
+ * `writer-check approve`, and `writer-check approval-state` are ONE `bin`
1330
+ * entry, one compiled file, dispatched by an explicit FIRST ARGUMENT — never
1331
+ * by how this file was invoked (its own path, a symlink name, an installed
1332
+ * `bin` shim, ...). That is deliberate: this repository's own root
1333
+ * `package.json` invokes every gate by compiled path
1309
1334
  * (`node packages/ui/dist/tokens/contrast-cli.js`,
1310
1335
  * `node packages/controller/dist/cli.js`, ...), never by `bin` name, so
1311
1336
  * any dispatch keyed on the invoking path/name (a second `bin` entry
@@ -1315,7 +1340,11 @@ function detectMainModule() {
1315
1340
  * exactly the "runs the wrong thing without ever failing" failure mode a
1316
1341
  * gate must never have. `rawArgs[0]` is read BEFORE `main()`'s own
1317
1342
  * `parseArgs` ever sees the array, so the default command's argument
1318
- * shape is completely unaffected by either subcommand existing at all.
1343
+ * shape is completely unaffected by any subcommand existing at all.
1344
+ * `mainApproveCommand`/`mainApprovalStateCommand` never throw (see their own
1345
+ * doc comments), so — unlike `runAddressabilityCheck`/`runPassagesCheck`
1346
+ * above — their branches assign `process.exitCode` directly, with no
1347
+ * try/catch wrapper of their own to add.
1319
1348
  */
1320
1349
  if (detectMainModule()) {
1321
1350
  const rawArgs = process.argv.slice(2);
@@ -1325,6 +1354,12 @@ if (detectMainModule()) {
1325
1354
  else if (rawArgs[0] === "passages") {
1326
1355
  runPassagesCheck(rawArgs.slice(1));
1327
1356
  }
1357
+ else if (rawArgs[0] === "approve") {
1358
+ process.exitCode = mainApproveCommand(rawArgs.slice(1));
1359
+ }
1360
+ else if (rawArgs[0] === "approval-state") {
1361
+ process.exitCode = mainApprovalStateCommand(rawArgs.slice(1));
1362
+ }
1328
1363
  else {
1329
1364
  run();
1330
1365
  }