@llman-sdd/core 0.1.3 → 0.1.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llman-sdd/core",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "Pure domain logic for llman-sdd (config, gherkin specs, validation, templates)",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -90,10 +90,12 @@ export async function runList(io: FreezeIo, sz: SevenZipPort, rootAbs: string):
90
90
  if (!io.exists(archiveAbs)) {
91
91
  return [`No freeze archive found at ./${ARCHIVE_DIR_REL}/${FREEZE_ARCHIVE_NAME}`];
92
92
  }
93
- const entries = (await sz.listEntries(archiveAbs))
94
- .map((n) => n.replace(/\/$/u, ''))
95
- .filter((n) => DATED_RE.test(n) && n.split('/').length === 1);
96
- const unique = [...new Set(entries)].toSorted();
93
+ // Real 7z lists file paths under their directory (`<dir>/proposal.md`);
94
+ // directory entries themselves are skipped by parseListNames. Derive the
95
+ // archived change names from the leading path segment.
96
+ const entries = (await sz.listEntries(archiveAbs)).map((n) => n.replace(/\/$/u, ''));
97
+ const top = (n: string): string => n.split('/')[0] ?? n;
98
+ const unique = [...new Set(entries.map(top).filter((n) => DATED_RE.test(n)))].toSorted();
97
99
  if (unique.length === 0) {
98
100
  return [
99
101
  `Freeze archive ${ARCHIVE_DIR_REL}/${FREEZE_ARCHIVE_NAME} contains no archived changes`,
@@ -1,3 +1,4 @@
1
+ import { Buffer } from 'node:buffer';
1
2
  import { mkdirSync } from 'node:fs';
2
3
  import { basename, join } from 'node:path';
3
4
 
@@ -7,6 +8,13 @@ import { basename, join } from 'node:path';
7
8
  * instance per operation; extraction target must be a fresh directory
8
9
  * (WASM 7z refuses existing -o dirs). Spike-proven on Bun: compress / list /
9
10
  * extract keep directory structure.
11
+ *
12
+ * Compiled single-file binaries have no on-disk 7zz.wasm (Emscripten would
13
+ * probe $bunfs and abort), so build-binary.ts additionally injects the wasm
14
+ * base64 via the literal define `process.env.LLMAN_SDD_EMBEDDED_7ZZ_WASM_B64`
15
+ * (define only rewrites literal member access — do not route through a
16
+ * variable). Unset in source/npm/Node runs → glue loads the .wasm from disk
17
+ * as before.
10
18
  */
11
19
  import SevenZip from '7z-wasm';
12
20
 
@@ -35,14 +43,34 @@ type ModuleFactory = (opts: {
35
43
  noInitialRun: boolean;
36
44
  print?: (text: string) => void;
37
45
  printErr?: (text: string) => void;
46
+ wasmBinary?: Uint8Array;
38
47
  }) => Promise<EmscriptenModule>;
39
48
 
40
- async function initModule(print?: (text: string) => void): Promise<EmscriptenModule> {
49
+ const WASM_MAGIC = [0x00, 0x61, 0x73, 0x6d] as const;
50
+
51
+ /** Decode a base64 wasm blob; undefined when absent or not a WASM binary.
52
+ * Base64 decoding itself never throws, so the magic bytes are the guard. */
53
+ export function resolveEmbeddedWasmB64(value: unknown): Uint8Array | undefined {
54
+ if (typeof value !== 'string' || value === '') return undefined;
55
+ const bytes = Buffer.from(value, 'base64');
56
+ if (bytes.length < 4 || WASM_MAGIC.some((b, i) => bytes[i] !== b)) return undefined;
57
+ return new Uint8Array(bytes);
58
+ }
59
+
60
+ /** Read the build-injected wasm; undefined when absent or malformed. */
61
+ export function embeddedWasmBinary(): Uint8Array | undefined {
62
+ return resolveEmbeddedWasmB64(process.env.LLMAN_SDD_EMBEDDED_7ZZ_WASM_B64);
63
+ }
64
+
65
+ async function initModule(
66
+ print?: (text: string) => void,
67
+ wasmBinary?: Uint8Array,
68
+ ): Promise<EmscriptenModule> {
41
69
  const sink = (text: string): void => {
42
70
  if (print) print(text);
43
71
  };
44
72
  const factory = SevenZip as unknown as ModuleFactory;
45
- return factory({ noInitialRun: true, print: sink, printErr: sink });
73
+ return factory({ noInitialRun: true, print: sink, printErr: sink, wasmBinary });
46
74
  }
47
75
 
48
76
  function mount(mod: EmscriptenModule, seq: number, hostDir: string): string {
@@ -78,9 +106,10 @@ function parseListNames(lines: string[]): string[] {
78
106
  }
79
107
 
80
108
  export async function makeWasmSevenZip(): Promise<SevenZipPort> {
109
+ const wasmBinary = embeddedWasmBinary();
81
110
  return {
82
111
  async add(archivePath: string, baseDir: string, entries: string[]): Promise<void> {
83
- const mod = await initModule();
112
+ const mod = await initModule(undefined, wasmBinary);
84
113
  const archMount = mount(mod, 1, join(archivePath, '..'));
85
114
  const baseMount = mount(mod, 2, baseDir);
86
115
  mod.FS.chdir(baseMount);
@@ -90,7 +119,7 @@ export async function makeWasmSevenZip(): Promise<SevenZipPort> {
90
119
 
91
120
  async listEntries(archivePath: string): Promise<string[]> {
92
121
  const lines: string[] = [];
93
- const mod = await initModule((text) => lines.push(text));
122
+ const mod = await initModule((text) => lines.push(text), wasmBinary);
94
123
  const archMount = mount(mod, 1, join(archivePath, '..'));
95
124
  const rc = mod.callMain(['l', `${archMount}/${basename(archivePath)}`]);
96
125
  if (rc !== 0) throw new SevenZipError(`7z list failed (rc=${rc})`);
@@ -98,7 +127,7 @@ export async function makeWasmSevenZip(): Promise<SevenZipPort> {
98
127
  },
99
128
 
100
129
  async extractAll(archivePath: string, destDir: string): Promise<void> {
101
- const mod = await initModule();
130
+ const mod = await initModule(undefined, wasmBinary);
102
131
  const archMount = mount(mod, 1, join(archivePath, '..'));
103
132
  mkdirSync(destDir, { recursive: true });
104
133
  const destMount = mount(mod, 2, destDir);
package/src/index.ts CHANGED
@@ -144,7 +144,13 @@ export {
144
144
  type SpecMorphology,
145
145
  type SpecSummary,
146
146
  } from './report/specs.ts';
147
- export { SevenZipError, makeWasmSevenZip, type SevenZipPort } from './archive/sevenzip.ts';
147
+ export {
148
+ SevenZipError,
149
+ embeddedWasmBinary,
150
+ makeWasmSevenZip,
151
+ resolveEmbeddedWasmB64,
152
+ type SevenZipPort,
153
+ } from './archive/sevenzip.ts';
148
154
  export {
149
155
  ARCHIVE_DIR_REL,
150
156
  FREEZE_ARCHIVE_NAME,
@@ -1,18 +1,19 @@
1
1
  /**
2
2
  * Embedded template table for single-file binaries.
3
3
  *
4
- * Bun <= 1.4.x has no working embedding mechanism (`assets` is a silent
5
- * no-op, `?raw`/`?asset` fail to resolve, `import.meta.glob` is unavailable
6
- * in compiled output), so templates are injected through the same build-time
7
- * `define` mechanism as LLMAN_SDD_VERSION: build-binary.ts collects
8
- * packages/core/templates into a root-relative path→content map and sets
9
- * process.env.LLMAN_SDD_EMBEDDED_TEMPLATES to that JSON. Bun inlines define
10
- * values as raw expressions — the JSON is a valid object literal, so in the
11
- * compiled binary the expression IS the table object itself, while a JSON
12
- * string reaches these callers through the same expression in other engines
13
- * or when force-set via env. Both forms are accepted below; every non-compiled
14
- * run (source, npm package, Node) leaves the define unset and falls back to
15
- * the real filesystem via TEMPLATES_ROOT.
4
+ * Bun 1.4.x offers no embedding that survives our Node >= 24 dual-runtime
5
+ * constraint: `with { type: "text" }` works in compiled output (probed on
6
+ * 1.4.2) but Node cannot load that attribute, and `assets`/`?raw`/`?asset`
7
+ * never land in the bundle — so templates are injected through the same
8
+ * build-time `define` mechanism as LLMAN_SDD_VERSION: build-binary.ts
9
+ * collects packages/core/templates into a root-relative path→content map and
10
+ * sets process.env.LLMAN_SDD_EMBEDDED_TEMPLATES to that JSON. Bun inlines
11
+ * define values as raw expressions — the JSON is a valid object literal, so
12
+ * in the compiled binary the expression IS the table object itself, while a
13
+ * JSON string reaches these callers through the same expression in other
14
+ * engines or when force-set via env. Both forms are accepted below; every
15
+ * non-compiled run (source, npm package, Node) leaves the define unset and
16
+ * falls back to the real filesystem via TEMPLATES_ROOT.
16
17
  */
17
18
  import type { TemplateIo } from './skills.ts';
18
19