@aikdna/kdna-core 0.11.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.
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.0",
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
@@ -484,6 +484,85 @@ export function matchDomain(input: string, candidates: Array<KDNAAssetInput | KD
484
484
  export function matchDomainSync(input: string, candidates: Array<KDNAAssetInput | KDNAInspectResult>, options?: KdnaDecryptOptions): KDNAMatchResult[];
485
485
  export function composeKDNA(inputs: KDNAAssetInput[], options?: { input?: string; profile?: 'compact' | 'scenario' | 'full' | string; separator?: string } & KdnaDecryptOptions): Promise<KDNAComposeResult>;
486
486
 
487
+ // KDNA Core v1 — source directory / container API
488
+ export const MIMETYPE: string;
489
+ export const MIMETYPE_V1: string;
490
+ export const MIMETYPE_V2: string;
491
+ export const V1_REQUIRED_DIR_ENTRIES: string[];
492
+
493
+ export interface KDNAV1ChecksumEntry {
494
+ algorithm: 'sha256';
495
+ value: string;
496
+ }
497
+
498
+ export interface KDNAV1Checksums {
499
+ algorithm: 'sha256';
500
+ manifest_digest: string;
501
+ payload_digest: string;
502
+ asset_digest: string;
503
+ entries: Record<string, KDNAV1ChecksumEntry>;
504
+ }
505
+
506
+ export function isV1SourceDir(inputPath: string): boolean;
507
+ export function detectContainerFormat(inputPath: string): 'v1' | 'v2' | null;
508
+ export function inspect(inputPath: string, options?: Record<string, any>): Record<string, any>;
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;
560
+ export function buildChecksumsV1(sourceDir: string): KDNAV1Checksums;
561
+ export function pack(sourceDir: string, outputPath: string): void;
562
+ export function unpack(inputPath: string, outputDir: string): void;
563
+ export function loadV1(inputPath: string, options?: { profile?: 'index' | 'compact' | 'scenario' | 'full' | string; as?: 'json' | 'prompt' | string }): Record<string, any>;
564
+ export const FORBIDDEN_OUTPUT_TERMS: readonly string[];
565
+
487
566
  // Lint
488
567
  export function lintDomain(dataMap: KDNAFileDataMap): LintResult;
489
568
 
package/src/v1/index.js CHANGED
@@ -580,6 +580,42 @@ function finalizeValidate(result, problems) {
580
580
  return result;
581
581
  }
582
582
 
583
+ function digestEntry(sourceDir, entry) {
584
+ const entryPath = path.join(sourceDir, entry);
585
+ if (!fs.existsSync(entryPath)) {
586
+ throw new Error(`cannot build checksums: missing required entry ${entry}`);
587
+ }
588
+ const bytes = fs.readFileSync(entryPath);
589
+ return {
590
+ algorithm: 'sha256',
591
+ value: crypto.createHash('sha256').update(bytes).digest('hex'),
592
+ };
593
+ }
594
+
595
+ function buildChecksumsV1(sourceDir) {
596
+ const absSrc = path.resolve(sourceDir);
597
+ if (!fs.existsSync(absSrc) || !fs.statSync(absSrc).isDirectory()) {
598
+ throw new Error(`not a directory: ${absSrc}`);
599
+ }
600
+
601
+ const entries = {
602
+ 'kdna.json': digestEntry(absSrc, 'kdna.json'),
603
+ 'payload.kdnab': digestEntry(absSrc, 'payload.kdnab'),
604
+ };
605
+ const combined = Object.keys(entries)
606
+ .sort()
607
+ .map((name) => `${name}:${entries[name].value}`)
608
+ .join('\n');
609
+
610
+ return {
611
+ algorithm: 'sha256',
612
+ manifest_digest: `sha256:${entries['kdna.json'].value}`,
613
+ payload_digest: `sha256:${entries['payload.kdnab'].value}`,
614
+ asset_digest: `sha256:${crypto.createHash('sha256').update(combined).digest('hex')}`,
615
+ entries,
616
+ };
617
+ }
618
+
583
619
  // ─── pack ──────────────────────────────────────────────────────────────
584
620
 
585
621
  /**
@@ -714,6 +750,330 @@ function validate(inputPath, opts = {}) {
714
750
  return runValidate(v1);
715
751
  }
716
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
+
717
1077
  function assertNoForbiddenTerms(obj) {
718
1078
  const seen = new Set();
719
1079
  function walk(o) {
@@ -745,6 +1105,8 @@ module.exports = {
745
1105
  readV1Layout,
746
1106
  inspect,
747
1107
  validate,
1108
+ planLoad,
1109
+ buildChecksumsV1,
748
1110
  pack,
749
1111
  unpack,
750
1112
  loadV1,
@@ -753,6 +1115,35 @@ module.exports = {
753
1115
 
754
1116
  // ─── loadV1 — v1 runtime loading / load contract ──────────────────────
755
1117
 
1118
+ function renderPromptItem(item) {
1119
+ if (item === undefined || item === null) return '';
1120
+ if (typeof item === 'string') return item;
1121
+ if (typeof item !== 'object') return String(item);
1122
+
1123
+ if (item.type === 'axiom_applicability' && item.one_sentence) {
1124
+ const parts = [item.one_sentence];
1125
+ if (Array.isArray(item.does_not_apply_when) && item.does_not_apply_when.length) {
1126
+ parts.push(`does not apply when: ${item.does_not_apply_when.slice(0, 2).join('; ')}`);
1127
+ }
1128
+ if (item.failure_risk) parts.push(`failure risk: ${item.failure_risk}`);
1129
+ return parts.join(' — ');
1130
+ }
1131
+ if (item.boundary && item.one_sentence) return `${item.one_sentence}: ${item.boundary}`;
1132
+ if (item.stance) return item.stance;
1133
+ if (item.statement) return item.statement;
1134
+ if (item.term && item.definition) return `${item.term}: ${item.definition}`;
1135
+ if (item.term && item.why) return `${item.term}: ${item.why}`;
1136
+ if (item.wrong && item.correct) return `${item.wrong} -> ${item.correct}`;
1137
+ if (item.mode && item.correct) return `${item.mode} -> ${item.correct}`;
1138
+ if (item.name && item.description) return `${item.name}: ${item.description}`;
1139
+ if (item.name) return item.name;
1140
+ if (item.one_sentence) return item.one_sentence;
1141
+ if (item.essence) return item.essence;
1142
+ if (item.question) return item.question;
1143
+ if (item.id) return item.id;
1144
+ return JSON.stringify(item);
1145
+ }
1146
+
756
1147
  function loadV1(inputPath, opts = {}) {
757
1148
  const v1 = readV1Layout(path.resolve(inputPath));
758
1149
  const m = v1.manifest;
@@ -820,11 +1211,11 @@ function loadV1(inputPath, opts = {}) {
820
1211
  text += 'Profile: ' + result.profile + '\n';
821
1212
  if (result.max_tokens_hint) text += 'Max tokens hint: ' + result.max_tokens_hint + '\n';
822
1213
  if (c.highest_question) text += 'Highest question:\n' + c.highest_question + '\n';
823
- if (c.axioms && c.axioms.length) text += 'Axioms:\n' + c.axioms.map((a) => '- ' + (typeof a === 'string' ? a : JSON.stringify(a))).join('\n') + '\n';
824
- if (c.boundaries && c.boundaries.length) text += 'Boundaries:\n' + c.boundaries.map((b) => '- ' + (typeof b === 'string' ? b : JSON.stringify(b))).join('\n') + '\n';
825
- if (c.self_checks && c.self_checks.length) text += 'Self-checks:\n' + c.self_checks.map((s) => '- ' + (typeof s === 'string' ? s : JSON.stringify(s))).join('\n') + '\n';
826
- if (c.failure_modes && c.failure_modes.length) text += 'Failure modes:\n' + c.failure_modes.map((f) => '- ' + (f.mode || f)).join('\n') + '\n';
827
- if (c.patterns && c.patterns.length) text += 'Patterns:\n' + c.patterns.map((p) => '- ' + (p.name || p)).join('\n') + '\n';
1214
+ if (c.axioms && c.axioms.length) text += 'Axioms:\n' + c.axioms.map((a) => '- ' + renderPromptItem(a)).join('\n') + '\n';
1215
+ if (c.boundaries && c.boundaries.length) text += 'Boundaries:\n' + c.boundaries.map((b) => '- ' + renderPromptItem(b)).join('\n') + '\n';
1216
+ if (c.self_checks && c.self_checks.length) text += 'Self-checks:\n' + c.self_checks.map((s) => '- ' + renderPromptItem(s)).join('\n') + '\n';
1217
+ if (c.failure_modes && c.failure_modes.length) text += 'Failure modes:\n' + c.failure_modes.map((f) => '- ' + renderPromptItem(f)).join('\n') + '\n';
1218
+ if (c.patterns && c.patterns.length) text += 'Patterns:\n' + c.patterns.map((p) => '- ' + renderPromptItem(p)).join('\n') + '\n';
828
1219
  if (c.note) text += 'Note: ' + c.note + '\n';
829
1220
  return { status: result.status, profile: result.profile, text: text.trim() };
830
1221
  }
@@ -0,0 +1,19 @@
1
+ import v1 from './index.js';
2
+
3
+ export const MIMETYPE = v1.MIMETYPE;
4
+ export const MIMETYPE_V1 = v1.MIMETYPE_V1;
5
+ export const MIMETYPE_V2 = v1.MIMETYPE_V2;
6
+ export const V1_REQUIRED_DIR_ENTRIES = v1.V1_REQUIRED_DIR_ENTRIES;
7
+ export const isV1SourceDir = v1.isV1SourceDir;
8
+ export const detectContainerFormat = v1.detectContainerFormat;
9
+ export const readV1Layout = v1.readV1Layout;
10
+ export const inspect = v1.inspect;
11
+ export const validate = v1.validate;
12
+ export const planLoad = v1.planLoad;
13
+ export const buildChecksumsV1 = v1.buildChecksumsV1;
14
+ export const pack = v1.pack;
15
+ export const unpack = v1.unpack;
16
+ export const loadV1 = v1.loadV1;
17
+ export const FORBIDDEN_OUTPUT_TERMS = v1.FORBIDDEN_OUTPUT_TERMS;
18
+
19
+ export default v1;