@bilig/create-workpaper 0.17.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.
- package/README.md +20 -0
- package/bin/create-bilig-workpaper.js +100 -0
- package/package.json +45 -0
- package/template/README.md +27 -0
- package/template/package.json +18 -0
- package/template/src/index.ts +321 -0
- package/template/tsconfig.json +12 -0
package/README.md
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# @bilig/create-workpaper
|
|
2
|
+
|
|
3
|
+
Create a runnable Bilig WorkPaper starter for Node services and agent tools.
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
npm create @bilig/workpaper@latest pricing-workpaper
|
|
7
|
+
cd pricing-workpaper
|
|
8
|
+
npm install
|
|
9
|
+
npm run smoke
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
The generated starter builds a quote-approval workbook, writes inputs through an
|
|
13
|
+
API-style handler, recalculates formulas, persists JSON, restores the workbook,
|
|
14
|
+
and prints `verified: true`.
|
|
15
|
+
|
|
16
|
+
After the smoke proof passes, it also prints a star/bookmark link for the GitHub
|
|
17
|
+
repo: <https://github.com/proompteng/bilig/stargazers>.
|
|
18
|
+
|
|
19
|
+
Use this when you want to evaluate `@bilig/headless` from a blank directory
|
|
20
|
+
without cloning the full monorepo.
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { cp, mkdir, readdir, readFile, stat, writeFile } from 'node:fs/promises'
|
|
3
|
+
import { dirname, join, relative, resolve } from 'node:path'
|
|
4
|
+
import { fileURLToPath } from 'node:url'
|
|
5
|
+
|
|
6
|
+
const packageRoot = dirname(dirname(fileURLToPath(import.meta.url)))
|
|
7
|
+
const templateRoot = join(packageRoot, 'template')
|
|
8
|
+
|
|
9
|
+
const args = process.argv.slice(2)
|
|
10
|
+
const wantsHelp = args.includes('--help') || args.includes('-h')
|
|
11
|
+
const force = args.includes('--force')
|
|
12
|
+
const targetArg = args.find((arg) => !arg.startsWith('-'))
|
|
13
|
+
|
|
14
|
+
if (wantsHelp) {
|
|
15
|
+
printHelp()
|
|
16
|
+
process.exit(0)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const targetDirectory = resolve(process.cwd(), targetArg ?? 'bilig-workpaper-starter')
|
|
20
|
+
const projectName = normalizePackageName(targetArg ?? 'bilig-workpaper-starter')
|
|
21
|
+
|
|
22
|
+
await ensureWritableTarget(targetDirectory, force)
|
|
23
|
+
await copyTemplate(targetDirectory, projectName)
|
|
24
|
+
|
|
25
|
+
console.log(`Created ${relative(process.cwd(), targetDirectory) || '.'}`)
|
|
26
|
+
console.log('')
|
|
27
|
+
console.log('Next:')
|
|
28
|
+
console.log(` cd ${relative(process.cwd(), targetDirectory) || '.'}`)
|
|
29
|
+
console.log(' npm install')
|
|
30
|
+
console.log(' npm run smoke')
|
|
31
|
+
console.log('')
|
|
32
|
+
console.log('Expected smoke output includes:')
|
|
33
|
+
console.log(' "verified": true')
|
|
34
|
+
|
|
35
|
+
function printHelp() {
|
|
36
|
+
console.log(`@bilig/create-workpaper
|
|
37
|
+
|
|
38
|
+
Usage:
|
|
39
|
+
npm create @bilig/workpaper@latest <directory>
|
|
40
|
+
npm exec @bilig/create-workpaper@latest <directory>
|
|
41
|
+
|
|
42
|
+
Options:
|
|
43
|
+
--force Allow writing into an existing directory.
|
|
44
|
+
-h, --help
|
|
45
|
+
`)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function ensureWritableTarget(directory, allowExisting) {
|
|
49
|
+
try {
|
|
50
|
+
const existing = await stat(directory)
|
|
51
|
+
if (!existing.isDirectory()) {
|
|
52
|
+
throw new Error(`${directory} exists and is not a directory`)
|
|
53
|
+
}
|
|
54
|
+
const entries = await readdir(directory)
|
|
55
|
+
if (entries.length > 0 && !allowExisting) {
|
|
56
|
+
throw new Error(`${directory} is not empty. Re-run with --force to write into it.`)
|
|
57
|
+
}
|
|
58
|
+
} catch (error) {
|
|
59
|
+
if (error?.code === 'ENOENT') {
|
|
60
|
+
await mkdir(directory, { recursive: true })
|
|
61
|
+
return
|
|
62
|
+
}
|
|
63
|
+
throw error
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function copyTemplate(outputDirectory, packageName) {
|
|
68
|
+
await cp(templateRoot, outputDirectory, {
|
|
69
|
+
recursive: true,
|
|
70
|
+
filter: async (source) => {
|
|
71
|
+
const sourceStat = await stat(source)
|
|
72
|
+
if (sourceStat.isDirectory()) {
|
|
73
|
+
return true
|
|
74
|
+
}
|
|
75
|
+
const relativePath = relative(templateRoot, source)
|
|
76
|
+
const targetPath = join(outputDirectory, relativePath)
|
|
77
|
+
const text = await readFile(source, 'utf8')
|
|
78
|
+
await mkdir(dirname(targetPath), { recursive: true })
|
|
79
|
+
await writeFile(targetPath, text.replaceAll('__PROJECT_NAME__', packageName))
|
|
80
|
+
return false
|
|
81
|
+
},
|
|
82
|
+
})
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function normalizePackageName(name) {
|
|
86
|
+
const parts = name.split(/[\\/]/)
|
|
87
|
+
let base = 'bilig-workpaper-starter'
|
|
88
|
+
for (let index = parts.length - 1; index >= 0; index -= 1) {
|
|
89
|
+
if (parts[index] !== '') {
|
|
90
|
+
base = parts[index]
|
|
91
|
+
break
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
const normalized = base
|
|
95
|
+
.toLowerCase()
|
|
96
|
+
.replace(/[^a-z0-9._-]+/g, '-')
|
|
97
|
+
.replace(/^-+|-+$/g, '')
|
|
98
|
+
|
|
99
|
+
return normalized || 'bilig-workpaper-starter'
|
|
100
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@bilig/create-workpaper",
|
|
3
|
+
"version": "0.17.0",
|
|
4
|
+
"description": "Create a runnable Bilig WorkPaper starter for Node services and agent tools.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"bilig",
|
|
7
|
+
"create",
|
|
8
|
+
"formula-engine",
|
|
9
|
+
"headless-spreadsheet",
|
|
10
|
+
"nodejs",
|
|
11
|
+
"spreadsheet-formulas",
|
|
12
|
+
"typescript",
|
|
13
|
+
"workbook-api",
|
|
14
|
+
"workpaper",
|
|
15
|
+
"xlsx-recalculation"
|
|
16
|
+
],
|
|
17
|
+
"homepage": "https://proompteng.github.io/bilig/",
|
|
18
|
+
"bugs": {
|
|
19
|
+
"url": "https://github.com/proompteng/bilig/issues"
|
|
20
|
+
},
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"repository": {
|
|
23
|
+
"type": "git",
|
|
24
|
+
"url": "git+https://github.com/proompteng/bilig.git",
|
|
25
|
+
"directory": "packages/create-workpaper"
|
|
26
|
+
},
|
|
27
|
+
"bin": {
|
|
28
|
+
"create-workpaper": "bin/create-bilig-workpaper.js"
|
|
29
|
+
},
|
|
30
|
+
"files": [
|
|
31
|
+
"bin",
|
|
32
|
+
"template",
|
|
33
|
+
"README.md"
|
|
34
|
+
],
|
|
35
|
+
"type": "module",
|
|
36
|
+
"publishConfig": {
|
|
37
|
+
"access": "public"
|
|
38
|
+
},
|
|
39
|
+
"scripts": {
|
|
40
|
+
"smoke": "node ./bin/create-bilig-workpaper.js --help"
|
|
41
|
+
},
|
|
42
|
+
"engines": {
|
|
43
|
+
"node": ">=22.0.0"
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# __PROJECT_NAME__
|
|
2
|
+
|
|
3
|
+
Formula-backed quote approval API built with `@bilig/headless`.
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
npm install
|
|
7
|
+
npm run smoke
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
The smoke run writes quote inputs, recalculates workbook formulas, persists the
|
|
11
|
+
WorkPaper as JSON, restores it, and checks that the restored formula output
|
|
12
|
+
matches the live output.
|
|
13
|
+
|
|
14
|
+
Expected output includes `verified: true`. After that verification passes, the
|
|
15
|
+
starter prints a repo star/bookmark link so Bilig is easier to find later.
|
|
16
|
+
|
|
17
|
+
Run a local API:
|
|
18
|
+
|
|
19
|
+
```sh
|
|
20
|
+
npm run dev
|
|
21
|
+
curl http://localhost:8788/api/quote/approval
|
|
22
|
+
curl -X POST http://localhost:8788/api/quote/approval \
|
|
23
|
+
-H 'content-type: application/json' \
|
|
24
|
+
-d '{"units":40,"listPrice":1200,"discount":0.05,"unitCost":760,"minimumMargin":0.3}'
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Learn more: <https://github.com/proompteng/bilig>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "__PROJECT_NAME__",
|
|
3
|
+
"private": true,
|
|
4
|
+
"type": "module",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"dev": "tsx src/index.ts --serve",
|
|
7
|
+
"smoke": "tsx src/index.ts",
|
|
8
|
+
"typecheck": "tsc --noEmit"
|
|
9
|
+
},
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"@bilig/headless": "latest"
|
|
12
|
+
},
|
|
13
|
+
"devDependencies": {
|
|
14
|
+
"@types/node": "latest",
|
|
15
|
+
"tsx": "latest",
|
|
16
|
+
"typescript": "latest"
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
import { createServer, type IncomingMessage } from 'node:http'
|
|
2
|
+
import { pathToFileURL } from 'node:url'
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
WorkPaper,
|
|
6
|
+
createWorkPaperFromDocument,
|
|
7
|
+
exportWorkPaperDocument,
|
|
8
|
+
parseWorkPaperDocument,
|
|
9
|
+
serializeWorkPaperDocument,
|
|
10
|
+
} from '@bilig/headless'
|
|
11
|
+
|
|
12
|
+
type WorkPaperInstance = ReturnType<typeof WorkPaper.buildFromSheets>
|
|
13
|
+
|
|
14
|
+
type QuoteInput = {
|
|
15
|
+
units: number
|
|
16
|
+
listPrice: number
|
|
17
|
+
discount: number
|
|
18
|
+
unitCost: number
|
|
19
|
+
minimumMargin: number
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
type QuoteSummary = {
|
|
23
|
+
listRevenue: number
|
|
24
|
+
discountAmount: number
|
|
25
|
+
netRevenue: number
|
|
26
|
+
totalCost: number
|
|
27
|
+
grossMargin: number
|
|
28
|
+
decision: string
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
type WorkbookStorage = {
|
|
32
|
+
loadWorkbookJson(): Promise<string> | string
|
|
33
|
+
saveWorkbookJson(nextWorkbookJson: string): Promise<void> | void
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const inputCells = {
|
|
37
|
+
units: 'Inputs!B2',
|
|
38
|
+
listPrice: 'Inputs!B3',
|
|
39
|
+
discount: 'Inputs!B4',
|
|
40
|
+
unitCost: 'Inputs!B5',
|
|
41
|
+
minimumMargin: 'Inputs!B6',
|
|
42
|
+
} as const
|
|
43
|
+
|
|
44
|
+
export function createQuoteApprovalRequestHandler(storage: WorkbookStorage) {
|
|
45
|
+
return async function handleQuoteApprovalRequest(request: Request): Promise<Response> {
|
|
46
|
+
const url = new URL(request.url)
|
|
47
|
+
|
|
48
|
+
if (request.method === 'GET' && url.pathname === '/api/quote/approval') {
|
|
49
|
+
const workbook = await loadWorkbook(storage)
|
|
50
|
+
return json({ summary: readQuoteSummary(workbook), inputCells })
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (request.method === 'POST' && url.pathname === '/api/quote/approval') {
|
|
54
|
+
try {
|
|
55
|
+
const input = parseQuoteInput(await request.json())
|
|
56
|
+
const workbook = await loadWorkbook(storage)
|
|
57
|
+
const before = readQuoteSummary(workbook)
|
|
58
|
+
writeQuoteInputs(workbook, input)
|
|
59
|
+
const after = readQuoteSummary(workbook)
|
|
60
|
+
const workbookJson = serializeWorkbook(workbook)
|
|
61
|
+
await storage.saveWorkbookJson(workbookJson)
|
|
62
|
+
|
|
63
|
+
const restored = createWorkPaperFromDocument(parseWorkPaperDocument(workbookJson))
|
|
64
|
+
const restoredSummary = readQuoteSummary(restored)
|
|
65
|
+
|
|
66
|
+
return json({
|
|
67
|
+
input,
|
|
68
|
+
inputCells,
|
|
69
|
+
before,
|
|
70
|
+
after,
|
|
71
|
+
restored: restoredSummary,
|
|
72
|
+
checks: {
|
|
73
|
+
decisionChanged: before.decision !== after.decision,
|
|
74
|
+
formulasPersisted: workbookJson.includes('=IF(B6>=Inputs!B6'),
|
|
75
|
+
restoredMatchesAfter: JSON.stringify(restoredSummary) === JSON.stringify(after),
|
|
76
|
+
serializedBytes: Buffer.byteLength(workbookJson, 'utf8'),
|
|
77
|
+
},
|
|
78
|
+
})
|
|
79
|
+
} catch (error) {
|
|
80
|
+
return json({ error: error instanceof Error ? error.message : String(error) }, 400)
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return json({ error: 'not found' }, 404)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function createQuoteApprovalWorkbook(): WorkPaperInstance {
|
|
89
|
+
return WorkPaper.buildFromSheets({
|
|
90
|
+
Inputs: [
|
|
91
|
+
['Metric', 'Value'],
|
|
92
|
+
['Units', 40],
|
|
93
|
+
['List price', 1200],
|
|
94
|
+
['Discount', 0.1],
|
|
95
|
+
['Unit cost', 760],
|
|
96
|
+
['Minimum margin', 0.3],
|
|
97
|
+
],
|
|
98
|
+
Summary: [
|
|
99
|
+
['Metric', 'Value'],
|
|
100
|
+
['List revenue', '=Inputs!B2*Inputs!B3'],
|
|
101
|
+
['Discount amount', '=B2*Inputs!B4'],
|
|
102
|
+
['Net revenue', '=B2-B3'],
|
|
103
|
+
['Total cost', '=Inputs!B2*Inputs!B5'],
|
|
104
|
+
['Gross margin', '=(B4-B5)/B4'],
|
|
105
|
+
['Decision', '=IF(B6>=Inputs!B6,"approved","review")'],
|
|
106
|
+
],
|
|
107
|
+
})
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function createMemoryStorage(): WorkbookStorage {
|
|
111
|
+
let workbookJson = serializeWorkbook(createQuoteApprovalWorkbook())
|
|
112
|
+
return {
|
|
113
|
+
loadWorkbookJson() {
|
|
114
|
+
return workbookJson
|
|
115
|
+
},
|
|
116
|
+
saveWorkbookJson(nextWorkbookJson) {
|
|
117
|
+
workbookJson = nextWorkbookJson
|
|
118
|
+
},
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function writeQuoteInputs(workbook: WorkPaperInstance, input: QuoteInput): void {
|
|
123
|
+
const sheet = requireSheet(workbook, 'Inputs')
|
|
124
|
+
workbook.setCellContents({ sheet, row: 1, col: 1 }, input.units)
|
|
125
|
+
workbook.setCellContents({ sheet, row: 2, col: 1 }, input.listPrice)
|
|
126
|
+
workbook.setCellContents({ sheet, row: 3, col: 1 }, input.discount)
|
|
127
|
+
workbook.setCellContents({ sheet, row: 4, col: 1 }, input.unitCost)
|
|
128
|
+
workbook.setCellContents({ sheet, row: 5, col: 1 }, input.minimumMargin)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function loadWorkbook(storage: WorkbookStorage): Promise<WorkPaperInstance> {
|
|
132
|
+
return createWorkPaperFromDocument(parseWorkPaperDocument(await storage.loadWorkbookJson()))
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function serializeWorkbook(workbook: WorkPaperInstance): string {
|
|
136
|
+
return serializeWorkPaperDocument(exportWorkPaperDocument(workbook, { includeConfig: true }))
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function readQuoteSummary(workbook: WorkPaperInstance): QuoteSummary {
|
|
140
|
+
const sheet = requireSheet(workbook, 'Summary')
|
|
141
|
+
return {
|
|
142
|
+
listRevenue: readNumber(workbook, sheet, 1, 1, 'List revenue'),
|
|
143
|
+
discountAmount: readNumber(workbook, sheet, 2, 1, 'Discount amount'),
|
|
144
|
+
netRevenue: readNumber(workbook, sheet, 3, 1, 'Net revenue'),
|
|
145
|
+
totalCost: readNumber(workbook, sheet, 4, 1, 'Total cost'),
|
|
146
|
+
grossMargin: readRoundedNumber(workbook, sheet, 5, 1, 'Gross margin'),
|
|
147
|
+
decision: readString(workbook, sheet, 6, 1, 'Decision'),
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function parseQuoteInput(value: unknown): QuoteInput {
|
|
152
|
+
const record = readRecord(value, 'request body')
|
|
153
|
+
return {
|
|
154
|
+
units: readBoundedNumber(record.units, 'units', 1),
|
|
155
|
+
listPrice: readBoundedNumber(record.listPrice, 'listPrice', 0),
|
|
156
|
+
discount: readBoundedNumber(record.discount, 'discount', 0, 0.95),
|
|
157
|
+
unitCost: readBoundedNumber(record.unitCost, 'unitCost', 0),
|
|
158
|
+
minimumMargin: readBoundedNumber(record.minimumMargin, 'minimumMargin', 0, 1),
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function requireSheet(workbook: WorkPaperInstance, name: string): number {
|
|
163
|
+
const sheet = workbook.getSheetId(name)
|
|
164
|
+
if (sheet === undefined) {
|
|
165
|
+
throw new Error(`missing sheet: ${name}`)
|
|
166
|
+
}
|
|
167
|
+
return sheet
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function readNumber(workbook: WorkPaperInstance, sheet: number, row: number, col: number, label: string): number {
|
|
171
|
+
return Math.round(readCellNumber(workbook, sheet, row, col, label) * 100) / 100
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function readRoundedNumber(workbook: WorkPaperInstance, sheet: number, row: number, col: number, label: string): number {
|
|
175
|
+
return Math.round(readCellNumber(workbook, sheet, row, col, label) * 10_000) / 10_000
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function readCellNumber(workbook: WorkPaperInstance, sheet: number, row: number, col: number, label: string): number {
|
|
179
|
+
const cell: unknown = workbook.getCellValue({ sheet, row, col })
|
|
180
|
+
if (!isRecord(cell) || typeof cell.value !== 'number') {
|
|
181
|
+
throw new Error(`expected ${label} to be numeric, received ${JSON.stringify(cell)}`)
|
|
182
|
+
}
|
|
183
|
+
return cell.value
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function readString(workbook: WorkPaperInstance, sheet: number, row: number, col: number, label: string): string {
|
|
187
|
+
const cell: unknown = workbook.getCellValue({ sheet, row, col })
|
|
188
|
+
if (!isRecord(cell) || typeof cell.value !== 'string') {
|
|
189
|
+
throw new Error(`expected ${label} to be text, received ${JSON.stringify(cell)}`)
|
|
190
|
+
}
|
|
191
|
+
return cell.value
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function readBoundedNumber(value: unknown, label: string, min: number, max = Number.POSITIVE_INFINITY): number {
|
|
195
|
+
const numberValue = Number(value)
|
|
196
|
+
if (!Number.isFinite(numberValue) || numberValue < min || numberValue > max) {
|
|
197
|
+
throw new Error(`${label} must be a finite number between ${min.toString()} and ${max.toString()}`)
|
|
198
|
+
}
|
|
199
|
+
return numberValue
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function readRecord(value: unknown, label: string): Record<string, unknown> {
|
|
203
|
+
if (!isRecord(value)) {
|
|
204
|
+
throw new Error(`${label} must be a JSON object`)
|
|
205
|
+
}
|
|
206
|
+
return value
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
210
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function json(payload: unknown, status = 200): Response {
|
|
214
|
+
return Response.json(payload, { status, headers: { 'cache-control': 'no-store' } })
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
async function runSmoke(): Promise<void> {
|
|
218
|
+
const handler = createQuoteApprovalRequestHandler(createMemoryStorage())
|
|
219
|
+
const before = await requestJson(handler, '/api/quote/approval')
|
|
220
|
+
const edit = await requestJson(handler, '/api/quote/approval', {
|
|
221
|
+
method: 'POST',
|
|
222
|
+
headers: { 'content-type': 'application/json' },
|
|
223
|
+
body: JSON.stringify({
|
|
224
|
+
units: 40,
|
|
225
|
+
listPrice: 1200,
|
|
226
|
+
discount: 0.05,
|
|
227
|
+
unitCost: 760,
|
|
228
|
+
minimumMargin: 0.3,
|
|
229
|
+
}),
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
const output = {
|
|
233
|
+
before,
|
|
234
|
+
edit,
|
|
235
|
+
verified: true,
|
|
236
|
+
nextStep:
|
|
237
|
+
'If this proof matches your service or agent workflow, star or bookmark Bilig: https://github.com/proompteng/bilig/stargazers',
|
|
238
|
+
}
|
|
239
|
+
assertSmokeOutput(output)
|
|
240
|
+
console.log(JSON.stringify(output, null, 2))
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
async function requestJson(handler: (request: Request) => Promise<Response>, path: string, init?: RequestInit): Promise<unknown> {
|
|
244
|
+
const response = await handler(new Request(`http://localhost:8788${path}`, init))
|
|
245
|
+
const body: unknown = await response.json()
|
|
246
|
+
if (!response.ok) {
|
|
247
|
+
throw new Error(`request failed: ${response.status.toString()} ${JSON.stringify(body)}`)
|
|
248
|
+
}
|
|
249
|
+
return body
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function assertSmokeOutput(value: unknown): void {
|
|
253
|
+
const output = readRecord(value, 'smoke output')
|
|
254
|
+
const edit = readRecord(output.edit, 'smoke edit')
|
|
255
|
+
const checks = readRecord(edit.checks, 'smoke checks')
|
|
256
|
+
const after = readRecord(edit.after, 'smoke after')
|
|
257
|
+
const restored = readRecord(edit.restored, 'smoke restored')
|
|
258
|
+
|
|
259
|
+
if (
|
|
260
|
+
output.verified !== true ||
|
|
261
|
+
after.decision !== 'approved' ||
|
|
262
|
+
JSON.stringify(after) !== JSON.stringify(restored) ||
|
|
263
|
+
checks.decisionChanged !== true ||
|
|
264
|
+
checks.formulasPersisted !== true ||
|
|
265
|
+
checks.restoredMatchesAfter !== true ||
|
|
266
|
+
Number(checks.serializedBytes) <= 0
|
|
267
|
+
) {
|
|
268
|
+
throw new Error(`unexpected smoke output: ${JSON.stringify(value)}`)
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
async function toWebRequest(incoming: IncomingMessage): Promise<Request> {
|
|
273
|
+
const origin = `http://${incoming.headers.host ?? 'localhost:8788'}`
|
|
274
|
+
const headers = new Headers()
|
|
275
|
+
|
|
276
|
+
for (const [name, value] of Object.entries(incoming.headers)) {
|
|
277
|
+
if (Array.isArray(value)) {
|
|
278
|
+
headers.set(name, value.join(', '))
|
|
279
|
+
} else if (value !== undefined) {
|
|
280
|
+
headers.set(name, value)
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return new Request(new URL(incoming.url ?? '/', origin), {
|
|
285
|
+
method: incoming.method,
|
|
286
|
+
headers,
|
|
287
|
+
body: incoming.method === 'GET' || incoming.method === 'HEAD' ? undefined : await readIncomingBody(incoming),
|
|
288
|
+
duplex: 'half',
|
|
289
|
+
} as RequestInit & { duplex: 'half' })
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function readIncomingBody(incoming: IncomingMessage): Promise<string> {
|
|
293
|
+
return new Promise((resolve, reject) => {
|
|
294
|
+
const chunks: Uint8Array[] = []
|
|
295
|
+
incoming.on('data', (chunk: Buffer | string) => {
|
|
296
|
+
chunks.push(typeof chunk === 'string' ? Buffer.from(chunk) : chunk)
|
|
297
|
+
})
|
|
298
|
+
incoming.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')))
|
|
299
|
+
incoming.on('error', reject)
|
|
300
|
+
})
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (process.argv[1] !== undefined && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
304
|
+
if (process.argv.includes('--serve')) {
|
|
305
|
+
const handler = createQuoteApprovalRequestHandler(createMemoryStorage())
|
|
306
|
+
createServer(async (incoming, outgoing) => {
|
|
307
|
+
try {
|
|
308
|
+
const response = await handler(await toWebRequest(incoming))
|
|
309
|
+
outgoing.writeHead(response.status, Object.fromEntries(response.headers))
|
|
310
|
+
outgoing.end(Buffer.from(await response.arrayBuffer()))
|
|
311
|
+
} catch (error) {
|
|
312
|
+
outgoing.writeHead(500, { 'content-type': 'application/json; charset=utf-8' })
|
|
313
|
+
outgoing.end(`${JSON.stringify({ error: error instanceof Error ? error.message : String(error) })}\n`)
|
|
314
|
+
}
|
|
315
|
+
}).listen(8788, () => {
|
|
316
|
+
console.log('Quote approval WorkPaper API listening on http://localhost:8788')
|
|
317
|
+
})
|
|
318
|
+
} else {
|
|
319
|
+
await runSmoke()
|
|
320
|
+
}
|
|
321
|
+
}
|