@aikdna/kdna-core 0.11.1 → 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.
package/README.md CHANGED
@@ -1,6 +1,10 @@
1
1
  # @aikdna/kdna-core
2
2
 
3
- Core library for loading, validating, linting, rendering, composing, and directly reading KDNA `.kdna` cognition assets. It has zero npm runtime dependencies.
3
+ Core library for KDNA judgment assets.
4
+
5
+ KDNA Core v1 defines the official `.kdna` source/container contract used by
6
+ the CLI, Studio export, skills, MCP integrations, and downstream agent
7
+ runtimes.
4
8
 
5
9
  ## Installation
6
10
 
@@ -8,187 +12,92 @@ Core library for loading, validating, linting, rendering, composing, and directl
8
12
  npm install @aikdna/kdna-core
9
13
  ```
10
14
 
11
- ## Preferred public API
12
-
13
- Third-party adapters should start with the stable asset-first API. These
14
- functions accept a `.kdna` file path, bytes, or an already opened asset and do
15
- not require persistent extraction.
15
+ If you need full JSON Schema validation through Ajv, also install:
16
16
 
17
- ```js
18
- const {
19
- inspectKDNA,
20
- validateKDNA,
21
- loadKDNA,
22
- renderForAgent,
23
- verifyAsset,
24
- verifyDigest,
25
- verifySignature,
26
- matchDomain,
27
- composeKDNA
28
- } = require('@aikdna/kdna-core');
29
-
30
- const info = await inspectKDNA('./writing.kdna');
31
- const validation = await validateKDNA('./writing.kdna');
32
- const loaded = await loadKDNA('./writing.kdna', { profile: 'compact' });
33
- const promptContext = await renderForAgent('./writing.kdna');
34
-
35
- await verifyAsset('./writing.kdna', {
36
- asset_digest: info.asset_digest,
37
- requireSignature: true
38
- });
39
- await verifyDigest('./writing.kdna', info.asset_digest);
40
- await verifySignature('./writing.kdna');
41
-
42
- const matches = await matchDomain('Review this writing draft', ['./writing.kdna']);
43
- const composed = await composeKDNA(['./writing.kdna', './agent_safety.kdna'], {
44
- input: 'Review this public release note for safety and writing quality'
45
- });
17
+ ```bash
18
+ npm install ajv ajv-formats
46
19
  ```
47
20
 
48
- Stable entry points:
49
-
50
- | Function | Purpose |
51
- | --- | --- |
52
- | `openKDNA()` | Open a `.kdna` file or bytes as an immutable asset. |
53
- | `inspectKDNA()` | Return manifest, entries, access, quality, risk, and digests. |
54
- | `validateKDNA()` | Run asset, lint, schema, and cross-file validation. |
55
- | `loadKDNA()` | Load index/compact/scenario/full profiles directly from `.kdna`. |
56
- | `renderForAgent()` | Render a loaded asset into agent prompt context. |
57
- | `verifyAsset()` | Run full asset verification: digest, content digest, signature, and trust metadata. |
58
- | `verifyDigest()` | Check whole-file `asset_digest`. |
59
- | `verifySignature()` | Require Ed25519 signature verification. |
60
- | `matchDomain()` | Rank candidate assets for a task string. |
61
- | `composeKDNA()` | Compose multiple assets with attribution and conflict reporting. |
21
+ ## KDNA Core v1 API
62
22
 
63
- ## Lower-level usage
23
+ Use the `./v1` entrypoint for current KDNA Core v1 tooling:
64
24
 
65
25
  ```js
66
26
  const {
67
- createKdnaAssetReader,
68
- lintDomain,
69
- validateDomainSchema,
70
- validateCrossFile
71
- } = require('@aikdna/kdna-core');
72
-
73
- // Validate a domain
74
- const dataMap = {
75
- 'KDNA_Core.json': { meta: { domain: 'my_domain' }, axioms: [...] },
76
- 'KDNA_Patterns.json': { meta: { domain: 'my_domain' }, self_check: [...] }
77
- };
78
-
79
- const lintResult = lintDomain(dataMap);
80
- const schemaResult = validateDomainSchema(dataMap, schemas);
81
- const crossResult = validateCrossFile(dataMap);
82
- ```
83
-
84
- ## API
85
-
86
- ### `createKdnaAssetReader()`
87
-
88
- Direct `.kdna` container reader. The reader opens ZIP-backed `.kdna` assets without persistent extraction and exposes:
89
-
90
- - `open(pathOrBytes)`
91
- - `listEntries(asset)`
92
- - `readEntry(asset, entryName)`
93
- - `readJson(asset, entryName)`
94
- - `readManifest(asset)`
95
- - `readDataMap(asset)`
96
- - `contentDigest(asset)`
97
- - `verify(asset, { asset_digest?, content_digest?, requireSignature? })`
98
- - `loadProfile(asset, "index" | "compact" | "scenario" | "full", options?)`
99
-
100
- Example:
101
-
102
- ```js
103
- const { createKdnaAssetReader } = require('@aikdna/kdna-core');
104
-
105
- const reader = createKdnaAssetReader();
106
- const asset = await reader.open('./writing.kdna');
107
- const manifest = await reader.readManifest(asset);
108
- const trust = await reader.verify(asset, { requireSignature: true });
109
- const loaded = await reader.loadProfile(asset, 'compact');
110
- ```
111
-
112
- The asset reader treats extraction caches as implementation details. The `.kdna` file remains the identity, install, verification, and loading object.
113
-
114
- Licensed assets can list encrypted JSON entries in `kdna.json`:
115
-
116
- ```json
117
- {
118
- "access": "licensed",
119
- "encryption": {
120
- "profile": "kdna-licensed-entry-v1",
121
- "encrypted_entries": ["KDNA_Core.json", "KDNA_Patterns.json"]
122
- }
27
+ inspect,
28
+ validate,
29
+ pack,
30
+ unpack,
31
+ loadV1,
32
+ buildChecksumsV1
33
+ } = require('@aikdna/kdna-core/v1');
34
+
35
+ const validation = validate('./asset.kdna');
36
+ if (!validation.overall_valid) {
37
+ throw new Error(validation.problems.join('\n'));
123
38
  }
124
- ```
125
-
126
- The reader never writes decrypted entries to disk. Callers provide an in-memory
127
- `decryptEntry` hook when they have already validated license activation:
128
-
129
- ```js
130
- const { createLicensedDecryptEntry } = require('@aikdna/kdna-core');
131
39
 
132
- const decryptEntry = createLicensedDecryptEntry({
133
- licenseKey: activation.license_key,
134
- machineFingerprint: activation.machine_fingerprint
40
+ const compact = loadV1('./asset.kdna', {
41
+ profile: 'compact',
42
+ as: 'prompt'
135
43
  });
136
-
137
- const loaded = await reader.loadProfile(asset, 'compact', { decryptEntry });
138
44
  ```
139
45
 
140
- The profile uses AES-256-GCM over each protected entry and derives the entry key
141
- from the license key plus machine fingerprint using `scrypt-sha256`. This is a
142
- runtime primitive, not a license activation system; callers must validate license
143
- status before passing a decrypt hook to the reader.
144
-
145
- ### `lintDomain(dataMap)`
146
- Structural linting — checks required files, field presence, unique IDs, yes/no answerable self-checks, cross-file references, and flags potentially vague axioms.
147
-
148
- Returns `{ errors: string[], warnings: string[] }`.
149
-
150
- ### `validateDomainSchema(dataMap, schemaMap)`
151
- JSON Schema validation against published schemas (KDNA_Core, KDNA_Patterns, KDNA_Scenarios, KDNA_Cases, KDNA_Reasoning, KDNA_Evolution).
46
+ ## Supported Runtime Flow
152
47
 
153
- Returns `{ errors: string[], warnings: string[] }`.
154
-
155
- ### `validateCrossFile(dataMap)`
156
- Cross-file consistency checks — ensures references between domain files are valid.
157
-
158
- Returns `{ errors: string[], warnings: string[] }`.
159
-
160
- ### `renderDomain(dataMap, options?)`
161
- Renders domain files into a structured context block using a standard template. The rendered context preserves the domain's structure as distinct, named sections suitable for agent system prompts.
48
+ ```text
49
+ v1 source directory
50
+ buildChecksumsV1
51
+ pack
52
+ → validate
53
+ loadV1
54
+ → agent/runtime context
55
+ ```
162
56
 
163
- ## Compose API (9 functions)
57
+ ## v1 Source Directory
164
58
 
165
- Multi-domain composition load multiple KDNA domains, classify which should activate for a given input, detect conflicts, and merge their judgment into a single agent context.
59
+ A v1 source directory contains:
166
60
 
167
- ### Context Composition
61
+ - `mimetype`
62
+ - `kdna.json`
63
+ - `payload.kdnab`
64
+ - `checksums.json`
168
65
 
169
- - **`composeContext(domains, options?)`** — Merge multiple loaded domains into a single context string. Conflicting axioms or banned terms from different domains are both included; the agent must report the conflict rather than silently resolve it.
66
+ ## v1 Container
170
67
 
171
- - **`composeContextWithAttribution(domains, options?)`** — Same as `composeContext`, but every axiom, misunderstanding, banned term, and self-check is prefixed with its origin domain (e.g., `[writing:axiom.axiom_problem_not_prose]`). Returns `{ context, attributionMap }`.
68
+ A v1 `.kdna` container is a zip package of the same files. `validate()` checks:
172
69
 
173
- - **`loadAndCompose(dataMaps, options?)`** — Convenience function: loads each domain from file data maps, classifies signals against input, then composes the active domains. Returns `{ domains, context, activeIndices }`.
70
+ - format
71
+ - manifest schema
72
+ - payload parseability
73
+ - checksum consistency
74
+ - load-profile contract
174
75
 
175
- ### Signal Classification
76
+ ## Load Profiles
176
77
 
177
- - **`classifySignals(input, domains)`** — Match user input against each domain's `trigger_signals`. Returns indices of matching domains. Domains with no signals defined are treated as primary (always active).
78
+ `loadV1()` supports:
178
79
 
179
- - **`classifySignalsAcrossDomains(input, domainEntries)`** — Full diagnostic version of signal classification. Returns `{ selected, excluded }` with reasons (`signal_match`, `required`, `blocked by does_not_apply_when`, `no signal match`).
80
+ - `index`
81
+ - `compact`
82
+ - `scenario`
83
+ - `full`
180
84
 
181
- ### Cluster Operations
85
+ Output formats:
182
86
 
183
- - **`loadCluster(clusterManifestPath, domainLoader)`** — Load a cluster manifest (`kdna.cluster.json`) and resolve each domain via the provided loader function. Returns `{ manifest, domains, errors }`.
87
+ - `json`
88
+ - `prompt`
184
89
 
185
- - **`detectDomainConflicts(domains)`** — Detect conflicts between loaded domains in a cluster. Currently checks for: (1) banned term collisions across domains, (2) contradictory stances (simple negation heuristic). Returns array of conflict objects with `type`, `domains`, and `description`.
90
+ ## Boundary
186
91
 
187
- - **`generateClusterTrace({ input, loadedDomains, activeDomains, conflicts })`** — Generate a judgment trace record for a cluster operation. Returns `{ input, timestamp, loaded_domains, active_domains, active_count, domains_excluded, conflicts }`.
92
+ KDNA Core v1 is content-neutral. It does not recommend assets, assign quality
93
+ badges, run a marketplace, or define a public registry. Future signature,
94
+ encryption, licensing, and entitlement work is gated outside the current v1
95
+ baseline.
188
96
 
189
- ### Utilities
97
+ ## Legacy API
190
98
 
191
- - **`composeChecks(domains)`** Merge self-check items from multiple domains into a single checklist. Each item is prefixed with its domain name so overlaps are visible.
99
+ The package root still exports compatibility APIs for older KDNA paths. New
100
+ tooling should prefer `@aikdna/kdna-core/v1`.
192
101
 
193
102
  ## License
194
103
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aikdna/kdna-core",
3
- "version": "0.11.1",
3
+ "version": "0.12.0",
4
4
  "description": "KDNA core library — load, validate, lint, and render KDNA domain judgment assets. Supports KDNA Container format (payload.kdnab via CBOR).",
5
5
  "type": "commonjs",
6
6
  "main": "src/index.js",
@@ -0,0 +1,119 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://github.com/aikdna/kdna/schema/load-plan.schema.json",
4
+ "title": "KDNA LoadPlan v1",
5
+ "description": "Machine-readable pre-load authorization and runtime planning result for KDNA Core v1 assets.",
6
+ "type": "object",
7
+ "required": [
8
+ "kdna_version",
9
+ "asset",
10
+ "access",
11
+ "access_alias",
12
+ "entitlement_profile",
13
+ "state",
14
+ "required_action",
15
+ "can_load_now",
16
+ "projection_policy",
17
+ "checks",
18
+ "issues",
19
+ "source"
20
+ ],
21
+ "additionalProperties": false,
22
+ "properties": {
23
+ "kdna_version": { "type": ["string", "null"] },
24
+ "asset": {
25
+ "type": "object",
26
+ "required": ["asset_id", "asset_uid", "title", "version", "judgment_version"],
27
+ "additionalProperties": false,
28
+ "properties": {
29
+ "asset_id": { "type": ["string", "null"] },
30
+ "asset_uid": { "type": ["string", "null"] },
31
+ "title": { "type": ["string", "null"] },
32
+ "version": { "type": ["string", "null"] },
33
+ "judgment_version": { "type": ["string", "null"] }
34
+ }
35
+ },
36
+ "access": {
37
+ "type": ["string", "null"],
38
+ "enum": ["public", "licensed", "remote", null]
39
+ },
40
+ "access_alias": { "type": ["string", "null"] },
41
+ "entitlement_profile": { "type": ["string", "null"] },
42
+ "state": {
43
+ "type": "string",
44
+ "enum": [
45
+ "ready",
46
+ "needs_password",
47
+ "needs_license",
48
+ "needs_account",
49
+ "needs_org_auth",
50
+ "needs_runtime",
51
+ "offline_grace",
52
+ "expired",
53
+ "revoked",
54
+ "invalid"
55
+ ]
56
+ },
57
+ "required_action": {
58
+ "type": "string",
59
+ "enum": [
60
+ "none",
61
+ "load",
62
+ "enter_password",
63
+ "install_receipt",
64
+ "sign_in_or_activate",
65
+ "sync",
66
+ "connect_runtime",
67
+ "migrate_legacy",
68
+ "block"
69
+ ]
70
+ },
71
+ "can_load_now": { "type": "boolean" },
72
+ "projection_policy": {
73
+ "type": "string",
74
+ "enum": ["minimal", "remote", "none"]
75
+ },
76
+ "checks": {
77
+ "type": "object",
78
+ "required": [
79
+ "format_valid",
80
+ "schema_valid",
81
+ "payload_valid",
82
+ "checksums_valid",
83
+ "load_contract_valid",
84
+ "overall_valid"
85
+ ],
86
+ "additionalProperties": false,
87
+ "properties": {
88
+ "format_valid": { "type": "boolean" },
89
+ "schema_valid": { "type": "boolean" },
90
+ "payload_valid": { "type": "boolean" },
91
+ "checksums_valid": { "type": "boolean" },
92
+ "load_contract_valid": { "type": "boolean" },
93
+ "overall_valid": { "type": "boolean" }
94
+ }
95
+ },
96
+ "issues": {
97
+ "type": "array",
98
+ "items": {
99
+ "type": "object",
100
+ "required": ["code", "severity", "message"],
101
+ "additionalProperties": false,
102
+ "properties": {
103
+ "code": { "type": "string", "pattern": "^KDNA_[A-Z0-9_]+$" },
104
+ "severity": { "type": "string", "enum": ["info", "warning", "blocking"] },
105
+ "message": { "type": "string" }
106
+ }
107
+ }
108
+ },
109
+ "source": {
110
+ "type": "object",
111
+ "required": ["kind", "path"],
112
+ "additionalProperties": false,
113
+ "properties": {
114
+ "kind": { "type": ["string", "null"], "enum": ["dir", "file", null] },
115
+ "path": { "type": "string" }
116
+ }
117
+ }
118
+ }
119
+ }
package/src/types.d.ts CHANGED
@@ -507,6 +507,56 @@ export function isV1SourceDir(inputPath: string): boolean;
507
507
  export function detectContainerFormat(inputPath: string): 'v1' | 'v2' | null;
508
508
  export function inspect(inputPath: string, options?: Record<string, any>): Record<string, any>;
509
509
  export function validate(inputPath: string, options?: Record<string, any>): Record<string, any>;
510
+ export interface KDNAV1LoadPlanIssue {
511
+ code: string;
512
+ severity: 'info' | 'warning' | 'blocking' | string;
513
+ message: string;
514
+ }
515
+ export interface KDNAV1LoadPlan {
516
+ kdna_version: string | null;
517
+ asset: {
518
+ asset_id: string | null;
519
+ asset_uid: string | null;
520
+ title: string | null;
521
+ version: string | null;
522
+ judgment_version: string | null;
523
+ };
524
+ access: 'public' | 'licensed' | 'remote' | string | null;
525
+ access_alias: string | null;
526
+ entitlement_profile: string | null;
527
+ state:
528
+ | 'ready'
529
+ | 'needs_password'
530
+ | 'needs_license'
531
+ | 'needs_account'
532
+ | 'needs_org_auth'
533
+ | 'needs_runtime'
534
+ | 'offline_grace'
535
+ | 'expired'
536
+ | 'revoked'
537
+ | 'invalid'
538
+ | string;
539
+ required_action:
540
+ | 'none'
541
+ | 'load'
542
+ | 'enter_password'
543
+ | 'install_receipt'
544
+ | 'sign_in_or_activate'
545
+ | 'sync'
546
+ | 'connect_runtime'
547
+ | 'migrate_legacy'
548
+ | 'block'
549
+ | string;
550
+ can_load_now: boolean;
551
+ projection_policy: 'minimal' | 'remote' | 'none' | string;
552
+ checks: Record<string, boolean>;
553
+ issues: KDNAV1LoadPlanIssue[];
554
+ source: {
555
+ kind: 'dir' | 'file' | string | null;
556
+ path: string;
557
+ };
558
+ }
559
+ export function planLoad(inputPath: string, options?: { password?: string; hasPassword?: boolean; entitlement?: Record<string, any> }): KDNAV1LoadPlan;
510
560
  export function buildChecksumsV1(sourceDir: string): KDNAV1Checksums;
511
561
  export function pack(sourceDir: string, outputPath: string): void;
512
562
  export function unpack(inputPath: string, outputDir: string): void;
package/src/v1/index.js CHANGED
@@ -750,6 +750,330 @@ function validate(inputPath, opts = {}) {
750
750
  return runValidate(v1);
751
751
  }
752
752
 
753
+ function normalizeAccess(access) {
754
+ const value = access || 'public';
755
+ if (value === 'open') return { access: 'public', alias: value };
756
+ if (value === 'protected') return { access: 'licensed', alias: value };
757
+ if (value === 'runtime') return { access: 'remote', alias: value };
758
+ return { access: value, alias: null };
759
+ }
760
+
761
+ function inferEntitlementProfile(manifest) {
762
+ if (manifest.entitlement && typeof manifest.entitlement.profile === 'string') {
763
+ return manifest.entitlement.profile;
764
+ }
765
+ if (manifest.encryption && manifest.encryption.profile === 'kdna-password-protected-v1') {
766
+ return 'password';
767
+ }
768
+ if (manifest.access === 'protected') return 'password';
769
+ return null;
770
+ }
771
+
772
+ function buildLoadPlanIssue(code, severity, message) {
773
+ return { code, severity, message };
774
+ }
775
+
776
+ function validationProblemCode(problem) {
777
+ if (/checksums?:/i.test(problem)) return 'KDNA_INTEGRITY_DIGEST_FAILED';
778
+ if (/signature/i.test(problem)) return 'KDNA_INTEGRITY_SIGNATURE_FAILED';
779
+ if (/payload:/i.test(problem)) return 'KDNA_FORMAT_INVALID';
780
+ if (/manifest:/i.test(problem)) return 'KDNA_FORMAT_INVALID';
781
+ if (/load_contract:/i.test(problem)) return 'KDNA_FORMAT_INVALID';
782
+ return 'KDNA_FORMAT_INVALID';
783
+ }
784
+
785
+ function finalizeLoadPlan(plan) {
786
+ assertNoForbiddenTerms(plan);
787
+ return plan;
788
+ }
789
+
790
+ function baseLoadPlan(inputPath, v1, validation) {
791
+ const manifest = v1.manifest;
792
+ const accessInfo = normalizeAccess(manifest.access);
793
+ const entitlementProfile = inferEntitlementProfile(manifest);
794
+ const asset = {
795
+ asset_id: manifest.asset_id || null,
796
+ asset_uid: manifest.asset_uid || null,
797
+ title: manifest.title || null,
798
+ version: manifest.version || null,
799
+ judgment_version: manifest.judgment_version || null,
800
+ };
801
+
802
+ const plan = {
803
+ kdna_version: manifest.kdna_version || null,
804
+ asset,
805
+ access: accessInfo.access,
806
+ access_alias: accessInfo.alias,
807
+ entitlement_profile: entitlementProfile,
808
+ state: 'invalid',
809
+ required_action: 'block',
810
+ can_load_now: false,
811
+ projection_policy: 'none',
812
+ checks: {
813
+ format_valid: validation.format_valid,
814
+ schema_valid: validation.schema_valid,
815
+ payload_valid: validation.payload_valid,
816
+ checksums_valid: validation.checksums_valid,
817
+ load_contract_valid: validation.load_contract_valid,
818
+ overall_valid: validation.overall_valid,
819
+ },
820
+ issues: [],
821
+ source: {
822
+ kind: v1.kind,
823
+ path: path.resolve(inputPath),
824
+ },
825
+ };
826
+
827
+ if (accessInfo.alias) {
828
+ plan.issues.push(buildLoadPlanIssue(
829
+ 'KDNA_AUTH_ACCESS_ALIAS',
830
+ 'info',
831
+ `Access value "${accessInfo.alias}" is treated as "${accessInfo.access}".`,
832
+ ));
833
+ }
834
+
835
+ return plan;
836
+ }
837
+
838
+ /**
839
+ * Plan a KDNA v1 runtime load without decrypting or emitting judgment content.
840
+ * Product consumers such as Chat should render authorization UI from this
841
+ * result instead of parsing manifest fields directly.
842
+ */
843
+ function planLoad(inputPath, opts = {}) {
844
+ let v1;
845
+ try {
846
+ v1 = readV1Layout(path.resolve(inputPath));
847
+ } catch (e) {
848
+ return finalizeLoadPlan({
849
+ kdna_version: null,
850
+ asset: {
851
+ asset_id: null,
852
+ asset_uid: null,
853
+ title: null,
854
+ version: null,
855
+ judgment_version: null,
856
+ },
857
+ access: null,
858
+ access_alias: null,
859
+ entitlement_profile: null,
860
+ state: 'invalid',
861
+ required_action: 'block',
862
+ can_load_now: false,
863
+ projection_policy: 'none',
864
+ checks: {
865
+ format_valid: false,
866
+ schema_valid: false,
867
+ payload_valid: false,
868
+ checksums_valid: false,
869
+ load_contract_valid: false,
870
+ overall_valid: false,
871
+ },
872
+ issues: [
873
+ buildLoadPlanIssue('KDNA_FORMAT_INVALID', 'blocking', e.message),
874
+ ],
875
+ source: {
876
+ kind: null,
877
+ path: path.resolve(inputPath),
878
+ },
879
+ });
880
+ }
881
+
882
+ const validation = runValidate(v1);
883
+ const plan = baseLoadPlan(inputPath, v1, validation);
884
+
885
+ if (!validation.overall_valid) {
886
+ plan.state = 'invalid';
887
+ plan.required_action = 'block';
888
+ plan.can_load_now = false;
889
+ plan.projection_policy = 'none';
890
+ for (const problem of validation.problems) {
891
+ plan.issues.push(buildLoadPlanIssue(validationProblemCode(problem), 'blocking', problem));
892
+ }
893
+ return finalizeLoadPlan(plan);
894
+ }
895
+
896
+ const manifest = v1.manifest;
897
+ const payloadDeclaredEncrypted =
898
+ manifest.payload && manifest.payload.encrypted === true;
899
+ const encryptedEntries = Array.isArray(manifest.encryption && manifest.encryption.encrypted_entries)
900
+ ? manifest.encryption.encrypted_entries
901
+ : [];
902
+ const hasEncryptedPayload = payloadDeclaredEncrypted || encryptedEntries.length > 0;
903
+
904
+ if (!['public', 'licensed', 'remote'].includes(plan.access)) {
905
+ const unknownAccess = plan.access;
906
+ plan.access = null;
907
+ plan.state = 'invalid';
908
+ plan.required_action = 'block';
909
+ plan.issues.push(buildLoadPlanIssue(
910
+ 'KDNA_ACCESS_MODE_UNKNOWN',
911
+ 'blocking',
912
+ `Unknown access value "${unknownAccess}".`,
913
+ ));
914
+ return finalizeLoadPlan(plan);
915
+ }
916
+
917
+ if (plan.access === 'remote') {
918
+ plan.state = 'needs_runtime';
919
+ plan.required_action = 'connect_runtime';
920
+ plan.can_load_now = false;
921
+ plan.projection_policy = 'remote';
922
+ plan.issues.push(buildLoadPlanIssue(
923
+ 'KDNA_AUTH_REMOTE_RUNTIME_REQUIRED',
924
+ 'blocking',
925
+ 'Remote assets require a runtime projection endpoint.',
926
+ ));
927
+ return finalizeLoadPlan(plan);
928
+ }
929
+
930
+ if (plan.access === 'licensed') {
931
+ const knownProfiles = new Set([
932
+ 'password',
933
+ 'local_receipt',
934
+ 'account',
935
+ 'org',
936
+ 'purchase_receipt',
937
+ 'device_bound',
938
+ ]);
939
+ if (plan.entitlement_profile && !knownProfiles.has(plan.entitlement_profile)) {
940
+ plan.state = 'invalid';
941
+ plan.required_action = 'block';
942
+ plan.can_load_now = false;
943
+ plan.projection_policy = 'none';
944
+ plan.issues.push(buildLoadPlanIssue(
945
+ 'KDNA_ENTITLEMENT_PROFILE_UNKNOWN',
946
+ 'blocking',
947
+ `Unknown entitlement profile "${plan.entitlement_profile}".`,
948
+ ));
949
+ return finalizeLoadPlan(plan);
950
+ }
951
+
952
+ if (plan.entitlement_profile === 'password') {
953
+ if (opts.password || opts.hasPassword === true) {
954
+ plan.state = 'ready';
955
+ plan.required_action = 'load';
956
+ plan.can_load_now = true;
957
+ plan.projection_policy = 'minimal';
958
+ } else {
959
+ plan.state = 'needs_password';
960
+ plan.required_action = 'enter_password';
961
+ plan.can_load_now = false;
962
+ plan.projection_policy = 'none';
963
+ plan.issues.push(buildLoadPlanIssue(
964
+ 'KDNA_AUTH_PASSWORD_REQUIRED',
965
+ 'blocking',
966
+ 'A password is required before this asset can be loaded.',
967
+ ));
968
+ }
969
+ return finalizeLoadPlan(plan);
970
+ }
971
+
972
+ if (plan.entitlement_profile === 'account') {
973
+ plan.state = 'needs_account';
974
+ plan.required_action = 'sign_in_or_activate';
975
+ plan.can_load_now = false;
976
+ plan.projection_policy = 'none';
977
+ plan.issues.push(buildLoadPlanIssue(
978
+ 'KDNA_AUTH_ACCOUNT_REQUIRED',
979
+ 'blocking',
980
+ 'Account authorization is required before this asset can be loaded.',
981
+ ));
982
+ return finalizeLoadPlan(plan);
983
+ }
984
+
985
+ if (plan.entitlement_profile === 'org') {
986
+ plan.state = 'needs_org_auth';
987
+ plan.required_action = 'sign_in_or_activate';
988
+ plan.can_load_now = false;
989
+ plan.projection_policy = 'none';
990
+ plan.issues.push(buildLoadPlanIssue(
991
+ 'KDNA_AUTH_ORG_REQUIRED',
992
+ 'blocking',
993
+ 'Organization authorization is required before this asset can be loaded.',
994
+ ));
995
+ return finalizeLoadPlan(plan);
996
+ }
997
+
998
+ if (opts.entitlement && opts.entitlement.status === 'active') {
999
+ plan.state = 'ready';
1000
+ plan.required_action = 'load';
1001
+ plan.can_load_now = true;
1002
+ plan.projection_policy = 'minimal';
1003
+ return finalizeLoadPlan(plan);
1004
+ }
1005
+
1006
+ if (opts.entitlement && opts.entitlement.status === 'expired') {
1007
+ plan.state = 'expired';
1008
+ plan.required_action = 'sync';
1009
+ plan.can_load_now = false;
1010
+ plan.projection_policy = 'none';
1011
+ plan.issues.push(buildLoadPlanIssue(
1012
+ 'KDNA_AUTH_EXPIRED',
1013
+ 'blocking',
1014
+ 'The entitlement is expired.',
1015
+ ));
1016
+ return finalizeLoadPlan(plan);
1017
+ }
1018
+
1019
+ if (opts.entitlement && opts.entitlement.status === 'revoked') {
1020
+ plan.state = 'revoked';
1021
+ plan.required_action = 'block';
1022
+ plan.can_load_now = false;
1023
+ plan.projection_policy = 'none';
1024
+ plan.issues.push(buildLoadPlanIssue(
1025
+ 'KDNA_AUTH_REVOKED',
1026
+ 'blocking',
1027
+ 'The entitlement has been revoked.',
1028
+ ));
1029
+ return finalizeLoadPlan(plan);
1030
+ }
1031
+
1032
+ if (opts.entitlement && opts.entitlement.status === 'offline_grace') {
1033
+ plan.state = 'offline_grace';
1034
+ plan.required_action = 'sync';
1035
+ plan.can_load_now = true;
1036
+ plan.projection_policy = 'minimal';
1037
+ plan.issues.push(buildLoadPlanIssue(
1038
+ 'KDNA_AUTH_OFFLINE_GRACE_ACTIVE',
1039
+ 'warning',
1040
+ 'The entitlement can load during offline grace but must sync before grace expires.',
1041
+ ));
1042
+ return finalizeLoadPlan(plan);
1043
+ }
1044
+
1045
+ plan.state = 'needs_license';
1046
+ plan.required_action = plan.entitlement_profile === 'local_receipt' ? 'install_receipt' : 'sign_in_or_activate';
1047
+ plan.can_load_now = false;
1048
+ plan.projection_policy = 'none';
1049
+ plan.issues.push(buildLoadPlanIssue(
1050
+ 'KDNA_AUTH_ENTITLEMENT_REQUIRED',
1051
+ 'blocking',
1052
+ 'A valid entitlement is required before this asset can be loaded.',
1053
+ ));
1054
+ return finalizeLoadPlan(plan);
1055
+ }
1056
+
1057
+ if (hasEncryptedPayload) {
1058
+ plan.state = 'invalid';
1059
+ plan.required_action = 'block';
1060
+ plan.can_load_now = false;
1061
+ plan.projection_policy = 'none';
1062
+ plan.issues.push(buildLoadPlanIssue(
1063
+ 'KDNA_CRYPTO_PROFILE_UNSUPPORTED',
1064
+ 'blocking',
1065
+ 'Encrypted entries require licensed access.',
1066
+ ));
1067
+ return finalizeLoadPlan(plan);
1068
+ }
1069
+
1070
+ plan.state = 'ready';
1071
+ plan.required_action = 'load';
1072
+ plan.can_load_now = true;
1073
+ plan.projection_policy = 'minimal';
1074
+ return finalizeLoadPlan(plan);
1075
+ }
1076
+
753
1077
  function assertNoForbiddenTerms(obj) {
754
1078
  const seen = new Set();
755
1079
  function walk(o) {
@@ -781,6 +1105,7 @@ module.exports = {
781
1105
  readV1Layout,
782
1106
  inspect,
783
1107
  validate,
1108
+ planLoad,
784
1109
  buildChecksumsV1,
785
1110
  pack,
786
1111
  unpack,
package/src/v1/index.mjs CHANGED
@@ -9,6 +9,7 @@ export const detectContainerFormat = v1.detectContainerFormat;
9
9
  export const readV1Layout = v1.readV1Layout;
10
10
  export const inspect = v1.inspect;
11
11
  export const validate = v1.validate;
12
+ export const planLoad = v1.planLoad;
12
13
  export const buildChecksumsV1 = v1.buildChecksumsV1;
13
14
  export const pack = v1.pack;
14
15
  export const unpack = v1.unpack;