create-cs-object 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +47 -2
- package/dist/cli.js +29 -2
- package/package.json +5 -2
- package/template/.gitattributes +2 -0
- package/template/AGENTS.md +11 -2
- package/template/README.md +60 -2
- package/template/authoring.md +247 -18
- package/template/components.json +25 -0
- package/template/gitignore +1 -0
- package/template/index.html +12 -0
- package/template/references/README.md +134 -0
- package/template/reports.json +8 -0
- package/template/scripts/dev.ts +104 -33
- package/template/scripts/reports.ts +24 -0
- package/template/src/App.tsx +681 -0
- package/template/src/components/theme-provider.tsx +230 -0
- package/template/src/components/ui/badge.tsx +51 -0
- package/template/src/components/ui/button.tsx +55 -0
- package/template/src/components/ui/card.tsx +102 -0
- package/template/src/components/ui/dropdown-menu.tsx +274 -0
- package/template/src/components/ui/input.tsx +19 -0
- package/template/src/components/ui/label.tsx +17 -0
- package/template/src/components/ui/separator.tsx +22 -0
- package/template/src/components/ui/sheet.tsx +137 -0
- package/template/src/hooks/use-mobile.ts +19 -0
- package/template/src/index.css +131 -0
- package/template/src/lib/utils.ts +1 -0
- package/template/src/main.tsx +9 -0
- package/template/tsconfig.app.json +29 -0
- package/template/tsconfig.json +12 -0
- package/template/tsconfig.node.json +24 -0
- package/template/vite.config.ts +144 -0
|
@@ -3,3 +3,137 @@
|
|
|
3
3
|
Keep source documents or links here. Record the relevant page or clause, edition,
|
|
4
4
|
and the assumptions each source supports. The starter uses the elementary
|
|
5
5
|
rectangle area relation and has no external engineering reference case.
|
|
6
|
+
|
|
7
|
+
## Bind an independent case
|
|
8
|
+
|
|
9
|
+
A CLI reference file contains `referenceVersion: "1"` and a `cases` array.
|
|
10
|
+
Each case records an ID, revision, independent basis, source/input binding,
|
|
11
|
+
and expected values for every calculated symbol, including intermediates.
|
|
12
|
+
Each expected entry needs the captured symbol ID, independently established
|
|
13
|
+
value and matching unit. A comparison of only public outputs is a separate
|
|
14
|
+
numerical check; it does not provide complete CLI reference coverage.
|
|
15
|
+
|
|
16
|
+
This recipe is for the unchanged rectangle starter. Establish its expected
|
|
17
|
+
area independently first: a 2 m by 3 m rectangle has area 6 m². The verification
|
|
18
|
+
report supplies only source hashes, function and input metadata for binding;
|
|
19
|
+
the expected value below is the hand-derived 6, not a captured result.
|
|
20
|
+
|
|
21
|
+
From the project root in a POSIX shell:
|
|
22
|
+
|
|
23
|
+
```sh
|
|
24
|
+
PYTHON="$PWD/.venv/bin/python" npx --no-install cso verify calculations/report.cso.py \
|
|
25
|
+
--function calculate --input width=2 --input height=3 --format json \
|
|
26
|
+
> references/rectangle-check.json
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Create `references/rectangle-reference.json` with the full case shape:
|
|
30
|
+
|
|
31
|
+
```sh
|
|
32
|
+
./.venv/bin/python - <<'PY'
|
|
33
|
+
import json
|
|
34
|
+
from pathlib import Path
|
|
35
|
+
|
|
36
|
+
report = json.loads(Path("references/rectangle-check.json").read_text(encoding="utf-8"))
|
|
37
|
+
if not report["ok"]:
|
|
38
|
+
raise SystemExit("Resolve verification diagnostics before binding a case")
|
|
39
|
+
fields = (
|
|
40
|
+
"entryModuleId", "entrySourceHash", "sourceClosureHash", "function",
|
|
41
|
+
"resolvedInputs", "resolvedInputKinds",
|
|
42
|
+
)
|
|
43
|
+
reference = {
|
|
44
|
+
"referenceVersion": "1",
|
|
45
|
+
"cases": [{
|
|
46
|
+
"id": "rectangle-2-by-3",
|
|
47
|
+
"revision": "1",
|
|
48
|
+
"basis": {
|
|
49
|
+
"method": "Hand-derived rectangle area",
|
|
50
|
+
"derivation": "Perpendicular sides: 2 m times 3 m equals 6 m^2.",
|
|
51
|
+
"sourceDescription": "Elementary geometry for the stated rectangle.",
|
|
52
|
+
},
|
|
53
|
+
"binding": {name: report["provenance"][name] for name in fields},
|
|
54
|
+
"expected": [{
|
|
55
|
+
"symbolId": '["symbol","root","area"]',
|
|
56
|
+
"value": 6,
|
|
57
|
+
"unit": "m^2",
|
|
58
|
+
}],
|
|
59
|
+
}],
|
|
60
|
+
}
|
|
61
|
+
Path("references/rectangle-reference.json").write_text(
|
|
62
|
+
json.dumps(reference, indent=2) + "\n", encoding="utf-8", newline="\n"
|
|
63
|
+
)
|
|
64
|
+
PY
|
|
65
|
+
PYTHON="$PWD/.venv/bin/python" npx --no-install cso verify calculations/report.cso.py \
|
|
66
|
+
--function calculate --input width=2 --input height=3 \
|
|
67
|
+
--reference references/rectangle-reference.json --format json
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
In PowerShell 5.1 or 7, run the following block instead. It captures only the
|
|
71
|
+
verification metadata needed to bind the independently derived value `6`.
|
|
72
|
+
The JSON stays inside PowerShell until the file write, so native argument
|
|
73
|
+
quoting cannot alter it. `Join-Path $PWD` gives the .NET writer an absolute path.
|
|
74
|
+
|
|
75
|
+
<!-- docs:reference-authoring:start -->
|
|
76
|
+
```powershell
|
|
77
|
+
$env:PYTHON = Join-Path $PWD '.venv\Scripts\python.exe'
|
|
78
|
+
$cso = Join-Path $PWD 'node_modules\.bin\cso.cmd'
|
|
79
|
+
$source = Join-Path $PWD 'calculations\report.cso.py'
|
|
80
|
+
$utf8 = [System.Text.UTF8Encoding]::new($false)
|
|
81
|
+
[Console]::OutputEncoding = $utf8
|
|
82
|
+
$reportText = & $cso verify $source --function calculate --input width=2 --input height=3 --format json
|
|
83
|
+
if ($LASTEXITCODE -ne 0) { throw 'Calculation verification failed.' }
|
|
84
|
+
$report = ($reportText -join "`n") | ConvertFrom-Json
|
|
85
|
+
if (-not $report.ok) { throw 'Resolve verification diagnostics before binding a case.' }
|
|
86
|
+
$binding = [ordered]@{}
|
|
87
|
+
foreach ($name in @('entryModuleId', 'entrySourceHash', 'sourceClosureHash', 'function', 'resolvedInputs', 'resolvedInputKinds')) {
|
|
88
|
+
$binding[$name] = $report.provenance.$name
|
|
89
|
+
}
|
|
90
|
+
$reference = [ordered]@{
|
|
91
|
+
referenceVersion = '1'
|
|
92
|
+
cases = @([ordered]@{
|
|
93
|
+
id = 'rectangle-2-by-3'
|
|
94
|
+
revision = '1'
|
|
95
|
+
basis = [ordered]@{
|
|
96
|
+
method = 'Hand-derived rectangle area'
|
|
97
|
+
derivation = 'Perpendicular sides: 2 m times 3 m equals 6 m^2.'
|
|
98
|
+
sourceDescription = 'Elementary geometry for the stated rectangle.'
|
|
99
|
+
}
|
|
100
|
+
binding = $binding
|
|
101
|
+
expected = @([ordered]@{
|
|
102
|
+
symbolId = '["symbol","root","area"]'
|
|
103
|
+
value = 6
|
|
104
|
+
unit = 'm^2'
|
|
105
|
+
})
|
|
106
|
+
})
|
|
107
|
+
}
|
|
108
|
+
$referencePath = Join-Path $PWD 'references\rectangle-reference.json'
|
|
109
|
+
$json = ($reference | ConvertTo-Json -Depth 20).Replace("`r`n", "`n") + "`n"
|
|
110
|
+
[System.IO.File]::WriteAllText($referencePath, $json, $utf8)
|
|
111
|
+
& $cso verify $source --function calculate --input width=2 --input height=3 --reference $referencePath --format json
|
|
112
|
+
if ($LASTEXITCODE -ne 0) { throw 'Independent reference verification failed.' }
|
|
113
|
+
```
|
|
114
|
+
<!-- docs:reference-authoring:end -->
|
|
115
|
+
|
|
116
|
+
This writer produces UTF-8 without a BOM and uses LF. Do not use `>` or
|
|
117
|
+
`Out-File` to save source or JSON in Windows PowerShell 5.1; their default
|
|
118
|
+
encoding differs from this file format. The console encoding assignment makes
|
|
119
|
+
UTF-8 CLI output safe to capture before `ConvertFrom-Json`, including Unicode
|
|
120
|
+
paths and diagnostics.
|
|
121
|
+
|
|
122
|
+
Check that `checks.independentReferenceAgreement.status` is `passed` with one checked
|
|
123
|
+
symbol. To use the case in the browser, add
|
|
124
|
+
`"reference": "references/rectangle-reference.json"` to the starter entry in
|
|
125
|
+
`reports.json`, restart `npm run dev`, and calculate with width 2 and height 3.
|
|
126
|
+
The same `--reference` option works with `cso html` and `cso pdf`.
|
|
127
|
+
|
|
128
|
+
For a replacement calculation, establish expected values for all calculated
|
|
129
|
+
symbols from a cited method or independent implementation. Use the symbol IDs
|
|
130
|
+
from its execution evidence, exact documented units, and the binding for that
|
|
131
|
+
source, function and input case. Check the reference against that execution.
|
|
132
|
+
Different inputs need their own cases; an unmatched case is `not_applicable` in
|
|
133
|
+
CLI reports and remains pending in the browser.
|
|
134
|
+
|
|
135
|
+
Source edits invalidate a binding, including edits to explanation text. After
|
|
136
|
+
reviewing a change, explicitly revise the binding and case revision while
|
|
137
|
+
preserving independently established expected values. Update expected values
|
|
138
|
+
only when their independent derivation changes, and record why. Reference
|
|
139
|
+
agreement does not establish human engineering approval.
|
package/template/scripts/dev.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { spawn } from 'node:child_process';
|
|
1
|
+
import { spawn, type ChildProcess } from 'node:child_process';
|
|
2
2
|
import { existsSync } from 'node:fs';
|
|
3
3
|
import { createRequire } from 'node:module';
|
|
4
4
|
import { dirname, join } from 'node:path';
|
|
@@ -10,48 +10,119 @@ const python = join(
|
|
|
10
10
|
'.venv',
|
|
11
11
|
process.platform === 'win32' ? 'Scripts/python.exe' : 'bin/python',
|
|
12
12
|
);
|
|
13
|
-
|
|
13
|
+
|
|
14
|
+
function awaitOrigin(child: ChildProcess): Promise<string> {
|
|
15
|
+
return new Promise((resolve, reject) => {
|
|
16
|
+
let stdout = '';
|
|
17
|
+
const finish = (error?: Error, origin?: string) => {
|
|
18
|
+
clearTimeout(timeout);
|
|
19
|
+
child.off('error', onError);
|
|
20
|
+
child.off('exit', onExit);
|
|
21
|
+
child.stdout?.off('data', onData);
|
|
22
|
+
if (error) reject(error);
|
|
23
|
+
else if (origin) resolve(origin);
|
|
24
|
+
};
|
|
25
|
+
const onError = (error: Error) => finish(error);
|
|
26
|
+
const onExit = (code: number | null) =>
|
|
27
|
+
finish(new Error(`Calculation server exited (${code})`));
|
|
28
|
+
const onData = (chunk: string) => {
|
|
29
|
+
stdout += chunk;
|
|
30
|
+
const match = /CSO dev (http:\/\/127\.0\.0\.1:\d+)/.exec(stdout);
|
|
31
|
+
if (match?.[1]) finish(undefined, match[1]);
|
|
32
|
+
};
|
|
33
|
+
const timeout = setTimeout(
|
|
34
|
+
() => finish(new Error('Calculation server did not start')),
|
|
35
|
+
30_000,
|
|
36
|
+
);
|
|
37
|
+
child.once('error', onError);
|
|
38
|
+
child.once('exit', onExit);
|
|
39
|
+
child.stdout?.setEncoding('utf8');
|
|
40
|
+
child.stdout?.on('data', onData);
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function main() {
|
|
14
45
|
if (!existsSync(python))
|
|
15
46
|
throw new Error(
|
|
16
47
|
'Run npm run setup to prepare the project Python environment.',
|
|
17
48
|
);
|
|
49
|
+
const { loadReports } = await import('./reports.ts');
|
|
50
|
+
const args = process.argv.slice(2);
|
|
51
|
+
if (args.length !== 0 && (args.length !== 2 || args[0] !== '--port'))
|
|
52
|
+
throw new Error('Usage: npm run dev -- [--port 0-65535]');
|
|
53
|
+
const spelling = args[1] ?? '5173';
|
|
54
|
+
if (!/^\d+$/.test(spelling) || Number(spelling) > 65535)
|
|
55
|
+
throw new Error('--port must be an integer between 0 and 65535');
|
|
56
|
+
const port = Number(spelling);
|
|
57
|
+
const reports = loadReports(project);
|
|
18
58
|
const require = createRequire(import.meta.url);
|
|
19
59
|
const cli = join(
|
|
20
60
|
dirname(require.resolve('@cs-object/cli/package.json')),
|
|
21
61
|
'dist/cli.js',
|
|
22
62
|
);
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
63
|
+
const children: ChildProcess[] = [];
|
|
64
|
+
let vite: Awaited<ReturnType<typeof import('vite').createServer>> | undefined;
|
|
65
|
+
let stopping = false;
|
|
66
|
+
const stop = () => {
|
|
67
|
+
if (stopping) return;
|
|
68
|
+
stopping = true;
|
|
69
|
+
for (const child of children) child.kill('SIGTERM');
|
|
70
|
+
if (vite) void vite.close();
|
|
71
|
+
};
|
|
72
|
+
process.on('SIGINT', stop);
|
|
73
|
+
process.on('SIGTERM', stop);
|
|
74
|
+
try {
|
|
75
|
+
const backends = [];
|
|
76
|
+
for (const report of reports) {
|
|
77
|
+
const child = spawn(
|
|
78
|
+
process.execPath,
|
|
79
|
+
[
|
|
80
|
+
cli,
|
|
81
|
+
'dev',
|
|
82
|
+
report.source,
|
|
83
|
+
'--function',
|
|
84
|
+
report.function,
|
|
85
|
+
...(report.reference ? ['--reference', report.reference] : []),
|
|
86
|
+
'--port',
|
|
87
|
+
'0',
|
|
88
|
+
],
|
|
89
|
+
{
|
|
90
|
+
cwd: project,
|
|
91
|
+
env: { ...process.env, PYTHON: python },
|
|
92
|
+
stdio: ['ignore', 'pipe', 'inherit'],
|
|
93
|
+
},
|
|
94
|
+
);
|
|
95
|
+
children.push(child);
|
|
96
|
+
const origin = await awaitOrigin(child);
|
|
97
|
+
child.once('exit', (code) => {
|
|
98
|
+
if (stopping) return;
|
|
99
|
+
process.stderr.write(
|
|
100
|
+
`Calculation server for ${report.id} exited (${code}).\n`,
|
|
101
|
+
);
|
|
102
|
+
process.exitCode = code || 1;
|
|
103
|
+
stop();
|
|
104
|
+
});
|
|
105
|
+
backends.push({ id: report.id, title: report.title, origin });
|
|
106
|
+
}
|
|
107
|
+
process.env.CSO_REPORT_BACKENDS = JSON.stringify(backends);
|
|
108
|
+
const { createServer } = await import('vite');
|
|
109
|
+
vite = await createServer({
|
|
110
|
+
server: { host: '127.0.0.1', port, strictPort: port !== 0 },
|
|
111
|
+
});
|
|
112
|
+
await vite.listen();
|
|
113
|
+
const address = vite.httpServer?.address();
|
|
114
|
+
if (!address || typeof address === 'string')
|
|
115
|
+
throw new Error('Vite has no localhost address');
|
|
116
|
+
process.stdout.write(`CSO dev http://127.0.0.1:${address.port}\n`);
|
|
117
|
+
} catch (error) {
|
|
118
|
+
stop();
|
|
119
|
+
throw error;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
main().catch((error: unknown) => {
|
|
53
124
|
process.stderr.write(
|
|
54
125
|
`${error instanceof Error ? error.message : String(error)}\n`,
|
|
55
126
|
);
|
|
56
127
|
process.exitCode = 1;
|
|
57
|
-
}
|
|
128
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { PythonIdentifierSchema } from '@cs-object/core';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
|
|
6
|
+
const ReportSchema = z.strictObject({
|
|
7
|
+
id: z.string().regex(/^[a-z][a-z0-9-]*$/),
|
|
8
|
+
title: z.string().min(1),
|
|
9
|
+
source: z.string().regex(/^calculations\/(?!.*\.\.\/)[^/].*\.cso\.py$/),
|
|
10
|
+
function: PythonIdentifierSchema,
|
|
11
|
+
reference: z.string().optional(),
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
export type Report = z.infer<typeof ReportSchema>;
|
|
15
|
+
|
|
16
|
+
export function loadReports(project: string): Report[] {
|
|
17
|
+
const reports = z
|
|
18
|
+
.array(ReportSchema)
|
|
19
|
+
.min(1)
|
|
20
|
+
.parse(JSON.parse(readFileSync(join(project, 'reports.json'), 'utf8')));
|
|
21
|
+
if (new Set(reports.map(({ id }) => id)).size !== reports.length)
|
|
22
|
+
throw new Error('reports.json contains duplicate report IDs');
|
|
23
|
+
return reports;
|
|
24
|
+
}
|