@nebulacomponents/citable 1.18.1 → 1.18.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 (135) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/dist/universal/.agents/skills/citable/SKILL.md +1 -1
  3. package/dist/universal/.agents/skills/citable/VERSION +1 -1
  4. package/dist/universal/.agents/skills/citable/commands/artifact-interchange.md +5 -0
  5. package/dist/universal/.agents/skills/citable/commands/remediation.md +1 -1
  6. package/dist/universal/.agents/skills/citable/commands/sow.md +2 -2
  7. package/dist/universal/.agents/skills/citable/manifest.json +11 -9
  8. package/dist/universal/.agents/skills/citable/schemas/cro-report.schema.json +112 -0
  9. package/dist/universal/.agents/skills/citable/schemas/search-report.schema.json +99 -0
  10. package/dist/universal/.agents/skills/citable/schemas/sow.schema.json +3 -2
  11. package/dist/universal/.claude/agents/citable/manifest.json +2 -2
  12. package/dist/universal/.claude/skills/citable/SKILL.md +1 -1
  13. package/dist/universal/.claude/skills/citable/VERSION +1 -1
  14. package/dist/universal/.claude/skills/citable/commands/artifact-interchange.md +5 -0
  15. package/dist/universal/.claude/skills/citable/commands/remediation.md +1 -1
  16. package/dist/universal/.claude/skills/citable/commands/sow.md +2 -2
  17. package/dist/universal/.claude/skills/citable/manifest.json +11 -9
  18. package/dist/universal/.claude/skills/citable/schemas/cro-report.schema.json +112 -0
  19. package/dist/universal/.claude/skills/citable/schemas/search-report.schema.json +99 -0
  20. package/dist/universal/.claude/skills/citable/schemas/sow.schema.json +3 -2
  21. package/dist/universal/.cursor/skills/citable/SKILL.md +1 -1
  22. package/dist/universal/.cursor/skills/citable/VERSION +1 -1
  23. package/dist/universal/.cursor/skills/citable/commands/artifact-interchange.md +5 -0
  24. package/dist/universal/.cursor/skills/citable/commands/remediation.md +1 -1
  25. package/dist/universal/.cursor/skills/citable/commands/sow.md +2 -2
  26. package/dist/universal/.cursor/skills/citable/manifest.json +11 -9
  27. package/dist/universal/.cursor/skills/citable/schemas/cro-report.schema.json +112 -0
  28. package/dist/universal/.cursor/skills/citable/schemas/search-report.schema.json +99 -0
  29. package/dist/universal/.cursor/skills/citable/schemas/sow.schema.json +3 -2
  30. package/dist/universal/.gemini/skills/citable/SKILL.md +1 -1
  31. package/dist/universal/.gemini/skills/citable/VERSION +1 -1
  32. package/dist/universal/.gemini/skills/citable/commands/artifact-interchange.md +5 -0
  33. package/dist/universal/.gemini/skills/citable/commands/remediation.md +1 -1
  34. package/dist/universal/.gemini/skills/citable/commands/sow.md +2 -2
  35. package/dist/universal/.gemini/skills/citable/manifest.json +11 -9
  36. package/dist/universal/.gemini/skills/citable/schemas/cro-report.schema.json +112 -0
  37. package/dist/universal/.gemini/skills/citable/schemas/search-report.schema.json +99 -0
  38. package/dist/universal/.gemini/skills/citable/schemas/sow.schema.json +3 -2
  39. package/dist/universal/.github/skills/citable/SKILL.md +1 -1
  40. package/dist/universal/.github/skills/citable/VERSION +1 -1
  41. package/dist/universal/.github/skills/citable/commands/artifact-interchange.md +5 -0
  42. package/dist/universal/.github/skills/citable/commands/remediation.md +1 -1
  43. package/dist/universal/.github/skills/citable/commands/sow.md +2 -2
  44. package/dist/universal/.github/skills/citable/manifest.json +11 -9
  45. package/dist/universal/.github/skills/citable/schemas/cro-report.schema.json +112 -0
  46. package/dist/universal/.github/skills/citable/schemas/search-report.schema.json +99 -0
  47. package/dist/universal/.github/skills/citable/schemas/sow.schema.json +3 -2
  48. package/dist/universal/.kiro/skills/citable/SKILL.md +1 -1
  49. package/dist/universal/.kiro/skills/citable/VERSION +1 -1
  50. package/dist/universal/.kiro/skills/citable/commands/artifact-interchange.md +5 -0
  51. package/dist/universal/.kiro/skills/citable/commands/remediation.md +1 -1
  52. package/dist/universal/.kiro/skills/citable/commands/sow.md +2 -2
  53. package/dist/universal/.kiro/skills/citable/manifest.json +11 -9
  54. package/dist/universal/.kiro/skills/citable/schemas/cro-report.schema.json +112 -0
  55. package/dist/universal/.kiro/skills/citable/schemas/search-report.schema.json +99 -0
  56. package/dist/universal/.kiro/skills/citable/schemas/sow.schema.json +3 -2
  57. package/dist/universal/.opencode/skills/citable/SKILL.md +1 -1
  58. package/dist/universal/.opencode/skills/citable/VERSION +1 -1
  59. package/dist/universal/.opencode/skills/citable/commands/artifact-interchange.md +5 -0
  60. package/dist/universal/.opencode/skills/citable/commands/remediation.md +1 -1
  61. package/dist/universal/.opencode/skills/citable/commands/sow.md +2 -2
  62. package/dist/universal/.opencode/skills/citable/manifest.json +11 -9
  63. package/dist/universal/.opencode/skills/citable/schemas/cro-report.schema.json +112 -0
  64. package/dist/universal/.opencode/skills/citable/schemas/search-report.schema.json +99 -0
  65. package/dist/universal/.opencode/skills/citable/schemas/sow.schema.json +3 -2
  66. package/dist/universal/.pi/agent/skills/citable/SKILL.md +1 -1
  67. package/dist/universal/.pi/agent/skills/citable/VERSION +1 -1
  68. package/dist/universal/.pi/agent/skills/citable/commands/artifact-interchange.md +5 -0
  69. package/dist/universal/.pi/agent/skills/citable/commands/remediation.md +1 -1
  70. package/dist/universal/.pi/agent/skills/citable/commands/sow.md +2 -2
  71. package/dist/universal/.pi/agent/skills/citable/manifest.json +11 -9
  72. package/dist/universal/.pi/agent/skills/citable/schemas/cro-report.schema.json +112 -0
  73. package/dist/universal/.pi/agent/skills/citable/schemas/search-report.schema.json +99 -0
  74. package/dist/universal/.pi/agent/skills/citable/schemas/sow.schema.json +3 -2
  75. package/dist/universal/.qoder/skills/citable/SKILL.md +1 -1
  76. package/dist/universal/.qoder/skills/citable/VERSION +1 -1
  77. package/dist/universal/.qoder/skills/citable/commands/artifact-interchange.md +5 -0
  78. package/dist/universal/.qoder/skills/citable/commands/remediation.md +1 -1
  79. package/dist/universal/.qoder/skills/citable/commands/sow.md +2 -2
  80. package/dist/universal/.qoder/skills/citable/manifest.json +11 -9
  81. package/dist/universal/.qoder/skills/citable/schemas/cro-report.schema.json +112 -0
  82. package/dist/universal/.qoder/skills/citable/schemas/search-report.schema.json +99 -0
  83. package/dist/universal/.qoder/skills/citable/schemas/sow.schema.json +3 -2
  84. package/dist/universal/.rovodev/skills/citable/SKILL.md +1 -1
  85. package/dist/universal/.rovodev/skills/citable/VERSION +1 -1
  86. package/dist/universal/.rovodev/skills/citable/commands/artifact-interchange.md +5 -0
  87. package/dist/universal/.rovodev/skills/citable/commands/remediation.md +1 -1
  88. package/dist/universal/.rovodev/skills/citable/commands/sow.md +2 -2
  89. package/dist/universal/.rovodev/skills/citable/manifest.json +11 -9
  90. package/dist/universal/.rovodev/skills/citable/schemas/cro-report.schema.json +112 -0
  91. package/dist/universal/.rovodev/skills/citable/schemas/search-report.schema.json +99 -0
  92. package/dist/universal/.rovodev/skills/citable/schemas/sow.schema.json +3 -2
  93. package/dist/universal/.trae/skills/citable/SKILL.md +1 -1
  94. package/dist/universal/.trae/skills/citable/VERSION +1 -1
  95. package/dist/universal/.trae/skills/citable/commands/artifact-interchange.md +5 -0
  96. package/dist/universal/.trae/skills/citable/commands/remediation.md +1 -1
  97. package/dist/universal/.trae/skills/citable/commands/sow.md +2 -2
  98. package/dist/universal/.trae/skills/citable/manifest.json +11 -9
  99. package/dist/universal/.trae/skills/citable/schemas/cro-report.schema.json +112 -0
  100. package/dist/universal/.trae/skills/citable/schemas/search-report.schema.json +99 -0
  101. package/dist/universal/.trae/skills/citable/schemas/sow.schema.json +3 -2
  102. package/dist/universal/.trae-cn/skills/citable/SKILL.md +1 -1
  103. package/dist/universal/.trae-cn/skills/citable/VERSION +1 -1
  104. package/dist/universal/.trae-cn/skills/citable/commands/artifact-interchange.md +5 -0
  105. package/dist/universal/.trae-cn/skills/citable/commands/remediation.md +1 -1
  106. package/dist/universal/.trae-cn/skills/citable/commands/sow.md +2 -2
  107. package/dist/universal/.trae-cn/skills/citable/manifest.json +11 -9
  108. package/dist/universal/.trae-cn/skills/citable/schemas/cro-report.schema.json +112 -0
  109. package/dist/universal/.trae-cn/skills/citable/schemas/search-report.schema.json +99 -0
  110. package/dist/universal/.trae-cn/skills/citable/schemas/sow.schema.json +3 -2
  111. package/dist/universal/manifest.json +136 -112
  112. package/package.json +2 -1
  113. package/schemas/cro-report.schema.json +112 -0
  114. package/schemas/search-report.schema.json +99 -0
  115. package/schemas/sow.schema.json +3 -2
  116. package/skill/SKILL.md +1 -1
  117. package/skill/commands/artifact-interchange.md +5 -0
  118. package/skill/commands/remediation.md +1 -1
  119. package/skill/commands/sow.md +2 -2
  120. package/src/analysis/behavioral.js +5 -4
  121. package/src/analysis/eeat.js +10 -9
  122. package/src/analysis/offpage.js +19 -36
  123. package/src/analysis/readiness.js +8 -2
  124. package/src/artifacts/verifyCustomerArtifact.js +283 -0
  125. package/src/cli/index.js +8 -1
  126. package/src/reporting/executiveCroReport.js +393 -229
  127. package/src/reporting/executiveExport.js +6 -2
  128. package/src/reporting/executiveSearchReport.js +519 -271
  129. package/src/shared/domainUtils.js +140 -0
  130. package/src/shared/epistemicStatus.js +111 -0
  131. package/src/shared/evidenceSourceResolver.js +286 -0
  132. package/src/shared/htmlEscape.js +65 -0
  133. package/src/shared/runPackageVerifier.js +153 -0
  134. package/src/sow/admissibilityGate.js +20 -2
  135. package/src/sow/generateSow.js +125 -159
@@ -0,0 +1,283 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { readJson } from '../shared/io.js';
4
+ import { validateAgainst } from '../shared/schemaValidator.js';
5
+
6
+ export class CustomerArtifactVerificationError extends Error {
7
+ constructor(message, errors = []) {
8
+ super(message);
9
+ this.name = 'CustomerArtifactVerificationError';
10
+ this.errors = errors;
11
+ }
12
+ }
13
+
14
+ const DANGEROUS_PATTERNS = [
15
+ /<script\b[^>]*>/i,
16
+ /javascript\s*:/i,
17
+ /on(?:load|error|click|mouseover|focus|blur|submit)\s*=/i,
18
+ /<iframe\b[^>]*>/i,
19
+ /<object\b[^>]*>/i,
20
+ /<embed\b[^>]*>/i,
21
+ ];
22
+
23
+ function scanForInjections(value, pathPrefix = '', found = []) {
24
+ if (typeof value === 'string') {
25
+ for (const pattern of DANGEROUS_PATTERNS) {
26
+ if (pattern.test(value)) {
27
+ found.push(`Potential unescaped script/event injection at ${pathPrefix}: "${value.slice(0, 80)}"`);
28
+ break;
29
+ }
30
+ }
31
+ } else if (Array.isArray(value)) {
32
+ for (let i = 0; i < value.length; i++) {
33
+ scanForInjections(value[i], `${pathPrefix}[${i}]`, found);
34
+ }
35
+ } else if (value && typeof value === 'object') {
36
+ for (const [k, v] of Object.entries(value)) {
37
+ scanForInjections(v, pathPrefix ? `${pathPrefix}.${k}` : k, found);
38
+ }
39
+ }
40
+ return found;
41
+ }
42
+
43
+ /**
44
+ * Identify artifact type and validate customer artifact against schema,
45
+ * epistemic invariants, arithmetic integrity, and injection hazards.
46
+ */
47
+ export async function verifyCustomerArtifact(artifactPathOrData, options = {}) {
48
+ const { failClosed = false, contractual = false } = options;
49
+ const errors = [];
50
+ const warnings = [];
51
+
52
+ let rawContent = null;
53
+ let data = null;
54
+ let format = 'json';
55
+ let artifactPath = null;
56
+
57
+ if (typeof artifactPathOrData === 'string') {
58
+ if (fs.existsSync(artifactPathOrData)) {
59
+ artifactPath = artifactPathOrData;
60
+ rawContent = fs.readFileSync(artifactPathOrData, 'utf8');
61
+ const ext = path.extname(artifactPathOrData).toLowerCase();
62
+ if (ext === '.html' || ext === '.htm') {
63
+ format = 'html';
64
+ } else if (ext === '.md' || ext === '.markdown') {
65
+ format = 'markdown';
66
+ } else {
67
+ try {
68
+ data = JSON.parse(rawContent);
69
+ format = 'json';
70
+ } catch (e) {
71
+ errors.push(`Failed to parse JSON file ${artifactPathOrData}: ${e.message}`);
72
+ }
73
+ }
74
+ } else {
75
+ // String content passed directly
76
+ rawContent = artifactPathOrData;
77
+ if (rawContent.trim().startsWith('{')) {
78
+ try {
79
+ data = JSON.parse(rawContent);
80
+ format = 'json';
81
+ } catch {
82
+ format = 'text';
83
+ }
84
+ } else if (rawContent.includes('<!DOCTYPE html>') || rawContent.includes('<html')) {
85
+ format = 'html';
86
+ } else {
87
+ format = 'markdown';
88
+ }
89
+ }
90
+ } else if (typeof artifactPathOrData === 'object' && artifactPathOrData !== null) {
91
+ data = artifactPathOrData;
92
+ format = 'json';
93
+ } else {
94
+ errors.push('Invalid artifact input: must be file path, string content, or JSON object');
95
+ }
96
+
97
+ if (errors.length > 0) {
98
+ if (failClosed) throw new CustomerArtifactVerificationError(errors.join('; '), errors);
99
+ return { valid: false, artifact_type: 'UNKNOWN', format, errors, warnings };
100
+ }
101
+
102
+ let artifactType = 'UNKNOWN';
103
+
104
+ // 1. JSON Verification
105
+ if (format === 'json' && data) {
106
+ // Detect artifact type
107
+ if (data.sow_id || data.traceability_matrix || (data.$schema && data.$schema.includes('sow'))) {
108
+ artifactType = 'SOW';
109
+ } else if (data.fact_status === 'enterprise_search_executive_report' || data.overall_visibility_score !== undefined) {
110
+ artifactType = 'SEARCH_REPORT';
111
+ } else if (data.fact_status === 'enterprise_cro_executive_report' || data.overall_conversion_readiness !== undefined) {
112
+ artifactType = 'CRO_REPORT';
113
+ }
114
+
115
+ // Schema validation
116
+ if (artifactType === 'SOW') {
117
+ const val = validateAgainst('sow.schema.json', data);
118
+ if (!val.valid) {
119
+ errors.push(...val.errors.map((e) => `SOW schema violation: ${e}`));
120
+ }
121
+
122
+ // Check commercial arithmetic
123
+ if (typeof data.commercial_total_fee_minor === 'number' && typeof data.commercial_total_fee_usd === 'number') {
124
+ if (data.commercial_total_fee_minor !== Math.round(data.commercial_total_fee_usd * 100)) {
125
+ errors.push(`SOW commercial fee mismatch: minor ${data.commercial_total_fee_minor} != usd ${data.commercial_total_fee_usd}`);
126
+ }
127
+ }
128
+
129
+ // Milestones check
130
+ if (data.delivery_schedule?.milestones) {
131
+ const minorSum = data.delivery_schedule.milestones.reduce((acc, m) => acc + (m.fee_minor || 0), 0);
132
+ if (data.commercial_total_fee_minor !== undefined && minorSum !== data.commercial_total_fee_minor) {
133
+ errors.push(`SOW milestone fees sum (${minorSum}) does not match total fee (${data.commercial_total_fee_minor})`);
134
+ }
135
+ }
136
+
137
+ // Admissibility gate balance
138
+ if (data.admissibility_gate) {
139
+ const { total_findings_evaluated, admitted_count, refused_count } = data.admissibility_gate;
140
+ if (total_findings_evaluated !== admitted_count + refused_count) {
141
+ errors.push(`SOW admissibility numbers do not balance: ${total_findings_evaluated} != ${admitted_count} + ${refused_count}`);
142
+ }
143
+ }
144
+
145
+ // Contractual mode checks
146
+ const isContractual = data.generation_mode === 'CONTRACTUAL' || contractual;
147
+ if (isContractual) {
148
+ if (data.synthetic_evidence) {
149
+ errors.push('Contractual SOW cannot contain synthetic_evidence');
150
+ }
151
+ if (data.traceability_matrix) {
152
+ for (const row of data.traceability_matrix) {
153
+ if (row.owner_source === 'built_in_template_default') {
154
+ errors.push(`Contractual SOW requirement ${row.sow_requirement_id} relies on unverified built_in_template_default owner`);
155
+ }
156
+ }
157
+ }
158
+ }
159
+
160
+ // Injection scan on user fields
161
+ const injections = scanForInjections({
162
+ client: data.client,
163
+ title: data.title,
164
+ traceability_matrix: data.traceability_matrix,
165
+ refusal_log: data.admissibility_gate?.refusal_log,
166
+ });
167
+ errors.push(...injections);
168
+
169
+ } else if (artifactType === 'SEARCH_REPORT') {
170
+ const val = validateAgainst('search-report.schema.json', data);
171
+ if (!val.valid) {
172
+ errors.push(...val.errors.map((e) => `Search report schema violation: ${e}`));
173
+ }
174
+
175
+ if (data.pillars && Object.keys(data.pillars).length < 19) {
176
+ errors.push(`Search report must contain all 19 enterprise pillars, found ${Object.keys(data.pillars).length}`);
177
+ }
178
+
179
+ // Scan for dangerous injection in titles, client, target
180
+ const injections = scanForInjections({
181
+ client_name: data.client_name,
182
+ target_domain: data.target_domain,
183
+ decision_summary: data.decision_summary,
184
+ });
185
+ errors.push(...injections);
186
+
187
+ } else if (artifactType === 'CRO_REPORT') {
188
+ const val = validateAgainst('cro-report.schema.json', data);
189
+ if (!val.valid) {
190
+ errors.push(...val.errors.map((e) => `CRO report schema violation: ${e}`));
191
+ }
192
+
193
+ if (data.pillars && Object.keys(data.pillars).length < 25) {
194
+ errors.push(`CRO report must contain all 25 enterprise pillars, found ${Object.keys(data.pillars).length}`);
195
+ }
196
+
197
+ // Scan for injection
198
+ const injections = scanForInjections({
199
+ client_name: data.client_name,
200
+ target_domain: data.target_domain,
201
+ decision_summary: data.decision_summary,
202
+ });
203
+ errors.push(...injections);
204
+
205
+ } else {
206
+ errors.push('Unrecognized customer artifact structure');
207
+ }
208
+
209
+ } else if (format === 'html' || format === 'markdown') {
210
+ // HTML or Markdown scan
211
+ const content = rawContent || '';
212
+
213
+ // Check for malicious unescaped scripts
214
+ for (const pattern of DANGEROUS_PATTERNS) {
215
+ if (pattern.test(content)) {
216
+ errors.push(`Artifact contains forbidden unescaped script or event handler: ${pattern}`);
217
+ break;
218
+ }
219
+ }
220
+
221
+ // Check governance disclaimers
222
+ if (content.includes('Statement of Work') || content.includes('SOW-')) {
223
+ artifactType = 'SOW';
224
+ if (!content.includes('guaranteed') && !content.includes('traceability')) {
225
+ warnings.push('SOW document lacks standard non-guarantee governance disclosure');
226
+ }
227
+ } else if (content.includes('Conversion Rate Optimization') || content.includes('CRO')) {
228
+ artifactType = 'CRO_REPORT';
229
+ if (!content.includes('guaranteed') && !content.includes('Scientific Epistemology')) {
230
+ warnings.push('CRO report lacks standard scientific epistemology disclaimer');
231
+ }
232
+ } else if (content.includes('Search Intelligence') || content.includes('SEO')) {
233
+ artifactType = 'SEARCH_REPORT';
234
+ if (!content.includes('guaranteed') && !content.includes('Governance Notice')) {
235
+ warnings.push('Search report lacks standard non-guarantee disclosure');
236
+ }
237
+ }
238
+ }
239
+
240
+ const valid = errors.length === 0;
241
+ if (!valid && failClosed) {
242
+ throw new CustomerArtifactVerificationError(`Artifact verification failed: ${errors.join('; ')}`, errors);
243
+ }
244
+
245
+ return {
246
+ valid,
247
+ artifact_type: artifactType,
248
+ format,
249
+ generation_mode: data?.generation_mode || data?.generation_provenance?.generation_mode || 'UNKNOWN',
250
+ errors,
251
+ warnings,
252
+ provenance: data?.generation_provenance || null,
253
+ summary: valid
254
+ ? `Artifact verified successfully (${artifactType} - ${format})`
255
+ : `Artifact verification failed (${artifactType} - ${format}): ${errors.length} error(s)`,
256
+ };
257
+ }
258
+
259
+ /**
260
+ * CLI command entry point
261
+ */
262
+ export async function verifyCustomerArtifactCommand(filePath, args = [], root = process.cwd()) {
263
+ const fullPath = path.isAbsolute(filePath) ? filePath : path.resolve(root, filePath);
264
+ if (!fs.existsSync(fullPath)) {
265
+ return {
266
+ command: 'artifacts verify-customer',
267
+ file: filePath,
268
+ valid: false,
269
+ errors: [`File not found: ${fullPath}`],
270
+ message: `Artifact verification error: file not found: ${fullPath}`,
271
+ };
272
+ }
273
+
274
+ const result = await verifyCustomerArtifact(fullPath);
275
+ return {
276
+ command: 'artifacts verify-customer',
277
+ file: filePath,
278
+ ...result,
279
+ message: result.valid
280
+ ? `SUCCESS: Artifact ${filePath} verified successfully (${result.artifact_type}, format: ${result.format}).`
281
+ : `FAILED: Artifact ${filePath} failed verification:\n${result.errors.map((e) => ` - ${e}`).join('\n')}`,
282
+ };
283
+ }
package/src/cli/index.js CHANGED
@@ -9,6 +9,7 @@ import { auditBacklinks, formatBacklinksOutput } from "../commands/auditBacklink
9
9
  import { roadmapCommand } from "../commands/roadmapCmd.js";
10
10
  import { sowCommand } from "../commands/sowCmd.js";
11
11
  import { buildIceMatrix, formatIceMatrixOutput } from "../analysis/iceMatrix.js";
12
+ import { verifyCustomerArtifactCommand } from "../artifacts/verifyCustomerArtifact.js";
12
13
  import fs from 'node:fs';
13
14
  import path from 'node:path';
14
15
  import { pathToFileURL } from 'node:url';
@@ -1117,7 +1118,13 @@ ${r.per_detector.filter((d) => d.true_positives + d.false_negatives + d.false_po
1117
1118
  } else if (mode === 'import') {
1118
1119
  const r = importArtifactPackage(root, { input: args.input });
1119
1120
  out(args, `artifacts import ${r.run_id}: ${r.status}\nRun package: ${r.destination}`, r);
1120
- } else throw new Error('usage: citable artifacts <export <run-id> --output <directory>|verify --input <directory>|import --input <directory>>');
1121
+ } else if (mode === 'verify-customer') {
1122
+ const filePath = args._[1];
1123
+ if (!filePath) throw new Error('usage: citable artifacts verify-customer <file>');
1124
+ const r = await verifyCustomerArtifactCommand(filePath, argv.slice(3), root);
1125
+ out(args, r.message, r);
1126
+ if (!r.valid) process.exitCode = 1;
1127
+ } else throw new Error('usage: citable artifacts <export <run-id> --output <directory>|verify --input <directory>|import --input <directory>|verify-customer <file>>');
1121
1128
  break;
1122
1129
  }
1123
1130
  case 'verify': {