@holoscript/holosystem 0.1.1 → 0.2.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,9 +1,10 @@
1
1
  # @holoscript/holosystem
2
2
 
3
- Create and inspect a portable HoloSystem consumer configuration from public npm
4
- and PyPI package contracts. The package is for external founders, agent
5
- operators, and cold consumers who need a small bootstrap contract without a
6
- private workspace or machine-specific defaults.
3
+ Create and inspect a portable HoloSystem consumer configuration, discover a
4
+ multi-rail public consumption catalog, map registry artifacts to canonical
5
+ source repositories, and select bounded next work. The package is for external
6
+ founders, agent operators, and cold consumers who need the same evidence
7
+ contract without a private workspace or machine-specific defaults.
7
8
 
8
9
  ```bash
9
10
  npm install @holoscript/holosystem
@@ -11,6 +12,49 @@ npx holosystem create --id acme-founder --workspace acme --json
11
12
  npx holosystem inspect holosystem.config.json --json
12
13
  ```
13
14
 
15
+ ## Consumption Catalog
16
+
17
+ The catalog keeps npm, PyPI, GitHub repositories, public services, containers,
18
+ and agent surfaces as separate rails. It never creates a misleading grand total.
19
+ Each rail reports publication, consumer readiness, dogfood evidence when known,
20
+ and gaps independently.
21
+
22
+ ```bash
23
+ npx holosystem lineage \
24
+ --portfolio portfolio-consumer.json \
25
+ --output source-lineage.json \
26
+ --json
27
+
28
+ npx holosystem catalog \
29
+ --seeds public-surface-seeds.json \
30
+ --portfolio portfolio-consumer.json \
31
+ --manifest package-manifest.json \
32
+ --lineage source-lineage.json \
33
+ --output consumption-catalog.json \
34
+ --json
35
+ ```
36
+
37
+ `lineage` reads registry metadata and records canonical repository URLs and
38
+ portable package directories. Unknown lineage remains a named gap; local source
39
+ paths are never emitted. `catalog` performs public GitHub, service, container,
40
+ MCP-health, and public-skill discovery, then joins those results with the
41
+ caller's package admission, lineage, proof-batch, and promotion receipts.
42
+
43
+ ```js
44
+ import {
45
+ buildConsumptionSurfaceCatalog,
46
+ buildSourceLineageReceipt,
47
+ discoverConsumptionSurfaceCatalog,
48
+ discoverSourceLineage,
49
+ selectNextConsumptionWork,
50
+ } from '@holoscript/holosystem';
51
+ ```
52
+
53
+ The bounded selector excludes artifacts already present in active proof batches,
54
+ returns one highest-priority candidate, and carries authority, validation, lease,
55
+ and spend stop conditions. It selects work; it does not claim a board task,
56
+ publish a package, spend funds, or bypass caller authority.
57
+
14
58
  ## What It Creates
15
59
 
16
60
  The default configuration pins public contracts for:
@@ -99,7 +143,8 @@ package check, an npm pack dry run, and the repository public-consumption gate.
99
143
 
100
144
  Release lane: `v0-preview`. Supported behavior is deterministic config creation
101
145
  and static, secret-safe portability inspection for the documented v1 schema.
102
- Known limitations: this package does not verify registry availability, install
103
- dependencies, test live credentials, bootstrap databases, or authorize agent
104
- work. Roll back by pinning the previous package version and retaining the
105
- consumer-owned config and receipts.
146
+ Known limitations: this package does not install package dependencies, test live
147
+ credentials, bootstrap databases, claim team work, or authorize publishing.
148
+ Public catalog and lineage discovery depend on caller network access and the
149
+ availability of the queried registries and endpoints. Roll back by pinning the
150
+ previous package version and retaining the consumer-owned config and receipts.
@@ -2,21 +2,29 @@
2
2
  import { existsSync, readFileSync, writeFileSync } from 'node:fs';
3
3
  import { resolve } from 'node:path';
4
4
 
5
- import { createHoloSystemConfig, inspectHoloSystemConfig } from '../src/index.mjs';
5
+ import {
6
+ createHoloSystemConfig,
7
+ discoverConsumptionSurfaceCatalog,
8
+ discoverSourceLineage,
9
+ inspectHoloSystemConfig,
10
+ } from '../src/index.mjs';
6
11
 
7
12
  const CLI_RECEIPT_SCHEMA = 'holoscript.holosystem.cli-receipt.v1';
8
- const HELP = `holosystem - create or inspect a portable HoloSystem consumer config
13
+ const HELP = `holosystem - portable HoloSystem consumer configuration and evidence
9
14
 
10
15
  Usage:
11
16
  holosystem create [--id <id>] [--workspace <id>] [--output <file>] [--force] [--json]
12
17
  holosystem create --stdout
13
18
  holosystem inspect [file|-] [--json]
19
+ holosystem catalog --seeds <file> --portfolio <file> --manifest <file> [--lineage <file>] [--active-batches <file>] [--promotions <file>] [--output <file>] [--json]
20
+ holosystem lineage --portfolio <file> [--concurrency <1-12>] [--output <file>] [--json]
14
21
  holosystem --help
15
22
  holosystem --version
16
23
 
17
24
  Defaults:
18
25
  create writes holosystem.config.json and never overwrites it without --force.
19
26
  inspect reads holosystem.config.json. Use - to read JSON from stdin.
27
+ catalog and lineage read caller-owned evidence and never read credentials.
20
28
  `;
21
29
 
22
30
  function die(message, { json = false, code = 1 } = {}) {
@@ -124,6 +132,23 @@ function readStdin() {
124
132
  return readFileSync(0, 'utf8');
125
133
  }
126
134
 
135
+ function readJsonFile(path, label) {
136
+ if (!path) throw new Error(`--${label} is required.`);
137
+ const absolute = resolve(process.cwd(), path);
138
+ return JSON.parse(readFileSync(absolute, 'utf8').replace(/^\uFEFF/u, ''));
139
+ }
140
+
141
+ function readOptionalJsonFile(path, fallback) {
142
+ if (!path) return fallback;
143
+ return JSON.parse(readFileSync(resolve(process.cwd(), path), 'utf8').replace(/^\uFEFF/u, ''));
144
+ }
145
+
146
+ function writeJsonOutput(path, value, { force = false } = {}) {
147
+ const absolute = resolve(process.cwd(), path);
148
+ if (existsSync(absolute) && !force) throw new Error(`${path} already exists; use --force to replace it.`);
149
+ writeFileSync(absolute, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
150
+ }
151
+
127
152
  function runInspect(args) {
128
153
  let parsed;
129
154
  try {
@@ -157,6 +182,97 @@ function runInspect(args) {
157
182
  if (!report.ready) process.exitCode = 2;
158
183
  }
159
184
 
185
+ async function runCatalog(args) {
186
+ let parsed;
187
+ try {
188
+ parsed = parseArguments(args, {
189
+ seeds: 'value',
190
+ portfolio: 'value',
191
+ manifest: 'value',
192
+ lineage: 'value',
193
+ 'active-batches': 'value',
194
+ promotions: 'value',
195
+ output: 'value',
196
+ force: 'boolean',
197
+ json: 'boolean',
198
+ });
199
+ } catch (error) {
200
+ die(error.message, { json: args.includes('--json') });
201
+ }
202
+ const { options, positionals } = parsed;
203
+ if (positionals.length > 0) die('catalog does not accept positional arguments.', { json: options.json });
204
+ let catalog;
205
+ try {
206
+ const seeds = readJsonFile(options.seeds, 'seeds');
207
+ const portfolio = readJsonFile(options.portfolio, 'portfolio');
208
+ const manifest = readJsonFile(options.manifest, 'manifest');
209
+ const lineage = readOptionalJsonFile(options.lineage, null);
210
+ const activeProofBatches = readOptionalJsonFile(options['active-batches'], []);
211
+ const promotionHistory = readOptionalJsonFile(options.promotions, []);
212
+ catalog = await discoverConsumptionSurfaceCatalog({
213
+ seeds,
214
+ portfolio,
215
+ manifest,
216
+ lineage,
217
+ activeProofBatches: Array.isArray(activeProofBatches) ? activeProofBatches : activeProofBatches.batches,
218
+ promotionHistory: Array.isArray(promotionHistory) ? promotionHistory : promotionHistory.promotions,
219
+ evidence: {
220
+ operatingSet: options.manifest,
221
+ packageAdmission: options.portfolio,
222
+ sourceLineage: options.lineage || 'not-supplied',
223
+ },
224
+ });
225
+ if (options.output) writeJsonOutput(options.output, catalog, { force: options.force });
226
+ } catch (error) {
227
+ die(`Cannot build catalog: ${error.message}`, { json: options.json, code: 2 });
228
+ }
229
+ if (options.json) outputJson(catalog);
230
+ else {
231
+ process.stdout.write(`Catalog: ${catalog.status}\n`);
232
+ for (const [id, rail] of Object.entries(catalog.rails)) {
233
+ process.stdout.write(`${id}: published=${rail.published} ready=${rail.consumerReady} gaps=${rail.gaps}\n`);
234
+ }
235
+ if (options.output) process.stdout.write(`Wrote ${options.output}\n`);
236
+ }
237
+ if (catalog.status !== 'current') process.exitCode = 2;
238
+ }
239
+
240
+ async function runLineage(args) {
241
+ let parsed;
242
+ try {
243
+ parsed = parseArguments(args, {
244
+ portfolio: 'value',
245
+ concurrency: 'value',
246
+ output: 'value',
247
+ force: 'boolean',
248
+ json: 'boolean',
249
+ });
250
+ } catch (error) {
251
+ die(error.message, { json: args.includes('--json') });
252
+ }
253
+ const { options, positionals } = parsed;
254
+ if (positionals.length > 0) die('lineage does not accept positional arguments.', { json: options.json });
255
+ let lineage;
256
+ try {
257
+ const portfolio = readJsonFile(options.portfolio, 'portfolio');
258
+ lineage = await discoverSourceLineage({
259
+ portfolio,
260
+ concurrency: options.concurrency ? Number(options.concurrency) : 6,
261
+ });
262
+ if (options.output) writeJsonOutput(options.output, lineage, { force: options.force });
263
+ } catch (error) {
264
+ die(`Cannot build lineage: ${error.message}`, { json: options.json, code: 2 });
265
+ }
266
+ if (options.json) outputJson(lineage);
267
+ else {
268
+ process.stdout.write(
269
+ `Lineage: ${lineage.status} mapped=${lineage.summary.mapped}/${lineage.summary.total} gaps=${lineage.summary.gaps}\n`
270
+ );
271
+ if (options.output) process.stdout.write(`Wrote ${options.output}\n`);
272
+ }
273
+ if (lineage.status !== 'complete') process.exitCode = 2;
274
+ }
275
+
160
276
  const argv = process.argv.slice(2);
161
277
  const command = argv[0];
162
278
  if (!command || command === '--help' || command === '-h' || command === 'help') {
@@ -168,6 +284,10 @@ if (!command || command === '--help' || command === '-h' || command === 'help')
168
284
  runCreate(argv.slice(1));
169
285
  } else if (command === 'inspect') {
170
286
  runInspect(argv.slice(1));
287
+ } else if (command === 'catalog') {
288
+ await runCatalog(argv.slice(1));
289
+ } else if (command === 'lineage') {
290
+ await runLineage(argv.slice(1));
171
291
  } else {
172
292
  die(`Unknown command ${command}. Run holosystem --help.`);
173
293
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@holoscript/holosystem",
3
- "version": "0.1.1",
4
- "description": "Portable public-package bootstrap and inspection contract for HoloSystem consumers.",
3
+ "version": "0.2.0",
4
+ "description": "Portable bootstrap, consumption catalog, lineage, and bounded-work contract for HoloSystem consumers.",
5
5
  "releaseLane": "v0-preview",
6
6
  "keywords": [
7
7
  "holoscript",
@@ -41,8 +41,8 @@
41
41
  "node": ">=18"
42
42
  },
43
43
  "scripts": {
44
- "build": "node --check src/index.mjs && node --check bin/holosystem.mjs",
45
- "test": "node --test test/holosystem.test.mjs test/cli.test.mjs",
44
+ "build": "node --check src/index.mjs && node --check src/catalog.mjs && node --check bin/holosystem.mjs",
45
+ "test": "node --test test/*.test.mjs",
46
46
  "smoke": "node bin/holosystem.mjs --help",
47
47
  "check": "pnpm run build && pnpm run test && pnpm run smoke",
48
48
  "prepublishOnly": "pnpm run check"
@@ -51,9 +51,9 @@
51
51
  "releaseLane": "v0-preview",
52
52
  "pypiCompanion": "holoscript-holorepo",
53
53
  "publicConsumption": {
54
- "coreRule": "Cold consumers create and inspect a HoloSystem configuration from public registry contracts and caller-owned bindings.",
54
+ "coreRule": "Cold consumers create and inspect configuration, discover unlike public rails separately, map source lineage, and select bounded next work from caller-owned evidence.",
55
55
  "localBoundary": "Machine paths, credentials, private repositories, and operator state are caller inputs, never package defaults.",
56
- "supportBoundary": "Configuration creation and static portability inspection are supported; installation, credential acquisition, service mutation, and publishing are outside this package."
56
+ "supportBoundary": "Configuration, public discovery, lineage, and bounded selection are supported; installation, credential acquisition, board mutation, service mutation, spending, and publishing remain caller-authorized operations."
57
57
  }
58
58
  }
59
59
  }
@@ -0,0 +1,583 @@
1
+ import { createHash } from 'node:crypto';
2
+
3
+ export const HOLOSYSTEM_CATALOG_SCHEMA = 'holoscript.holosystem.consumption-catalog.v1';
4
+ export const HOLOSYSTEM_LINEAGE_SCHEMA = 'holoscript.holosystem.source-lineage.v1';
5
+ export const HOLOSYSTEM_NEXT_WORK_SCHEMA = 'holoscript.holosystem.next-consumption-work.v1';
6
+ export const HOLOSYSTEM_CONSUMER_INPUT_SCHEMA = 'holoscript.holosystem.consumer-input.v1';
7
+
8
+ const STOP_CONDITIONS = [
9
+ 'authority-required',
10
+ 'validation-failed',
11
+ 'lease-expired',
12
+ 'spend-limit-reached',
13
+ ];
14
+
15
+ function list(value) {
16
+ return Array.isArray(value) ? value : [];
17
+ }
18
+
19
+ function clone(value) {
20
+ return JSON.parse(JSON.stringify(value));
21
+ }
22
+
23
+ function hashReceipt(value) {
24
+ return `sha256:${createHash('sha256').update(JSON.stringify(value)).digest('hex')}`;
25
+ }
26
+
27
+ function exactPublicVersion(value) {
28
+ return typeof value === 'string'
29
+ && /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/u.test(value)
30
+ && !/^(?:file|workspace|link|portal|git|https?):/iu.test(value);
31
+ }
32
+
33
+ export function inspectPublicDependencySpecs(dependencies = {}) {
34
+ const packages = Object.entries(dependencies)
35
+ .sort(([left], [right]) => left.localeCompare(right))
36
+ .map(([name, version]) => ({
37
+ name,
38
+ version,
39
+ exactPublicVersion: exactPublicVersion(version),
40
+ }));
41
+ const issues = packages
42
+ .filter((item) => !item.exactPublicVersion)
43
+ .map((item) => ({
44
+ code: 'dependency-not-exact-public-version',
45
+ name: item.name,
46
+ version: item.version,
47
+ }));
48
+ return {
49
+ schema: HOLOSYSTEM_CONSUMER_INPUT_SCHEMA,
50
+ ready: issues.length === 0,
51
+ packages,
52
+ issues,
53
+ };
54
+ }
55
+
56
+ export function hashConsumerInput({ dependencies = {}, manifest = {}, requirements = [] }) {
57
+ const dependencyInspection = inspectPublicDependencySpecs(dependencies);
58
+ const normalized = {
59
+ dependencies: dependencyInspection.packages.map(({ name, version }) => ({ name, version })),
60
+ npm: list(manifest?.npm)
61
+ .map((item) => ({
62
+ name: item?.name || null,
63
+ version: item?.version || null,
64
+ integrity: item?.integrity || null,
65
+ probeKind: item?.probe?.kind || 'import',
66
+ }))
67
+ .sort((left, right) => String(left.name).localeCompare(String(right.name))),
68
+ pypi: list(manifest?.pypi)
69
+ .map((item) => ({
70
+ name: item?.name || null,
71
+ version: item?.version || null,
72
+ hashes: list(item?.hashes).slice().sort(),
73
+ }))
74
+ .sort((left, right) => String(left.name).localeCompare(String(right.name))),
75
+ requirements: list(requirements).map(String).sort(),
76
+ };
77
+ return {
78
+ schema: HOLOSYSTEM_CONSUMER_INPUT_SCHEMA,
79
+ ready: dependencyInspection.ready,
80
+ hash: hashReceipt(normalized),
81
+ normalized,
82
+ issues: dependencyInspection.issues,
83
+ };
84
+ }
85
+
86
+ function artifactKey(ecosystem, name) {
87
+ return `${String(ecosystem || '').toLowerCase()}:${String(name || '').toLowerCase()}`;
88
+ }
89
+
90
+ function portableDirectory(value) {
91
+ if (typeof value !== 'string' || !value.trim()) return null;
92
+ const normalized = value.trim().replaceAll('\\', '/').replace(/^\.\//u, '');
93
+ if (/^(?:[A-Za-z]:|\/|~\/)/u.test(normalized) || /(?:^|\/)\.\.(?:\/|$)/u.test(normalized)) {
94
+ return null;
95
+ }
96
+ return normalized.replace(/\/$/u, '') || null;
97
+ }
98
+
99
+ export function normalizeRepositoryUrl(value) {
100
+ const raw = typeof value === 'string' ? value : value?.url;
101
+ if (typeof raw !== 'string' || !raw.trim()) return null;
102
+ let normalized = raw.trim();
103
+ normalized = normalized.replace(/^git\+/u, '');
104
+ normalized = normalized.replace(/^git:\/\/github\.com\//u, 'https://github.com/');
105
+ normalized = normalized.replace(/^git@github\.com:/u, 'https://github.com/');
106
+ normalized = normalized.replace(/\.git\/?$/u, '');
107
+ normalized = normalized.replace(/\/$/u, '');
108
+ return /^https?:\/\//u.test(normalized) ? normalized : null;
109
+ }
110
+
111
+ function packageRail(portfolio, manifest, ecosystem, evidence) {
112
+ const records = list(portfolio?.packages).filter((row) => row.ecosystem === ecosystem);
113
+ const dogfoodNames = new Set(list(manifest?.[ecosystem]).map((row) => row.name));
114
+ return {
115
+ published: records.length,
116
+ consumerReady: records.filter((row) => row.classification === 'passing').length,
117
+ dogfooded: records.filter((row) => dogfoodNames.has(row.name)).length,
118
+ gaps: records.filter((row) => row.classification !== 'passing').length,
119
+ evidence,
120
+ };
121
+ }
122
+
123
+ function countRail(items) {
124
+ return {
125
+ published: items.filter((item) => item.published).length,
126
+ consumerReady: items.filter((item) => item.contractReady).length,
127
+ gaps: items.filter((item) => item.published && !item.contractReady).length,
128
+ };
129
+ }
130
+
131
+ function packageFailureCategories(portfolio) {
132
+ const gaps = list(portfolio?.packages).filter((row) => row.classification !== 'passing');
133
+ const issueCount = (prefix) => gaps.filter((row) => list(row.issues).some((issue) => issue.startsWith(prefix))).length;
134
+ return {
135
+ total: gaps.length,
136
+ missing: gaps.filter((row) => row.classification === 'missing').length,
137
+ stale: gaps.filter((row) => row.classification === 'stale').length,
138
+ failed: gaps.filter((row) => row.classification === 'failed').length,
139
+ exactVersion: issueCount('exactVersion-'),
140
+ integrity: issueCount('integrity-'),
141
+ import: issueCount('import-'),
142
+ readback: issueCount('readback-'),
143
+ };
144
+ }
145
+
146
+ function projectProofBatches(value) {
147
+ return list(value).slice(-25).map((batch) => ({
148
+ id: typeof batch?.id === 'string' ? batch.id : null,
149
+ status: typeof batch?.status === 'string' ? batch.status : 'unknown',
150
+ generatedAt: typeof batch?.generatedAt === 'string' ? batch.generatedAt : null,
151
+ packages: list(batch?.packages).slice(0, 100).map((item) => (
152
+ typeof item === 'string'
153
+ ? item
154
+ : { ecosystem: item?.ecosystem || null, name: item?.name || null }
155
+ )),
156
+ summary: {
157
+ total: Number(batch?.summary?.total ?? batch?.packageCount) || 0,
158
+ passing: Number(batch?.summary?.passing ?? batch?.passing) || 0,
159
+ failed: Number(batch?.summary?.failed ?? batch?.failed) || 0,
160
+ },
161
+ receiptHash: typeof batch?.receiptHash === 'string' ? batch.receiptHash : null,
162
+ }));
163
+ }
164
+
165
+ function projectPromotionHistory(value) {
166
+ return list(value).slice(-25).map((attempt) => ({
167
+ id: typeof attempt?.id === 'string' ? attempt.id : null,
168
+ status: typeof attempt?.status === 'string' ? attempt.status : 'unknown',
169
+ eligible: attempt?.eligible === true,
170
+ generatedAt: typeof attempt?.generatedAt === 'string' ? attempt.generatedAt : null,
171
+ receiptHash: typeof attempt?.receiptHash === 'string' ? attempt.receiptHash : null,
172
+ }));
173
+ }
174
+
175
+ export function buildSourceLineageReceipt({ portfolio, metadata = [], now = new Date() }) {
176
+ const metadataByArtifact = new Map(
177
+ list(metadata).map((item) => [artifactKey(item.ecosystem, item.name), item])
178
+ );
179
+ const artifacts = list(portfolio?.packages).map((row) => {
180
+ const source = metadataByArtifact.get(artifactKey(row.ecosystem, row.name)) || {};
181
+ const sourceRepository = normalizeRepositoryUrl(source.sourceRepository || source.repository);
182
+ return {
183
+ ecosystem: row.ecosystem,
184
+ name: row.name,
185
+ version: row.expectedVersion || row.observedVersion || source.version || null,
186
+ sourceRepository,
187
+ sourceDirectory: portableDirectory(source.sourceDirectory || source.directory),
188
+ registryStatus: Number.isInteger(source.registryStatus) ? source.registryStatus : null,
189
+ registryError: source.registryError ? String(source.registryError).slice(0, 240) : null,
190
+ integrity: typeof source.integrity === 'string' ? source.integrity : null,
191
+ sourceRevision: typeof source.sourceRevision === 'string' ? source.sourceRevision : null,
192
+ mapped: Boolean(sourceRepository),
193
+ };
194
+ });
195
+ const mapped = artifacts.filter((artifact) => artifact.mapped).length;
196
+ const receipt = {
197
+ schema: HOLOSYSTEM_LINEAGE_SCHEMA,
198
+ generatedAt: now.toISOString(),
199
+ status: mapped === artifacts.length ? 'complete' : 'partial',
200
+ summary: {
201
+ total: artifacts.length,
202
+ mapped,
203
+ gaps: artifacts.length - mapped,
204
+ },
205
+ artifacts,
206
+ boundaries: {
207
+ registryMetadataIsEvidence: true,
208
+ localPathsForbidden: true,
209
+ unknownLineageBlocksSourceClaims: true,
210
+ },
211
+ };
212
+ receipt.receiptHash = hashReceipt({ ...receipt, generatedAt: null });
213
+ return receipt;
214
+ }
215
+
216
+ function activeArtifactKeys(activeProofBatches) {
217
+ const keys = new Set();
218
+ for (const batch of list(activeProofBatches)) {
219
+ if (!['running', 'claimed', 'queued'].includes(batch?.status)) continue;
220
+ for (const item of list(batch.packages)) {
221
+ if (typeof item === 'string') keys.add(item.toLowerCase());
222
+ else keys.add(artifactKey(item?.ecosystem, item?.name));
223
+ }
224
+ }
225
+ return keys;
226
+ }
227
+
228
+ export function selectNextConsumptionWork({
229
+ portfolio,
230
+ lineage,
231
+ activeProofBatches = [],
232
+ maxCandidates = 20,
233
+ now = new Date(),
234
+ }) {
235
+ const active = activeArtifactKeys(activeProofBatches);
236
+ const sourceByArtifact = new Map(
237
+ list(lineage?.artifacts).map((item) => [artifactKey(item.ecosystem, item.name), item])
238
+ );
239
+ const candidates = list(portfolio?.packages)
240
+ .filter((row) => row.classification !== 'passing')
241
+ .filter((row) => !active.has(artifactKey(row.ecosystem, row.name)))
242
+ .map((row) => {
243
+ const source = sourceByArtifact.get(artifactKey(row.ecosystem, row.name));
244
+ const basePriority = row.classification === 'failed' ? 400 : row.classification === 'stale' ? 300 : 200;
245
+ const priority = basePriority + (source?.mapped ? 0 : 25);
246
+ return {
247
+ ecosystem: row.ecosystem,
248
+ name: row.name,
249
+ version: row.expectedVersion || row.observedVersion || null,
250
+ classification: row.classification,
251
+ action: row.classification === 'stale' ? 'refresh-cold-consumption' : 'prove-cold-consumption',
252
+ priority,
253
+ sourceRepository: source?.sourceRepository || null,
254
+ reasons: [
255
+ `classification:${row.classification}`,
256
+ ...(source?.mapped ? [] : ['source-lineage-missing']),
257
+ ...list(row.issues),
258
+ ],
259
+ };
260
+ })
261
+ .sort((left, right) => right.priority - left.priority || artifactKey(left.ecosystem, left.name).localeCompare(artifactKey(right.ecosystem, right.name)))
262
+ .slice(0, Math.max(1, Math.min(Number(maxCandidates) || 20, 100)));
263
+
264
+ const receipt = {
265
+ schema: HOLOSYSTEM_NEXT_WORK_SCHEMA,
266
+ generatedAt: now.toISOString(),
267
+ status: candidates.length > 0 ? 'action-ready' : 'idle',
268
+ selected: candidates[0] || null,
269
+ candidates,
270
+ policy: {
271
+ mode: 'bounded',
272
+ maxCandidates: Math.max(1, Math.min(Number(maxCandidates) || 20, 100)),
273
+ excludesActiveBatches: true,
274
+ publishRequiresAuthority: true,
275
+ },
276
+ stopConditions: [...STOP_CONDITIONS],
277
+ };
278
+ receipt.receiptHash = hashReceipt({ ...receipt, generatedAt: null });
279
+ return receipt;
280
+ }
281
+
282
+ export function buildConsumptionSurfaceCatalog({
283
+ seeds = {},
284
+ manifest = {},
285
+ portfolio = {},
286
+ github = [],
287
+ services = [],
288
+ containers = [],
289
+ mcpHealth = {},
290
+ skills = { ok: false, count: 0 },
291
+ lineage = null,
292
+ activeProofBatches = [],
293
+ promotionHistory = [],
294
+ evidence = {},
295
+ now = new Date(),
296
+ }) {
297
+ const evidenceRefs = {
298
+ operatingSet: evidence.operatingSet || 'package-manifest.json',
299
+ packageAdmission: evidence.packageAdmission || 'portfolio-consumer.json',
300
+ sourceLineage: evidence.sourceLineage || 'source-lineage.json',
301
+ };
302
+ const npm = packageRail(portfolio, manifest, 'npm', evidenceRefs.packageAdmission);
303
+ const pypi = packageRail(portfolio, manifest, 'pypi', evidenceRefs.packageAdmission);
304
+ const githubRail = countRail(github);
305
+ const serviceRail = countRail(services);
306
+ const containerRail = countRail(containers);
307
+ const sourceAudit = seeds?.mcp?.sourceAudit || {};
308
+ const deployedTools = Number(mcpHealth?.tools) || 0;
309
+ const expectedProducts = list(seeds?.github?.products);
310
+ const discoveryCurrent = portfolio?.scope?.registries?.npm?.declaredComplete === true
311
+ && portfolio?.scope?.registries?.pypi?.declaredComplete === true
312
+ && github.length === expectedProducts.length
313
+ && github.every((item) => item.published)
314
+ && skills.ok === true
315
+ && deployedTools > 0;
316
+ const lineageSummary = lineage?.summary || {
317
+ total: list(portfolio?.packages).length,
318
+ mapped: 0,
319
+ gaps: list(portfolio?.packages).length,
320
+ };
321
+ const findings = [];
322
+ if (npm.gaps || pypi.gaps) findings.push({
323
+ id: 'package-cold-consumption-gaps',
324
+ severity: 'blocking',
325
+ count: npm.gaps + pypi.gaps,
326
+ evidence: evidenceRefs.packageAdmission,
327
+ });
328
+ if (lineageSummary.gaps) findings.push({
329
+ id: 'package-source-lineage-gaps',
330
+ severity: 'attention',
331
+ count: lineageSummary.gaps,
332
+ evidence: evidenceRefs.sourceLineage,
333
+ });
334
+ if (deployedTools !== Number(sourceAudit.tools || 0)) findings.push({
335
+ id: 'mcp-deploy-source-count-drift',
336
+ severity: 'attention',
337
+ deployedTools,
338
+ sourceTools: Number(sourceAudit.tools || 0),
339
+ });
340
+ if (Number(sourceAudit.orphanTools || 0)) findings.push({
341
+ id: 'agent-tool-surface-gaps',
342
+ severity: 'attention',
343
+ orphanTools: Number(sourceAudit.orphanTools || 0),
344
+ unbackedCoveredTools: Number(sourceAudit.unbackedCoveredTools || 0),
345
+ });
346
+ const nextWork = selectNextConsumptionWork({ portfolio, lineage, activeProofBatches, now });
347
+ const projectedProofBatches = projectProofBatches(activeProofBatches);
348
+ const projectedPromotionHistory = projectPromotionHistory(promotionHistory);
349
+ const receipt = {
350
+ schema: HOLOSYSTEM_CATALOG_SCHEMA,
351
+ generatedAt: now.toISOString(),
352
+ status: discoveryCurrent ? 'current' : 'incomplete-discovery',
353
+ rule: 'Never collapse unlike artifacts into one total. Published, consumer-ready, and dogfooded are separate states on every rail.',
354
+ rails: {
355
+ npm,
356
+ pypi,
357
+ github: { ...githubRail, dogfooded: 0, evidence: 'public GitHub repository metadata plus README' },
358
+ services: { ...serviceRail, dogfooded: null, evidence: 'public health endpoints' },
359
+ containers: { ...containerRail, dogfooded: null, evidence: 'public container package pages' },
360
+ },
361
+ packageFailures: packageFailureCategories(portfolio),
362
+ lineage: {
363
+ ...lineageSummary,
364
+ status: lineage?.status || 'unproven',
365
+ evidence: evidenceRefs.sourceLineage,
366
+ },
367
+ agentSurface: {
368
+ deployedMcpTools: deployedTools,
369
+ sourceMappedTools: Number(sourceAudit.tools || 0),
370
+ sourceCoveredTools: Number(sourceAudit.coveredTools || 0),
371
+ orphanTools: Number(sourceAudit.orphanTools || 0),
372
+ unbackedCoveredTools: Number(sourceAudit.unbackedCoveredTools || 0),
373
+ publicRoutes: Number(sourceAudit.routes || 0),
374
+ publicSkills: Number(skills.count || 0),
375
+ mcpVersion: mcpHealth?.version || null,
376
+ evidence: 'public MCP health plus public repository tree and capability audit',
377
+ },
378
+ activity: {
379
+ activeProofBatches: projectedProofBatches,
380
+ promotionHistory: projectedPromotionHistory,
381
+ nextWork,
382
+ },
383
+ items: { github: clone(github), services: clone(services), containers: clone(containers) },
384
+ findings,
385
+ boundaries: {
386
+ ...evidenceRefs,
387
+ noGrandTotal: true,
388
+ privateSourceFoldersAreNotPublishedArtifacts: true,
389
+ callerOwnsCredentialsAndPolicy: true,
390
+ },
391
+ };
392
+ receipt.receiptHash = hashReceipt({ ...receipt, generatedAt: null });
393
+ return receipt;
394
+ }
395
+
396
+ async function request(fetchImpl, url, { json = true } = {}) {
397
+ try {
398
+ const response = await fetchImpl(url, {
399
+ headers: {
400
+ accept: json ? 'application/json' : 'text/html,application/xhtml+xml',
401
+ 'user-agent': 'holoscript-holosystem-consumer/1',
402
+ },
403
+ redirect: 'follow',
404
+ signal: AbortSignal.timeout(20_000),
405
+ });
406
+ let body = null;
407
+ if (response.ok && json) {
408
+ try {
409
+ body = await response.json();
410
+ } catch {
411
+ body = null;
412
+ }
413
+ }
414
+ return { ok: response.ok, status: response.status, body };
415
+ } catch (error) {
416
+ return {
417
+ ok: false,
418
+ status: null,
419
+ body: null,
420
+ error: String(error?.message || error).slice(0, 240),
421
+ };
422
+ }
423
+ }
424
+
425
+ async function mapConcurrent(items, concurrency, worker) {
426
+ const results = new Array(items.length);
427
+ let index = 0;
428
+ async function runWorker() {
429
+ while (index < items.length) {
430
+ const current = index;
431
+ index += 1;
432
+ results[current] = await worker(items[current], current);
433
+ }
434
+ }
435
+ const count = Math.max(1, Math.min(Number(concurrency) || 6, 12, items.length || 1));
436
+ await Promise.all(Array.from({ length: count }, () => runWorker()));
437
+ return results;
438
+ }
439
+
440
+ function pypiRepository(info) {
441
+ const urls = info?.project_urls || {};
442
+ for (const key of ['Source', 'Source Code', 'Repository', 'Code', 'Homepage']) {
443
+ const value = normalizeRepositoryUrl(urls[key]);
444
+ if (value) return value;
445
+ }
446
+ return normalizeRepositoryUrl(info?.home_page);
447
+ }
448
+
449
+ export async function discoverSourceLineage({
450
+ portfolio,
451
+ fetchImpl = globalThis.fetch,
452
+ concurrency = 6,
453
+ now = new Date(),
454
+ }) {
455
+ if (typeof fetchImpl !== 'function') throw new TypeError('A fetch implementation is required.');
456
+ const metadata = await mapConcurrent(list(portfolio?.packages), concurrency, async (artifact) => {
457
+ const version = artifact.expectedVersion || artifact.observedVersion;
458
+ if (artifact.ecosystem === 'npm') {
459
+ const target = version ? `${encodeURIComponent(artifact.name)}/${encodeURIComponent(version)}` : encodeURIComponent(artifact.name);
460
+ const response = await request(fetchImpl, `https://registry.npmjs.org/${target}`);
461
+ return {
462
+ ecosystem: 'npm',
463
+ name: artifact.name,
464
+ version: response.body?.version || version || null,
465
+ sourceRepository: normalizeRepositoryUrl(response.body?.repository),
466
+ sourceDirectory: portableDirectory(response.body?.repository?.directory),
467
+ registryStatus: response.status,
468
+ registryError: response.error || null,
469
+ integrity: response.body?.dist?.integrity || null,
470
+ sourceRevision: response.body?.gitHead || null,
471
+ };
472
+ }
473
+ const suffix = version ? `/${encodeURIComponent(version)}` : '';
474
+ const response = await request(fetchImpl, `https://pypi.org/pypi/${encodeURIComponent(artifact.name)}${suffix}/json`);
475
+ const files = list(response.body?.urls);
476
+ const digest = files.map((file) => file?.digests?.sha256).find(Boolean);
477
+ return {
478
+ ecosystem: 'pypi',
479
+ name: artifact.name,
480
+ version: response.body?.info?.version || version || null,
481
+ sourceRepository: pypiRepository(response.body?.info),
482
+ sourceDirectory: null,
483
+ registryStatus: response.status,
484
+ registryError: response.error || null,
485
+ integrity: digest ? `sha256:${digest}` : null,
486
+ sourceRevision: null,
487
+ };
488
+ });
489
+ return buildSourceLineageReceipt({ portfolio, metadata, now });
490
+ }
491
+
492
+ export async function discoverConsumptionSurfaceCatalog({
493
+ seeds,
494
+ manifest,
495
+ portfolio,
496
+ lineage = null,
497
+ activeProofBatches = [],
498
+ promotionHistory = [],
499
+ evidence = {},
500
+ fetchImpl = globalThis.fetch,
501
+ now = new Date(),
502
+ }) {
503
+ if (typeof fetchImpl !== 'function') throw new TypeError('A fetch implementation is required.');
504
+ const github = await Promise.all(list(seeds?.github?.products).map(async (fullName) => {
505
+ const [metadata, readme] = await Promise.all([
506
+ request(fetchImpl, `https://api.github.com/repos/${fullName}`),
507
+ request(fetchImpl, `https://api.github.com/repos/${fullName}/readme`),
508
+ ]);
509
+ const repository = metadata.body || {};
510
+ const published = metadata.ok && repository.private === false;
511
+ return {
512
+ name: fullName,
513
+ published,
514
+ contractReady: published
515
+ && repository.archived !== true
516
+ && repository.fork !== true
517
+ && Boolean(repository.description)
518
+ && Boolean(repository.license?.spdx_id)
519
+ && readme.ok,
520
+ defaultBranch: repository.default_branch || null,
521
+ license: repository.license?.spdx_id || null,
522
+ readme: readme.ok,
523
+ archived: repository.archived === true,
524
+ fork: repository.fork === true,
525
+ url: repository.html_url || `https://github.com/${fullName}`,
526
+ };
527
+ }));
528
+ const services = await Promise.all(list(seeds?.services).map(async (service) => {
529
+ const probe = await request(fetchImpl, `${service.url}${service.health}`);
530
+ return {
531
+ name: service.name,
532
+ url: service.url,
533
+ health: service.health,
534
+ published: true,
535
+ contractReady: probe.ok,
536
+ status: probe.status,
537
+ };
538
+ }));
539
+ const containers = await Promise.all(list(seeds?.containers).map(async (container) => {
540
+ const page = await request(
541
+ fetchImpl,
542
+ `https://github.com/users/${container.owner}/packages/container/package/${container.name}`,
543
+ { json: false }
544
+ );
545
+ return {
546
+ name: container.name,
547
+ image: container.image,
548
+ published: page.ok,
549
+ contractReady: page.ok,
550
+ status: page.status,
551
+ };
552
+ }));
553
+ const [mcp, repository] = await Promise.all([
554
+ request(fetchImpl, seeds?.mcp?.healthUrl),
555
+ request(fetchImpl, `https://api.github.com/repos/${seeds?.mcp?.publicRepository}`),
556
+ ]);
557
+ const branch = repository.body?.default_branch || 'main';
558
+ const tree = await request(
559
+ fetchImpl,
560
+ `https://api.github.com/repos/${seeds?.mcp?.publicRepository}/git/trees/${branch}?recursive=1`
561
+ );
562
+ const paths = list(tree.body?.tree).map((item) => item.path);
563
+ const skills = {
564
+ ok: repository.ok && tree.ok && tree.body?.truncated !== true,
565
+ count: paths.filter((path) => /(?:^|\/)SKILL\.md$/u.test(path)).length,
566
+ treeTruncated: tree.body?.truncated === true,
567
+ };
568
+ return buildConsumptionSurfaceCatalog({
569
+ seeds,
570
+ manifest,
571
+ portfolio,
572
+ github,
573
+ services,
574
+ containers,
575
+ mcpHealth: mcp.body,
576
+ skills,
577
+ lineage,
578
+ activeProofBatches,
579
+ promotionHistory,
580
+ evidence,
581
+ now,
582
+ });
583
+ }
package/src/index.mjs CHANGED
@@ -1,6 +1,21 @@
1
1
  export const HOLOSYSTEM_CONFIG_SCHEMA = 'holoscript.holosystem.consumer.v1';
2
2
  export const HOLOSYSTEM_INSPECTION_SCHEMA = 'holoscript.holosystem.inspection.v1';
3
3
 
4
+ export {
5
+ HOLOSYSTEM_CATALOG_SCHEMA,
6
+ HOLOSYSTEM_CONSUMER_INPUT_SCHEMA,
7
+ HOLOSYSTEM_LINEAGE_SCHEMA,
8
+ HOLOSYSTEM_NEXT_WORK_SCHEMA,
9
+ buildConsumptionSurfaceCatalog,
10
+ buildSourceLineageReceipt,
11
+ discoverConsumptionSurfaceCatalog,
12
+ discoverSourceLineage,
13
+ hashConsumerInput,
14
+ inspectPublicDependencySpecs,
15
+ normalizeRepositoryUrl,
16
+ selectNextConsumptionWork,
17
+ } from './catalog.mjs';
18
+
4
19
  const REQUIRED_CONTRACTS = [
5
20
  { registry: 'npm', role: 'agent-runtime' },
6
21
  { registry: 'npm', role: 'memory' },