@principles/install-layout 0.3.1 → 0.4.1

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/dist/index.d.ts CHANGED
@@ -6,7 +6,23 @@ export type InstallHost = (typeof INSTALL_HOSTS)[number];
6
6
  export interface InstallManifest {
7
7
  layoutVersion: typeof INSTALL_LAYOUT_VERSION;
8
8
  mode: InstallLayoutMode;
9
+ /**
10
+ * Hosts THIS reader version recognizes. Host strings the reader does not
11
+ * know are NOT here — they land in `unknownHosts` so an install registered
12
+ * with a newer host keeps resolving instead of being rejected as malformed.
13
+ */
9
14
  hosts: InstallHost[];
15
+ /**
16
+ * Host strings present in the persisted `hosts` array that this reader
17
+ * version does not know (forward compatibility, 2026-10-16). A NEWER
18
+ * installer may register a host an OLDER reader never heard of; that is a
19
+ * compatibility warning, never "manifest malformed" — treating it as one
20
+ * made every pre-WorkBuddy Companion report `console_runtime_not_installed`
21
+ * against a healthy 3-host install and mass-stop its workers. Fail-closed
22
+ * stays reserved for structural damage: wrong layoutVersion, non-array or
23
+ * non-string hosts, bad mode, illegal workspace paths.
24
+ */
25
+ unknownHosts?: readonly string[];
10
26
  /**
11
27
  * Canonical absolute paths of Workspaces the install serves (PRI-624 Slice C).
12
28
  * Optional so pre-PRI-624 manifests stay valid; always normalized to
@@ -16,6 +32,11 @@ export interface InstallManifest {
16
32
  */
17
33
  workspaces?: readonly string[];
18
34
  }
35
+ /** Structured forward-compatibility observation from manifest parsing (rc-9). */
36
+ export interface InstallManifestWarning {
37
+ code: 'install_manifest_unknown_host';
38
+ host: string;
39
+ }
19
40
  export interface InstallLayoutPaths {
20
41
  homeDir: string;
21
42
  pdDir: string;
@@ -51,6 +72,14 @@ export interface InstallLayoutPaths {
51
72
  * the same way the Codex hook resolves the canonical runtime.
52
73
  */
53
74
  workbuddyAdapterDir: string;
75
+ /**
76
+ * Gate 4 (PRI-1071): the shared read-only MCP server component (payload dir
77
+ * pd-mcp-server/, npm name @principles/pd-mcp-server). This is the Install
78
+ * Layout SSoT for the canonical server entry OpenClaw/WorkBuddy launchers
79
+ * bind to; Codex keeps its plugin-data runtime layout and records that dir
80
+ * in its own binding.
81
+ */
82
+ mcpServerDir: string;
54
83
  }
55
84
  export interface LayoutResolution {
56
85
  mode: InstallLayoutMode | 'missing';
@@ -58,6 +87,8 @@ export interface LayoutResolution {
58
87
  manifest?: InstallManifest;
59
88
  reason?: string;
60
89
  nextAction?: string;
90
+ /** Forward-compatibility warnings surfaced while parsing the manifest. */
91
+ warnings?: readonly InstallManifestWarning[];
61
92
  }
62
93
  export interface ResolveInstallLayoutOptions {
63
94
  homeDir: string;
@@ -65,13 +96,36 @@ export interface ResolveInstallLayoutOptions {
65
96
  canonicalRuntimeExists: boolean;
66
97
  legacyExtensionExists: boolean;
67
98
  }
68
- /** Merge one Workspace into a manifest's workspace list (installer write path). Idempotent by canonical path. */
99
+ /**
100
+ * Merge one Workspace into a manifest's workspace list (installer write path). Idempotent by canonical path. */
69
101
  export declare function mergeInstallManifestWorkspaces(current: InstallManifest | undefined, workspaceDir: string): string[];
102
+ /**
103
+ * The `hosts` array a manifest WRITER must persist: every registration the
104
+ * reader recognized plus every unknown one, deduped, known hosts first.
105
+ * Writers that rebuild `hosts` from `manifest.hosts` alone silently DELETE a
106
+ * newer host's registration — the write-side twin of the read-side
107
+ * unknown-host brick (2026-10-16). `undefined` yields `[]` so the installer's
108
+ * fresh-install path keeps working unchanged.
109
+ */
110
+ export declare function installManifestHostsForPersist(manifest: InstallManifest | undefined): string[];
111
+ /**
112
+ * Parse the install manifest. Structural damage fails closed with `error`;
113
+ * a host string this reader version does not know is a `warnings` entry and
114
+ * the manifest still parses (forward compatibility — see `InstallManifest.unknownHosts`).
115
+ */
70
116
  export declare function parseInstallManifest(value: unknown): {
71
117
  manifest?: InstallManifest;
118
+ warnings?: readonly InstallManifestWarning[];
72
119
  error?: string;
73
120
  };
74
121
  export declare function getInstallLayoutPaths(homeDir: string): InstallLayoutPaths;
122
+ /**
123
+ * Canonical installed MCP server entry for the shared runtime layout
124
+ * (~/.pd/runtime/pd-mcp-server/dist/index.js). Launchers, binding writers and
125
+ * health diagnostics derive the path from this single owner instead of each
126
+ * repeating the segment list.
127
+ */
128
+ export declare function getMcpServerEntry(paths: InstallLayoutPaths): string;
75
129
  /**
76
130
  * WorkBuddy governance workspace (SPEC workbuddy-host-adapter §4.1 OD-07,
77
131
  * frozen by Phase 0 probe P06 2026-10-12): an explicit user-level directory
package/dist/index.js CHANGED
@@ -8,6 +8,9 @@ function isRecord(value) {
8
8
  function isHost(value) {
9
9
  return typeof value === 'string' && INSTALL_HOSTS.includes(value);
10
10
  }
11
+ function isNonEmptyString(value) {
12
+ return typeof value === 'string' && value.length > 0;
13
+ }
11
14
  /** Workspace entries must be non-empty absolute paths — the Companion spawns per-Workspace workers keyed by canonical path. */
12
15
  function parseWorkspaceList(value) {
13
16
  if (value === undefined)
@@ -25,7 +28,8 @@ function parseWorkspaceList(value) {
25
28
  }
26
29
  return { workspaces: resolved };
27
30
  }
28
- /** Merge one Workspace into a manifest's workspace list (installer write path). Idempotent by canonical path. */
31
+ /**
32
+ * Merge one Workspace into a manifest's workspace list (installer write path). Idempotent by canonical path. */
29
33
  export function mergeInstallManifestWorkspaces(current, workspaceDir) {
30
34
  const canonical = path.resolve(workspaceDir);
31
35
  const resolved = [];
@@ -38,6 +42,22 @@ export function mergeInstallManifestWorkspaces(current, workspaceDir) {
38
42
  resolved.push(canonical);
39
43
  return resolved;
40
44
  }
45
+ /**
46
+ * The `hosts` array a manifest WRITER must persist: every registration the
47
+ * reader recognized plus every unknown one, deduped, known hosts first.
48
+ * Writers that rebuild `hosts` from `manifest.hosts` alone silently DELETE a
49
+ * newer host's registration — the write-side twin of the read-side
50
+ * unknown-host brick (2026-10-16). `undefined` yields `[]` so the installer's
51
+ * fresh-install path keeps working unchanged.
52
+ */
53
+ export function installManifestHostsForPersist(manifest) {
54
+ return [...new Set([...(manifest?.hosts ?? []), ...(manifest?.unknownHosts ?? [])])];
55
+ }
56
+ /**
57
+ * Parse the install manifest. Structural damage fails closed with `error`;
58
+ * a host string this reader version does not know is a `warnings` entry and
59
+ * the manifest still parses (forward compatibility — see `InstallManifest.unknownHosts`).
60
+ */
41
61
  export function parseInstallManifest(value) {
42
62
  if (!isRecord(value))
43
63
  return { error: 'install_manifest_malformed: expected an object' };
@@ -47,14 +67,55 @@ export function parseInstallManifest(value) {
47
67
  if (value.mode !== 'canonical' && value.mode !== 'legacy') {
48
68
  return { error: 'install_manifest_malformed: mode must be canonical or legacy' };
49
69
  }
50
- if (!Array.isArray(value.hosts) || value.hosts.length === 0 || !value.hosts.every(isHost)) {
51
- return { error: 'install_manifest_malformed: hosts must contain codex, openclaw and/or workbuddy' };
70
+ // Host entries must be non-empty strings; WHICH strings are valid hosts is
71
+ // version-relative knowledge, so unknown strings warn instead of failing.
72
+ if (!Array.isArray(value.hosts) || !value.hosts.every(isNonEmptyString)) {
73
+ return { error: 'install_manifest_malformed: hosts must be an array of host name strings' };
74
+ }
75
+ // Idempotency: the parser must re-read its own output. An all-unknown-hosts
76
+ // manifest parses to hosts: [] + unknownHosts, so the optional persisted
77
+ // `unknownHosts` field is accepted on input too (installManifestHostsForPersist
78
+ // reunifies them on write; writers persist the union into `hosts`).
79
+ let carriedUnknownHosts = [];
80
+ if (value.unknownHosts !== undefined) {
81
+ if (!Array.isArray(value.unknownHosts) || !value.unknownHosts.every(isNonEmptyString)) {
82
+ return { error: 'install_manifest_malformed: unknownHosts must be an array of host name strings' };
83
+ }
84
+ carriedUnknownHosts = value.unknownHosts.filter((host) => !isHost(host));
85
+ }
86
+ if (value.hosts.length === 0 && carriedUnknownHosts.length === 0) {
87
+ return { error: 'install_manifest_malformed: hosts must contain at least one host' };
88
+ }
89
+ const hosts = [];
90
+ const unknownHosts = [];
91
+ const warnings = [];
92
+ for (const host of [...new Set(value.hosts)]) {
93
+ if (isHost(host))
94
+ hosts.push(host);
95
+ else {
96
+ unknownHosts.push(host);
97
+ warnings.push({ code: 'install_manifest_unknown_host', host });
98
+ }
99
+ }
100
+ for (const host of carriedUnknownHosts) {
101
+ if (!unknownHosts.includes(host)) {
102
+ unknownHosts.push(host);
103
+ warnings.push({ code: 'install_manifest_unknown_host', host });
104
+ }
52
105
  }
53
- const hosts = [...new Set(value.hosts)];
54
106
  const workspaces = parseWorkspaceList(value.workspaces);
55
107
  if (workspaces.error)
56
108
  return { error: workspaces.error };
57
- return { manifest: { layoutVersion: INSTALL_LAYOUT_VERSION, mode: value.mode, hosts, workspaces: workspaces.workspaces } };
109
+ return {
110
+ manifest: {
111
+ layoutVersion: INSTALL_LAYOUT_VERSION,
112
+ mode: value.mode,
113
+ hosts,
114
+ ...(unknownHosts.length > 0 ? { unknownHosts } : {}),
115
+ workspaces: workspaces.workspaces,
116
+ },
117
+ ...(warnings.length > 0 ? { warnings } : {}),
118
+ };
58
119
  }
59
120
  export function getInstallLayoutPaths(homeDir) {
60
121
  const home = path.resolve(homeDir);
@@ -79,8 +140,18 @@ export function getInstallLayoutPaths(homeDir) {
79
140
  releaseManagerDir: path.join(runtimeDir, 'release-manager'),
80
141
  codexAdapterDir: path.join(runtimeDir, 'codex-adapter'),
81
142
  workbuddyAdapterDir: path.join(runtimeDir, 'workbuddy-adapter'),
143
+ mcpServerDir: path.join(runtimeDir, 'pd-mcp-server'),
82
144
  };
83
145
  }
146
+ /**
147
+ * Canonical installed MCP server entry for the shared runtime layout
148
+ * (~/.pd/runtime/pd-mcp-server/dist/index.js). Launchers, binding writers and
149
+ * health diagnostics derive the path from this single owner instead of each
150
+ * repeating the segment list.
151
+ */
152
+ export function getMcpServerEntry(paths) {
153
+ return path.join(paths.mcpServerDir, 'dist', 'index.js');
154
+ }
84
155
  /**
85
156
  * WorkBuddy governance workspace (SPEC workbuddy-host-adapter §4.1 OD-07,
86
157
  * frozen by Phase 0 probe P06 2026-10-12): an explicit user-level directory
@@ -96,8 +167,28 @@ export function getWorkbuddyWorkspaceDir(homeDir) {
96
167
  export function resolveInstallLayout(options) {
97
168
  const paths = getInstallLayoutPaths(options.homeDir);
98
169
  const parsed = parseInstallManifest(options.manifest);
170
+ const { warnings } = parsed;
99
171
  if (parsed.manifest && parsed.manifest.mode === 'canonical' && options.canonicalRuntimeExists) {
100
- return { mode: 'canonical', paths, manifest: parsed.manifest };
172
+ return { mode: 'canonical', paths, manifest: parsed.manifest, ...(warnings !== undefined ? { warnings } : {}) };
173
+ }
174
+ // RF-01 (review 2026-10-16): when a canonical runtime EXISTS, the manifest
175
+ // is the install-state authority. A manifest that was READ but cannot be
176
+ // honored (newer layoutVersion, structural damage) must refuse the whole
177
+ // resolution — falling through to the legacy extension dir would launch a
178
+ // runtime that does not match the installed protocol. Legacy fallback stays
179
+ // legitimate only when the reader genuinely saw NO manifest (absent file →
180
+ // `options.manifest === undefined`), i.e. a real legacy-only install, or
181
+ // when there is no canonical runtime to bypass.
182
+ if (options.canonicalRuntimeExists && options.manifest !== undefined && parsed.manifest === undefined) {
183
+ return {
184
+ mode: 'missing',
185
+ paths,
186
+ reason: parsed.error ?? 'install_manifest_malformed',
187
+ nextAction: parsed.error?.startsWith('install_manifest_unsupported_version')
188
+ ? 'Update PD Companion from https://github.com/csuzngjh/principles/releases (companion-v* Windows installer); do not reinstall the healthy PD runtime.'
189
+ : 'Run the PD installer/updater to repair the structurally damaged installation manifest.',
190
+ ...(warnings !== undefined ? { warnings } : {}),
191
+ };
101
192
  }
102
193
  if (options.legacyExtensionExists) {
103
194
  return {
@@ -106,6 +197,7 @@ export function resolveInstallLayout(options) {
106
197
  ...(parsed.manifest ? { manifest: parsed.manifest } : {}),
107
198
  reason: parsed.error ?? 'install_layout_legacy_fallback',
108
199
  nextAction: 'Run the installer update to migrate the shared runtime to ~/.pd/runtime.',
200
+ ...(warnings !== undefined ? { warnings } : {}),
109
201
  };
110
202
  }
111
203
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@principles/install-layout",
3
- "version": "0.3.1",
3
+ "version": "0.4.1",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -33,5 +33,6 @@
33
33
  "@types/node": "^26.4.1",
34
34
  "typescript": "^7.0.2",
35
35
  "vitest": "^5.0.0"
36
- }
36
+ },
37
+ "gitHead": "d3355465b090daf098581cdb4c174634fe1e5279"
37
38
  }