@markii/pack 0.10.0 → 0.12.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.
@@ -0,0 +1,81 @@
1
+ import type { PackManifest } from './manifest.js';
2
+ /** Options bounding how much a single `.mkp` open is willing to materialize. Defaults match `@markii/bundle`'s own zip-bomb guard, since a pack archive is bounded the same way a bundle is. */
3
+ export interface OpenPackArchiveOptions {
4
+ maxEntryBytes?: number;
5
+ maxTotalBytes?: number;
6
+ }
7
+ /**
8
+ * Why an `openPackArchive` call failed. `kind: 'zip'` covers everything
9
+ * `openZipBundle` itself rejects (a path-escaping entry, an oversized
10
+ * entry, a corrupt or malformed zip, ZIP64, a CRC mismatch, a name
11
+ * collision) — the message is already loud and specific, so it is passed
12
+ * through rather than re-summarized. `kind: 'manifest'` covers a
13
+ * `pack.json` that parses to invalid JSON or fails `parsePackManifest`.
14
+ * `kind: 'missing-entry'` covers a `.mkp` that lacks a required root entry.
15
+ */
16
+ export type PackArchiveError = {
17
+ kind: 'zip';
18
+ message: string;
19
+ } | {
20
+ kind: 'manifest';
21
+ errors: string[];
22
+ } | {
23
+ kind: 'missing-entry';
24
+ entry: string;
25
+ message: string;
26
+ };
27
+ export type OpenPackArchiveResult = {
28
+ ok: true;
29
+ archive: PackArchiveContents;
30
+ } | {
31
+ ok: false;
32
+ error: PackArchiveError;
33
+ };
34
+ /** The validated, read-only contents of a `.mkp` archive. */
35
+ export interface PackArchiveContents {
36
+ /** The parsed, validated `pack.json`. */
37
+ readonly manifest: PackManifest;
38
+ /** Forward-compatible warnings from parsing `pack.json` (unknown keys etc.) — see `parsePackManifest`. */
39
+ readonly manifestWarnings: readonly string[];
40
+ /** The prebuilt registration script's bytes (`webview.js`, always present — see this module's doc comment). */
41
+ readonly scriptBytes: Uint8Array;
42
+ /** The prebuilt stylesheet's bytes (`webview.css`), present only when the pack ships one. */
43
+ readonly stylesheetBytes?: Uint8Array;
44
+ /** Shared Lua modules under `scripts/`, keyed by their path relative to that folder (e.g. `"http.lua"` for `scripts/http.lua`). Empty when the pack ships none. */
45
+ readonly scriptModules: Readonly<Record<string, Uint8Array>>;
46
+ /**
47
+ * Archive-root entries present in the zip but outside the prebuilt
48
+ * contract (e.g. a leftover component source alongside the compiled
49
+ * script, or an entry nested under a subfolder rather than at the zip
50
+ * root). Never an error: `.mkp` follows the same "prebuilt wins,
51
+ * anything else is ignored" posture a pack folder already has for a
52
+ * `webview.js` sitting beside sources. A host's diagnostics surface may
53
+ * report these; this module only supplies the facts.
54
+ */
55
+ readonly ignoredEntries: readonly string[];
56
+ }
57
+ /**
58
+ * Opens a `.mkp` archive from its raw bytes and validates it against the
59
+ * prebuilt-pack contract. Never throws: every failure — a hostile or
60
+ * corrupt zip, a missing required entry, an invalid `pack.json` — comes
61
+ * back as `{ ok: false, error }` rather than an exception, so a caller can
62
+ * report the reason without a `try`/`catch` of its own.
63
+ */
64
+ export declare function openPackArchive(bytes: Uint8Array, options?: OpenPackArchiveOptions): Promise<OpenPackArchiveResult>;
65
+ /**
66
+ * The archive naming rule for a PRODUCED `.mkp`: `<name>-<version>.mkp`
67
+ * when `pack.json` declares a `version`, or `<name>.mkp` when it does not.
68
+ *
69
+ * `version` is optional in the manifest (see `PackManifest`'s doc comment)
70
+ * and its absence is valid, not an error — so a produced archive's name
71
+ * must stay valid too. Falling back to the bare pack name rather than
72
+ * inventing a placeholder version (`0.0.0`, `unversioned`, ...) avoids
73
+ * claiming a version the manifest never declared: the filename says exactly
74
+ * as much as the manifest does, no more.
75
+ *
76
+ * Both `name` and a present `version` are already validated by
77
+ * `parsePackManifest` (lowercase-kebab; plain `MAJOR.MINOR.PATCH` digits
78
+ * and dots) before this function ever sees them, so no further escaping is
79
+ * needed to use them as a filename.
80
+ */
81
+ export declare function packArchiveFileName(manifest: Pick<PackManifest, 'name' | 'version'>): string;
@@ -0,0 +1,200 @@
1
+ /**
2
+ * `.mkp` pack archives (docs/packs.md, issue #16): a zip of a pack's
3
+ * PREBUILT form, files at the zip root — `pack.json`, `webview.js`,
4
+ * `webview.css` when the pack has styles, `scripts/` when the pack ships
5
+ * shared Lua. This is the reader half: given the raw bytes of a `.mkp`
6
+ * file, validate it and expose its contents for read-only loading.
7
+ *
8
+ * A `.mkp` is prebuilt-only, on purpose (docs/packs.md's "Two ways to run a
9
+ * pack: prebuilt and from source"): this module never compiles anything. A
10
+ * `webview.js` at the archive root is REQUIRED — an archive that ships only
11
+ * component sources is not a valid `.mkp`, because there would be nothing
12
+ * for a host to load without running a compiler, and `.mkp` exists
13
+ * precisely for hosts that may not carry one.
14
+ *
15
+ * This reader reuses `@markii/bundle`'s zip reader (`openZipBundle`)
16
+ * rather than re-implementing zip parsing or a path jail: AGENTS.md calls a
17
+ * reimplemented jail a bug. `openZipBundle` already rejects a `../`
18
+ * segment, an absolute path, a backslash path, or a Windows drive-letter
19
+ * path in any entry name (zip-slip), and bounds both a single entry's and
20
+ * the whole archive's declared uncompressed size before any bytes are
21
+ * inflated (the zip-bomb guard) — see `packages/markii-bundle/src/zip.ts`.
22
+ * Reusing it means a `.mkp` gets the exact same `bounded-open` guarantee a
23
+ * `.mkz` bundle gets, with no second implementation to drift out of sync.
24
+ *
25
+ * This module never touches a real filesystem: it takes bytes in and
26
+ * returns parsed, in-memory contents (or a structured error) out. It never
27
+ * writes anything anywhere, so "nothing is written outside the pack
28
+ * directory" holds structurally for every rejection path — there is no
29
+ * write side to leak from. A host that wants to install a `.mkp` (unzip it
30
+ * into its own pack directory) is a separate, host-specific concern (the
31
+ * brief's "Install pack from file" command) and is out of this module's
32
+ * scope entirely.
33
+ */
34
+ import { BundleZipError, DEFAULT_MAX_ZIP_ENTRY_BYTES, DEFAULT_MAX_ZIP_TOTAL_BYTES, openZipBundle, } from '@markii/bundle';
35
+ import { parsePackManifest } from './manifest.js';
36
+ /** The archive-root entry names the prebuilt contract reserves. */
37
+ const MANIFEST_ENTRY = 'pack.json';
38
+ const SCRIPT_ENTRY = 'webview.js';
39
+ const STYLESHEET_ENTRY = 'webview.css';
40
+ const SCRIPTS_DIR_PREFIX = 'scripts/';
41
+ function isScriptModulePath(path) {
42
+ return (path.startsWith(SCRIPTS_DIR_PREFIX) &&
43
+ path.length > SCRIPTS_DIR_PREFIX.length);
44
+ }
45
+ /**
46
+ * Decodes UTF-8 bytes, rejecting anything that is not well-formed UTF-8
47
+ * rather than silently substituting replacement characters (the default
48
+ * `TextDecoder` behavior) — a `pack.json` that decodes to mangled text
49
+ * should fail loudly as a manifest problem, not parse whatever garbled JSON
50
+ * `�` substitution happens to produce.
51
+ */
52
+ function decodeUtf8Strict(bytes) {
53
+ try {
54
+ return new TextDecoder('utf-8', { fatal: true }).decode(bytes);
55
+ }
56
+ catch {
57
+ return undefined;
58
+ }
59
+ }
60
+ /**
61
+ * Opens a `.mkp` archive from its raw bytes and validates it against the
62
+ * prebuilt-pack contract. Never throws: every failure — a hostile or
63
+ * corrupt zip, a missing required entry, an invalid `pack.json` — comes
64
+ * back as `{ ok: false, error }` rather than an exception, so a caller can
65
+ * report the reason without a `try`/`catch` of its own.
66
+ */
67
+ export async function openPackArchive(bytes, options = {}) {
68
+ const maxEntryBytes = options.maxEntryBytes ?? DEFAULT_MAX_ZIP_ENTRY_BYTES;
69
+ const maxTotalBytes = options.maxTotalBytes ?? DEFAULT_MAX_ZIP_TOTAL_BYTES;
70
+ let storage;
71
+ try {
72
+ storage = openZipBundle(bytes, { maxEntryBytes, maxTotalBytes });
73
+ }
74
+ catch (err) {
75
+ if (err instanceof BundleZipError) {
76
+ return { ok: false, error: { kind: 'zip', message: err.message } };
77
+ }
78
+ return {
79
+ ok: false,
80
+ error: {
81
+ kind: 'zip',
82
+ message: `pack archive rejected: ${err instanceof Error ? err.message : String(err)}`,
83
+ },
84
+ };
85
+ }
86
+ const paths = await storage.list();
87
+ if (!paths.includes(MANIFEST_ENTRY)) {
88
+ return {
89
+ ok: false,
90
+ error: {
91
+ kind: 'missing-entry',
92
+ entry: MANIFEST_ENTRY,
93
+ message: `pack archive rejected: no "${MANIFEST_ENTRY}" entry at the archive root`,
94
+ },
95
+ };
96
+ }
97
+ if (!paths.includes(SCRIPT_ENTRY)) {
98
+ return {
99
+ ok: false,
100
+ error: {
101
+ kind: 'missing-entry',
102
+ entry: SCRIPT_ENTRY,
103
+ message: `pack archive rejected: no "${SCRIPT_ENTRY}" entry at the archive root: a .mkp carries a pack's prebuilt form only, never its sources, so a compiled ${SCRIPT_ENTRY} is required`,
104
+ },
105
+ };
106
+ }
107
+ const manifestBytes = await storage.read(MANIFEST_ENTRY);
108
+ // Not reachable in practice (the entry was just confirmed present in the
109
+ // same storage instance's own listing), but `read` is typed to return
110
+ // `undefined` and this keeps the function honestly total rather than
111
+ // asserting past the type.
112
+ if (manifestBytes === undefined) {
113
+ return {
114
+ ok: false,
115
+ error: {
116
+ kind: 'missing-entry',
117
+ entry: MANIFEST_ENTRY,
118
+ message: `pack archive rejected: "${MANIFEST_ENTRY}" could not be read`,
119
+ },
120
+ };
121
+ }
122
+ const manifestText = decodeUtf8Strict(manifestBytes);
123
+ if (manifestText === undefined) {
124
+ return {
125
+ ok: false,
126
+ error: {
127
+ kind: 'manifest',
128
+ errors: [`"${MANIFEST_ENTRY}" is not valid UTF-8`],
129
+ },
130
+ };
131
+ }
132
+ const parsed = parsePackManifest(manifestText);
133
+ if (!parsed.ok) {
134
+ return { ok: false, error: { kind: 'manifest', errors: parsed.errors } };
135
+ }
136
+ const scriptBytes = await storage.read(SCRIPT_ENTRY);
137
+ if (scriptBytes === undefined) {
138
+ return {
139
+ ok: false,
140
+ error: {
141
+ kind: 'missing-entry',
142
+ entry: SCRIPT_ENTRY,
143
+ message: `pack archive rejected: "${SCRIPT_ENTRY}" could not be read`,
144
+ },
145
+ };
146
+ }
147
+ let stylesheetBytes;
148
+ if (paths.includes(STYLESHEET_ENTRY)) {
149
+ stylesheetBytes = await storage.read(STYLESHEET_ENTRY);
150
+ }
151
+ const scriptModules = Object.create(null);
152
+ const ignoredEntries = [];
153
+ for (const path of paths) {
154
+ if (path === MANIFEST_ENTRY ||
155
+ path === SCRIPT_ENTRY ||
156
+ path === STYLESHEET_ENTRY) {
157
+ continue;
158
+ }
159
+ if (isScriptModulePath(path)) {
160
+ const data = await storage.read(path);
161
+ if (data !== undefined) {
162
+ scriptModules[path.slice(SCRIPTS_DIR_PREFIX.length)] = data;
163
+ }
164
+ continue;
165
+ }
166
+ ignoredEntries.push(path);
167
+ }
168
+ return {
169
+ ok: true,
170
+ archive: {
171
+ manifest: parsed.manifest,
172
+ manifestWarnings: parsed.warnings,
173
+ scriptBytes,
174
+ ...(stylesheetBytes !== undefined ? { stylesheetBytes } : {}),
175
+ scriptModules,
176
+ ignoredEntries,
177
+ },
178
+ };
179
+ }
180
+ /**
181
+ * The archive naming rule for a PRODUCED `.mkp`: `<name>-<version>.mkp`
182
+ * when `pack.json` declares a `version`, or `<name>.mkp` when it does not.
183
+ *
184
+ * `version` is optional in the manifest (see `PackManifest`'s doc comment)
185
+ * and its absence is valid, not an error — so a produced archive's name
186
+ * must stay valid too. Falling back to the bare pack name rather than
187
+ * inventing a placeholder version (`0.0.0`, `unversioned`, ...) avoids
188
+ * claiming a version the manifest never declared: the filename says exactly
189
+ * as much as the manifest does, no more.
190
+ *
191
+ * Both `name` and a present `version` are already validated by
192
+ * `parsePackManifest` (lowercase-kebab; plain `MAJOR.MINOR.PATCH` digits
193
+ * and dots) before this function ever sees them, so no further escaping is
194
+ * needed to use them as a filename.
195
+ */
196
+ export function packArchiveFileName(manifest) {
197
+ return manifest.version === undefined
198
+ ? `${manifest.name}.mkp`
199
+ : `${manifest.name}-${manifest.version}.mkp`;
200
+ }
@@ -2,9 +2,13 @@ import type { PackManifest } from './manifest.js';
2
2
  /**
3
3
  * Which directive form a pack component is written as. Mirrors
4
4
  * `@markii/stdlib`'s `ComponentKind`; defined locally here rather than
5
- * imported because `@markii/pack` is a published, zero-dependency package
6
- * (see AGENTS.md's Stack section) and `@markii/stdlib` is a sibling
7
- * package, not a dependency of this one. The two unions must stay aligned;
5
+ * imported because `@markii/stdlib` is a sibling package this package
6
+ * deliberately does not depend on, to stay decoupled from the reference
7
+ * renderer's component contracts (see AGENTS.md's Stack section). This is
8
+ * unrelated to `@markii/pack`'s dependency on `@markii/bundle` for `.mkp`
9
+ * archive reading (`./archive.ts`, issue #16): that dependency reuses a
10
+ * neutral, framework-agnostic zip reader and path jail, not a renderer
11
+ * contract. The two unions must stay aligned;
8
12
  * `@markii/host` (which depends on both) carries an executable check of
9
13
  * that alignment, since this package cannot import `@markii/stdlib` to
10
14
  * check it itself.
package/dist/index.d.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  export type { PackManifest, PackManifestParseResult } from './manifest.js';
2
2
  export { parsePackManifest } from './manifest.js';
3
+ export type { OpenPackArchiveOptions, OpenPackArchiveResult, PackArchiveContents, PackArchiveError, } from './archive.js';
4
+ export { openPackArchive, packArchiveFileName } from './archive.js';
3
5
  export type { PackComponentAttribute, PackComponentDefinition, PackComponentEntry, PackComponentKind, PackComponentListing, ResolvedPackComponent, } from './components.js';
4
6
  export { PACK_ATTRIBUTE_NAME_PATTERN, PACK_COMPONENT_KINDS, isPackAttributeName, packComponents, resolvePackComponent, } from './components.js';
5
7
  export type { ComposeDirectiveNameResult, NamespaceCollision, NamespaceValidationResult, } from './namespace.js';
package/dist/index.js CHANGED
@@ -1,8 +1,12 @@
1
1
  // @markii/pack: the neutral component-pack contract (docs/packs.md). This
2
- // is slice 0 of issue #3 — the manifest shape, namespace/engine rules, and
3
- // hand-rolled validation only. No registry loading, no `uses:` surfacing,
4
- // no sandboxed `require`, no filesystem reads: those are later slices.
2
+ // started as slice 0 of issue #3 — the manifest shape, namespace/engine
3
+ // rules, and hand-rolled validation only, with no registry loading, no
4
+ // `uses:` surfacing, no sandboxed `require`, and no filesystem reads. Issue
5
+ // #16's `.mkp` archive reader (`./archive.ts`) is the first filesystem-
6
+ // adjacent slice: it reads validated bytes in and returns parsed contents
7
+ // out, touching no real filesystem itself (a host does that).
5
8
  export { parsePackManifest } from './manifest.js';
9
+ export { openPackArchive, packArchiveFileName } from './archive.js';
6
10
  export { PACK_ATTRIBUTE_NAME_PATTERN, PACK_COMPONENT_KINDS, isPackAttributeName, packComponents, resolvePackComponent, } from './components.js';
7
11
  export { RESERVED_NAMESPACE_SEGMENTS, composeDirectiveName, detectNamespaceCollisions, validateLocalComponentName, validatePackName, } from './namespace.js';
8
12
  export { isValidPackNameShape, resolveUses } from './uses.js';
@@ -14,12 +14,31 @@ import type { PackComponentEntry } from './components.js';
14
14
  * `kind`/`attributes` metadata (see `./components.ts`,
15
15
  * `resolvePackComponent`, `packComponents`).
16
16
  *
17
+ * `components` may be an EMPTY object. There is exactly one way to share
18
+ * Lua beyond a bundle's own `scripts/` folder: a pack module. A folder of
19
+ * shared Lua with nothing to render is simply a pack whose `components`
20
+ * is `{}` and which carries its own `scripts/*.lua` — it contributes zero
21
+ * directives and never appears in a component catalog, but its modules
22
+ * still resolve through `require "name/module"` like any other pack's.
23
+ *
17
24
  * Shared Lua modules are NOT a manifest field. docs/packs.md says they
18
25
  * travel under a conventional `scripts/` directory inside the pack
19
26
  * (`require "ana/http"`), the same way a bundle's `scripts/` works — there
20
27
  * is no separate manifest declaration for them, so this type doesn't add
21
- * one. (Flagged in the slice-0 report for orchestrator sign-off in case
22
- * docs/packs.md meant something more explicit.)
28
+ * one.
29
+ *
30
+ * `version` is an OPTIONAL fourth field: a plain semver string
31
+ * (`MAJOR.MINOR.PATCH`, digits only, no leading zeros on a multi-digit
32
+ * component — see `SEMVER_PATTERN`). A pack that omits it keeps loading
33
+ * exactly as before, with no warning. Prerelease/build suffixes
34
+ * (`-beta.1`, `+build5`) are deliberately NOT accepted: AGENTS.md already
35
+ * commits this project to "plain semver" for spec versioning, and a pack
36
+ * version exists so a host can compare/display it, not to drive a resolver
37
+ * that needs prerelease ordering. Accepting a wider grammar than the one
38
+ * fact this field is for would be a casual-user-facing complication with
39
+ * no present consumer. A malformed `version` is an ERROR, not a
40
+ * forward-compatible warning, the same posture `name` gets: a version a
41
+ * host cannot trust is worse than no version.
23
42
  */
24
43
  export interface PackManifest {
25
44
  /** The pack's namespace, e.g. `"ana"`. See `validatePackName`. */
@@ -35,6 +54,12 @@ export interface PackManifest {
35
54
  * the one place both forms are normalized.
36
55
  */
37
56
  components: Record<string, PackComponentEntry>;
57
+ /**
58
+ * Optional plain-semver version string, e.g. `"1.0.0"`. Absent unless
59
+ * the pack author declared one. See this interface's doc comment for
60
+ * the accepted grammar and why prerelease/build suffixes are rejected.
61
+ */
62
+ version?: string;
38
63
  }
39
64
  export type PackManifestParseResult = {
40
65
  ok: true;
package/dist/manifest.js CHANGED
@@ -1,6 +1,16 @@
1
1
  import { validateLocalComponentName, validatePackName } from './namespace.js';
2
2
  import { PACK_ATTRIBUTE_NAME_PATTERN, PACK_COMPONENT_KINDS, isPackAttributeName, } from './components.js';
3
- const KNOWN_TOP_LEVEL_KEYS = new Set(['name', 'engine', 'components']);
3
+ const KNOWN_TOP_LEVEL_KEYS = new Set([
4
+ 'name',
5
+ 'engine',
6
+ 'components',
7
+ 'version',
8
+ ]);
9
+ // Plain semver only: MAJOR.MINOR.PATCH, digits only, no leading zeros on a
10
+ // multi-digit component ("1.0.0" and "0.0.1" are valid; "01.0.0" is not).
11
+ // No prerelease or build metadata suffix — see PackManifest's doc comment
12
+ // for why this field deliberately does not accept the wider semver grammar.
13
+ const SEMVER_PATTERN = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/;
4
14
  // The keys a component's object-form entry may carry, and which of those
5
15
  // are required. Mirrors KNOWN_TOP_LEVEL_KEYS's role for the manifest root:
6
16
  // an unrecognized key here is a forward-compatible warning, not an error.
@@ -324,14 +334,28 @@ export function parsePackManifest(json) {
324
334
  sanitized.attributes = attributes;
325
335
  result[key] = sanitized;
326
336
  }
327
- if (Object.keys(componentsObj).length === 0) {
328
- errors.push('"components" must have at least one entry');
329
- componentsValid = false;
330
- }
331
337
  if (componentsValid) {
332
338
  components = result;
333
339
  }
334
340
  }
341
+ // --- version (optional) ---
342
+ //
343
+ // Absent is valid and produces neither an error nor a warning. Present
344
+ // but malformed is an ERROR (rejects the whole manifest), not a
345
+ // forward-compatible warning: a version a host cannot trust is worse
346
+ // than no version at all, the same posture as a malformed `name`.
347
+ let version;
348
+ let versionValid = true;
349
+ if (Object.hasOwn(obj, 'version')) {
350
+ const rawVersion = obj.version;
351
+ if (typeof rawVersion !== 'string' || !SEMVER_PATTERN.test(rawVersion)) {
352
+ errors.push('"version" must be a plain semver string ("MAJOR.MINOR.PATCH", digits only, no leading zeros, no prerelease or build suffix)');
353
+ versionValid = false;
354
+ }
355
+ else {
356
+ version = rawVersion;
357
+ }
358
+ }
335
359
  // --- unknown top-level keys: forward-compat warning, not an error ---
336
360
  for (const key of Object.keys(obj)) {
337
361
  if (Object.hasOwn(obj, key) && !KNOWN_TOP_LEVEL_KEYS.has(key)) {
@@ -341,12 +365,15 @@ export function parsePackManifest(json) {
341
365
  if (errors.length > 0 ||
342
366
  name === undefined ||
343
367
  engine === undefined ||
344
- components === undefined) {
368
+ components === undefined ||
369
+ !versionValid) {
345
370
  return { ok: false, errors };
346
371
  }
347
372
  return {
348
373
  ok: true,
349
- manifest: { name, engine, components },
374
+ manifest: version === undefined
375
+ ? { name, engine, components }
376
+ : { name, engine, components, version },
350
377
  warnings,
351
378
  };
352
379
  }
@@ -92,9 +92,10 @@ export interface NamespaceCollision {
92
92
  /**
93
93
  * Given several installed pack namespaces, returns the ones that appear
94
94
  * more than once. Pure predicate only — docs/packs.md says installing two
95
- * packs with the same namespace is rejected at install time, but that
96
- * install-time rejection (and the vault-library-shadows-a-pack warning
97
- * case) is a later slice; this is the detection primitive it will use.
95
+ * packs with the same namespace is rejected at install time; `@markii/host`'s
96
+ * `discoverPacks` (`packages/markii-host/src/packs/discover.ts`) is the
97
+ * caller that applies this at install time, excluding every pack sharing a
98
+ * colliding namespace.
98
99
  *
99
100
  * Comparison is exact-string (case-sensitive), matching `validatePackName`
100
101
  * which already forces lowercase, so two namespaces differing only in case
package/dist/namespace.js CHANGED
@@ -172,9 +172,10 @@ export function composeDirectiveName(packName, localName) {
172
172
  /**
173
173
  * Given several installed pack namespaces, returns the ones that appear
174
174
  * more than once. Pure predicate only — docs/packs.md says installing two
175
- * packs with the same namespace is rejected at install time, but that
176
- * install-time rejection (and the vault-library-shadows-a-pack warning
177
- * case) is a later slice; this is the detection primitive it will use.
175
+ * packs with the same namespace is rejected at install time; `@markii/host`'s
176
+ * `discoverPacks` (`packages/markii-host/src/packs/discover.ts`) is the
177
+ * caller that applies this at install time, excluding every pack sharing a
178
+ * colliding namespace.
178
179
  *
179
180
  * Comparison is exact-string (case-sensitive), matching `validatePackName`
180
181
  * which already forces lowercase, so two namespaces differing only in case
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@markii/pack",
3
- "version": "0.10.0",
4
- "description": "Component pack contract for Markii (docs/packs.md): the pack.json manifest shape, namespace and engine rules, and hand-rolled validation. No React, no parsing, no bundle loading.",
3
+ "version": "0.12.0",
4
+ "description": "Component pack contract for Markii (docs/packs.md): the pack.json manifest shape, namespace and engine rules, hand-rolled validation, and the .mkp prebuilt-archive reader. No React, no parsing.",
5
5
  "keywords": [
6
6
  "markdown",
7
7
  "mark",
@@ -40,5 +40,8 @@
40
40
  "build": "tsc --noEmit -p tsconfig.json",
41
41
  "build:dist": "rm -rf dist && tsc -p tsconfig.build.json",
42
42
  "lint": "eslint ."
43
+ },
44
+ "dependencies": {
45
+ "@markii/bundle": "0.12.0"
43
46
  }
44
47
  }