@holoscript/holosystem 0.2.5 → 0.2.6

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
@@ -55,6 +55,48 @@ returns one highest-priority candidate, and carries authority, validation, lease
55
55
  and spend stop conditions. It selects work; it does not claim a board task,
56
56
  publish a package, spend funds, or bypass caller authority.
57
57
 
58
+ ## HoloScript Source Canon
59
+
60
+ `source-canon` is the consumer-side language-sovereignty gate. Run it from the
61
+ root of a Git repository:
62
+
63
+ ```bash
64
+ npx holosystem source-canon \
65
+ --output runtime/visual-state/source-canon.hsplus \
66
+ --json
67
+ ```
68
+
69
+ The gate reads the fixed Git-tracked file inventory and accepts only `.holo`,
70
+ `.hs`, and `.hsplus` as canonical authored source. It then parses the actual
71
+ tracked bytes with the exact optional peer `@holoscript/core@8.0.14`; when that
72
+ parser is unavailable, the gate fails closed. This prevents JavaScript or another
73
+ language from becoming canonical merely by being renamed. Other HoloSystem
74
+ commands remain usable without the optional peer.
75
+
76
+ A caller cannot widen the registry with an extension allowlist. A future format
77
+ becomes canonical only when a HoloScript release owns its parser and adds it to
78
+ the package registry.
79
+
80
+ The command exits `0` only when at least one HoloScript source file exists and
81
+ every tracked file uses a canonical format. It exits `2` with every foreign
82
+ tracked path named otherwise. When `--output` is supplied, the durable
83
+ founder-visible projection must itself be a portable repository-relative
84
+ `.hsplus` path; JSON remains available only on standard output for transient
85
+ agent pipelines.
86
+
87
+ This gate audits Git-tracked canon. Dependency caches, runtime receipts, and
88
+ untracked generated artifacts are not silently promoted to source truth. They
89
+ remain non-canonical operational state and must stay untracked or move to an
90
+ external runtime store as the consumer migration proceeds.
91
+
92
+ ```js
93
+ import {
94
+ inspectGitTrackedSourceCanon,
95
+ inspectSourceCanon,
96
+ renderSourceCanonProjection,
97
+ } from '@holoscript/holosystem';
98
+ ```
99
+
58
100
  ## Substrate Closure
59
101
 
60
102
  The substrate closure replaces an implicit operating-system dependency tower
@@ -749,6 +791,9 @@ npx holosystem inspect holosystem.config.json --json
749
791
 
750
792
  # Pipeline input is supported.
751
793
  npx holosystem create --stdout | npx holosystem inspect - --json
794
+
795
+ # Prove that Git-tracked authoring is HoloScript-only.
796
+ npx holosystem source-canon --output source-canon.hsplus --json
752
797
  ```
753
798
 
754
799
  Creation never installs packages, connects to storage, acquires credentials, or
@@ -10,6 +10,7 @@ import {
10
10
  importDebianPackageSnapshot,
11
11
  importNpmPackageLock,
12
12
  inspectNativeBuildSource,
13
+ inspectGitTrackedSourceCanon,
13
14
  inspectHoloSystemConfig,
14
15
  inspectVmExecutor,
15
16
  inspectVmLaunchAsset,
@@ -19,6 +20,7 @@ import {
19
20
  runWhpxAppContainerVmLaunch,
20
21
  runWhpxSandboxedVmLaunch,
21
22
  runWhpxVmLaunch,
23
+ renderSourceCanonProjection,
22
24
  } from '../src/index.mjs';
23
25
 
24
26
  const CLI_RECEIPT_SCHEMA = 'holoscript.holosystem.cli-receipt.v1';
@@ -34,6 +36,7 @@ Usage:
34
36
  holosystem substrate-import-debian --status <status> (--packages <Packages> | --sources <json>) --maintainer-scripts <json> --config <file> [--output <file>] [--force] [--json]
35
37
  holosystem native-build-source --source <directory> [--json]
36
38
  holosystem native-build --plan <file> --source <directory> --executor <file> --artifact-dir <directory> [--output <receipt>] [--force] [--json]
39
+ holosystem source-canon [--output <projection.hsplus>] [--force] [--json]
37
40
  holosystem vm-executor --runtime <directory> [--json]
38
41
  holosystem vm-asset --kind <kernel|initrd> --file <file> [--json]
39
42
  holosystem vm-launch --plan <file> --runtime <directory> --kernel <file> --initrd <file> [--output <receipt>] [--force] [--json]
@@ -206,6 +209,63 @@ function writeJsonOutput(path, value, { force = false } = {}) {
206
209
  writeFileSync(absolute, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
207
210
  }
208
211
 
212
+ function writeSourceCanonProjection(path, value, { force = false } = {}) {
213
+ if (
214
+ typeof path !== 'string' ||
215
+ !path.endsWith('.hsplus') ||
216
+ /^[A-Za-z]:[\\/]/u.test(path) ||
217
+ /^(?:[\\/]{1,2}|~[\\/])/u.test(path) ||
218
+ path.replaceAll('\\', '/').split('/').some((segment) => segment === '..' || segment === '.')
219
+ ) {
220
+ throw new Error('--output must be a portable repository-relative .hsplus path.');
221
+ }
222
+ const absolute = resolve(process.cwd(), path);
223
+ if (existsSync(absolute) && !force)
224
+ throw new Error(`${path} already exists; use --force to replace it.`);
225
+ writeFileSync(absolute, value, 'utf8');
226
+ }
227
+
228
+ async function runSourceCanon(args) {
229
+ let parsed;
230
+ try {
231
+ parsed = parseArguments(args, {
232
+ output: 'value',
233
+ force: 'boolean',
234
+ json: 'boolean',
235
+ });
236
+ } catch (error) {
237
+ die(error.message, { json: args.includes('--json') });
238
+ }
239
+ const { options, positionals } = parsed;
240
+ if (positionals.length > 0) {
241
+ die('source-canon does not accept positional arguments.', { json: options.json });
242
+ }
243
+
244
+ let report;
245
+ try {
246
+ report = await inspectGitTrackedSourceCanon({ rootDirectory: process.cwd() });
247
+ if (options.output) {
248
+ writeSourceCanonProjection(options.output, renderSourceCanonProjection(report), {
249
+ force: options.force,
250
+ });
251
+ }
252
+ } catch (error) {
253
+ die(`Cannot inspect source canon: ${error.message}`, { json: options.json, code: 2 });
254
+ }
255
+
256
+ if (options.json) outputJson(report);
257
+ else {
258
+ process.stdout.write(
259
+ `Source canon: ${report.status} HoloScript=${report.summary.holoScriptFiles} foreign=${report.summary.foreignFiles}\n`
260
+ );
261
+ for (const issue of report.issues) {
262
+ process.stdout.write(`BLOCK ${issue.code} ${issue.path}: ${issue.message}\n`);
263
+ }
264
+ if (options.output) process.stdout.write(`Wrote ${options.output}\n`);
265
+ }
266
+ if (!report.verified) process.exitCode = 2;
267
+ }
268
+
209
269
  function runInspect(args) {
210
270
  let parsed;
211
271
  try {
@@ -701,6 +761,8 @@ if (!command || command === '--help' || command === '-h' || command === 'help')
701
761
  runNativeBuildSource(argv.slice(1));
702
762
  } else if (command === 'native-build') {
703
763
  runNativeBuildCommand(argv.slice(1));
764
+ } else if (command === 'source-canon') {
765
+ await runSourceCanon(argv.slice(1));
704
766
  } else if (command === 'vm-executor') {
705
767
  runVmExecutor(argv.slice(1));
706
768
  } else if (command === 'vm-asset') {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@holoscript/holosystem",
3
- "version": "0.2.5",
3
+ "version": "0.2.6",
4
4
  "description": "Portable bootstrap, consumption catalog, lineage, and bounded-work contract for HoloSystem consumers.",
5
5
  "releaseLane": "v0-preview",
6
6
  "keywords": [
@@ -43,7 +43,7 @@
43
43
  "node": ">=18"
44
44
  },
45
45
  "scripts": {
46
- "build": "node --check src/index.mjs && node --check src/catalog.mjs && node --check src/substrate.mjs && node --check src/substrate-import.mjs && node --check src/substrate-debian-release.mjs && node --check src/substrate-import-debian.mjs && node --check src/native-build.mjs && node --check src/vm-launch.mjs && node --check bin/holosystem.mjs",
46
+ "build": "node --check src/index.mjs && node --check src/catalog.mjs && node --check src/source-canon.mjs && node --check src/substrate.mjs && node --check src/substrate-import.mjs && node --check src/substrate-debian-release.mjs && node --check src/substrate-import-debian.mjs && node --check src/native-build.mjs && node --check src/vm-launch.mjs && node --check bin/holosystem.mjs",
47
47
  "test": "node --test test/*.test.mjs",
48
48
  "smoke": "node bin/holosystem.mjs --help",
49
49
  "check": "pnpm run build && pnpm run test && pnpm run smoke",
@@ -57,5 +57,13 @@
57
57
  "localBoundary": "Machine paths, credentials, private repositories, and operator state are caller inputs, never package defaults.",
58
58
  "supportBoundary": "Configuration, public discovery, lineage, and bounded selection are supported; installation, credential acquisition, board mutation, service mutation, spending, and publishing remain caller-authorized operations."
59
59
  }
60
+ },
61
+ "peerDependencies": {
62
+ "@holoscript/core": "8.0.14"
63
+ },
64
+ "peerDependenciesMeta": {
65
+ "@holoscript/core": {
66
+ "optional": true
67
+ }
60
68
  }
61
69
  }
package/src/index.mjs CHANGED
@@ -58,6 +58,14 @@ export {
58
58
  runWhpxVmLaunch,
59
59
  } from './vm-launch.mjs';
60
60
 
61
+ export {
62
+ HOLOSCRIPT_CANONICAL_SOURCE_EXTENSIONS,
63
+ HOLOSYSTEM_SOURCE_CANON_SCHEMA,
64
+ inspectGitTrackedSourceCanon,
65
+ inspectSourceCanon,
66
+ renderSourceCanonProjection,
67
+ } from './source-canon.mjs';
68
+
61
69
  export {
62
70
  HOLOSYSTEM_CATALOG_SCHEMA,
63
71
  HOLOSYSTEM_CONSUMER_INPUT_SCHEMA,
@@ -0,0 +1,307 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { createHash } from 'node:crypto';
3
+ import { readFileSync } from 'node:fs';
4
+ import { basename, resolve } from 'node:path';
5
+
6
+ export const HOLOSYSTEM_SOURCE_CANON_SCHEMA = 'holoscript.holosystem.source-canon.v1';
7
+ export const HOLOSCRIPT_CANONICAL_SOURCE_EXTENSIONS = Object.freeze([
8
+ '.holo',
9
+ '.hs',
10
+ '.hsplus',
11
+ ]);
12
+
13
+ const KNOWN_OPTIONS = new Set(['repository', 'trackedFiles', 'now']);
14
+ const PORTABLE_ID = /^[a-z0-9][a-z0-9._-]{0,63}$/iu;
15
+
16
+ function sha256(value) {
17
+ return `sha256:${createHash('sha256').update(value).digest('hex')}`;
18
+ }
19
+
20
+ function isRecord(value) {
21
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
22
+ }
23
+
24
+ function requireKnownOptions(value) {
25
+ for (const key of Object.keys(value)) {
26
+ if (!KNOWN_OPTIONS.has(key)) throw new TypeError(`Unknown source-canon option ${key}.`);
27
+ }
28
+ }
29
+
30
+ function normalizeRepository(value = {}) {
31
+ if (!isRecord(value)) throw new TypeError('repository must be an object.');
32
+ const id = value.id ?? 'workspace';
33
+ const head = value.head ?? null;
34
+ if (!PORTABLE_ID.test(id)) throw new TypeError('repository.id must be a portable identifier.');
35
+ if (head !== null && (typeof head !== 'string' || !/^[a-f0-9]{7,64}$/iu.test(head))) {
36
+ throw new TypeError('repository.head must be a Git object id or null.');
37
+ }
38
+ return { id, head };
39
+ }
40
+
41
+ function normalizeTrackedPath(value) {
42
+ if (
43
+ typeof value !== 'string' ||
44
+ value.length === 0 ||
45
+ value.length > 512 ||
46
+ /[\0-\x1f\x7f]/u.test(value) ||
47
+ /^[A-Za-z]:[\\/]/u.test(value) ||
48
+ /^(?:[\\/]{1,2}|~[\\/])/u.test(value)
49
+ ) {
50
+ throw new TypeError(`${String(value)} is not a portable repository-relative path.`);
51
+ }
52
+ const normalized = value.replaceAll('\\', '/');
53
+ const segments = normalized.split('/');
54
+ if (
55
+ segments.some((segment) => segment === '' || segment === '.' || segment === '..') ||
56
+ normalized.startsWith('.//')
57
+ ) {
58
+ throw new TypeError(`${value} is not a portable repository-relative path.`);
59
+ }
60
+ return normalized;
61
+ }
62
+
63
+ function sourceExtension(path) {
64
+ return HOLOSCRIPT_CANONICAL_SOURCE_EXTENSIONS.find((extension) => path.endsWith(extension)) ?? null;
65
+ }
66
+
67
+ function foreignExtension(path) {
68
+ const name = path.slice(path.lastIndexOf('/') + 1);
69
+ const index = name.lastIndexOf('.');
70
+ return index <= 0 ? '<none>' : name.slice(index).toLowerCase();
71
+ }
72
+
73
+ function buildSourceCanonReport(options, { sources = {}, parser = null } = {}) {
74
+ const repository = normalizeRepository(options.repository);
75
+ if (options.trackedFiles !== undefined && !Array.isArray(options.trackedFiles)) {
76
+ throw new TypeError('trackedFiles must be an array.');
77
+ }
78
+
79
+ const files = (options.trackedFiles ?? []).map(normalizeTrackedPath).sort();
80
+ if (new Set(files).size !== files.length) throw new TypeError('trackedFiles must be unique.');
81
+
82
+ const generatedAt = new Date(options.now ?? Date.now());
83
+ if (Number.isNaN(generatedAt.getTime())) throw new TypeError('now must be a valid date.');
84
+
85
+ const holoScriptFiles = [];
86
+ const foreignFiles = [];
87
+ const byFormat = Object.fromEntries(
88
+ HOLOSCRIPT_CANONICAL_SOURCE_EXTENSIONS.map((extension) => [extension, 0])
89
+ );
90
+ const foreignByFormat = {};
91
+
92
+ for (const path of files) {
93
+ const extension = sourceExtension(path);
94
+ if (extension) {
95
+ holoScriptFiles.push(path);
96
+ byFormat[extension] += 1;
97
+ continue;
98
+ }
99
+ foreignFiles.push(path);
100
+ const foreign = foreignExtension(path);
101
+ foreignByFormat[foreign] = (foreignByFormat[foreign] ?? 0) + 1;
102
+ }
103
+
104
+ const issues = foreignFiles.map((path) => ({
105
+ code: 'foreign-source-format',
106
+ path,
107
+ message: 'Git-tracked canon must be authored in a parser-owned HoloScript source format.',
108
+ }));
109
+ if (holoScriptFiles.length === 0) {
110
+ issues.unshift({
111
+ code: 'holoscript-source-missing',
112
+ path: '$',
113
+ message: 'At least one Git-tracked .holo, .hs, or .hsplus source file is required.',
114
+ });
115
+ }
116
+
117
+ for (const path of Object.keys(sources)) {
118
+ const normalized = normalizeTrackedPath(path);
119
+ if (!holoScriptFiles.includes(normalized)) {
120
+ throw new TypeError(`Source ${path} is not a tracked canonical HoloScript path.`);
121
+ }
122
+ if (typeof sources[path] !== 'string') {
123
+ throw new TypeError(`Source ${path} must be a string.`);
124
+ }
125
+ }
126
+
127
+ const parseChecks = holoScriptFiles.map((path) => {
128
+ const source = sources[path];
129
+ if (typeof parser !== 'function' || typeof source !== 'string') {
130
+ issues.push({
131
+ code: 'holoscript-parse-evidence-missing',
132
+ path,
133
+ message:
134
+ 'Canonical source requires repository-owned bytes and a real @holoscript/core parse result.',
135
+ });
136
+ return {
137
+ path,
138
+ status: 'not-run',
139
+ sourceDigest: typeof source === 'string' ? sha256(source) : null,
140
+ errors: [],
141
+ };
142
+ }
143
+ let parsed;
144
+ try {
145
+ parsed = parser(source);
146
+ } catch (error) {
147
+ parsed = {
148
+ success: false,
149
+ errors: [{ code: 'parser-threw', message: error?.message ?? String(error) }],
150
+ };
151
+ }
152
+ const errors = (Array.isArray(parsed?.errors) ? parsed.errors : []).slice(0, 8).map((error) => ({
153
+ code: error?.code ?? null,
154
+ message: error?.message ?? String(error),
155
+ line: Number.isInteger(error?.line) ? error.line : null,
156
+ column: Number.isInteger(error?.column) ? error.column : null,
157
+ }));
158
+ const passed = parsed?.success === true && errors.length === 0;
159
+ if (!passed) {
160
+ issues.push({
161
+ code: 'holoscript-source-invalid',
162
+ path,
163
+ message: 'Canonical source did not pass @holoscript/core.parse.',
164
+ });
165
+ }
166
+ return {
167
+ path,
168
+ status: passed ? 'passed' : 'failed',
169
+ sourceDigest: sha256(source),
170
+ errors,
171
+ };
172
+ });
173
+
174
+ const formatVerified = holoScriptFiles.length > 0 && foreignFiles.length === 0;
175
+ const parserVerified =
176
+ holoScriptFiles.length > 0 && parseChecks.every((item) => item.status === 'passed');
177
+ const verified = formatVerified && parserVerified && issues.length === 0;
178
+ const stableReceipt = {
179
+ schema: HOLOSYSTEM_SOURCE_CANON_SCHEMA,
180
+ scope: 'git-tracked-canon',
181
+ repository,
182
+ formatRegistry: [...HOLOSCRIPT_CANONICAL_SOURCE_EXTENSIONS],
183
+ trackedFiles: files,
184
+ holoScriptFiles,
185
+ foreignFiles,
186
+ parseChecks,
187
+ formatVerified,
188
+ parserVerified,
189
+ };
190
+
191
+ return {
192
+ schema: HOLOSYSTEM_SOURCE_CANON_SCHEMA,
193
+ generatedAt: generatedAt.toISOString(),
194
+ status: verified ? 'verified' : 'blocked',
195
+ verified,
196
+ formatVerified,
197
+ parserVerified,
198
+ scope: 'git-tracked-canon',
199
+ repository,
200
+ formatRegistry: {
201
+ owner: '@holoscript/holosystem',
202
+ extensions: [...HOLOSCRIPT_CANONICAL_SOURCE_EXTENSIONS],
203
+ callerExtensionsAccepted: false,
204
+ rule: 'New canonical extensions require a parser-owned HoloScript release; callers cannot widen the registry.',
205
+ },
206
+ summary: {
207
+ trackedFiles: files.length,
208
+ holoScriptFiles: holoScriptFiles.length,
209
+ foreignFiles: foreignFiles.length,
210
+ parsePassed: parseChecks.filter((item) => item.status === 'passed').length,
211
+ parseFailed: parseChecks.filter((item) => item.status === 'failed').length,
212
+ parseMissing: parseChecks.filter((item) => item.status === 'not-run').length,
213
+ byFormat,
214
+ foreignByFormat: Object.fromEntries(Object.entries(foreignByFormat).sort()),
215
+ },
216
+ holoScriptFiles,
217
+ foreignFiles,
218
+ parseChecks,
219
+ issues,
220
+ boundaries: {
221
+ untrackedRuntimeIsCanon: false,
222
+ dependencyCachesAreCanon: false,
223
+ generatedArtifactsAreCanonOnlyWhenTracked: true,
224
+ extensionOnlyClaimsAccepted: false,
225
+ },
226
+ receiptHash: sha256(JSON.stringify(stableReceipt)),
227
+ };
228
+ }
229
+
230
+ export function inspectSourceCanon(options = {}) {
231
+ if (!isRecord(options)) throw new TypeError('source-canon options must be an object.');
232
+ requireKnownOptions(options);
233
+ return buildSourceCanonReport(options);
234
+ }
235
+
236
+ function runGit(rootDirectory, args) {
237
+ const result = spawnSync('git', ['-C', rootDirectory, ...args], {
238
+ encoding: 'utf8',
239
+ stdio: ['ignore', 'pipe', 'pipe'],
240
+ });
241
+ if (result.status !== 0) {
242
+ throw new Error(`Git ${args.join(' ')} failed: ${String(result.stderr || '').trim()}`);
243
+ }
244
+ return result.stdout;
245
+ }
246
+
247
+ export async function inspectGitTrackedSourceCanon({ rootDirectory = process.cwd(), now } = {}) {
248
+ const root = resolve(rootDirectory);
249
+ const repositoryRoot = resolve(runGit(root, ['rev-parse', '--show-toplevel']).trim());
250
+ if (repositoryRoot.toLowerCase() !== root.toLowerCase()) {
251
+ throw new Error('source-canon must run from the repository root.');
252
+ }
253
+ const idCandidate = basename(root).toLowerCase();
254
+ const repository = {
255
+ id: PORTABLE_ID.test(idCandidate) ? idCandidate : 'workspace',
256
+ head: runGit(root, ['rev-parse', 'HEAD']).trim(),
257
+ };
258
+ const trackedFiles = runGit(root, ['ls-files', '-z'])
259
+ .split('\0')
260
+ .filter(Boolean);
261
+ const sources = Object.fromEntries(
262
+ trackedFiles
263
+ .map(normalizeTrackedPath)
264
+ .filter((path) => sourceExtension(path))
265
+ .map((path) => [path, readFileSync(resolve(root, path), 'utf8')])
266
+ );
267
+ let parser = null;
268
+ try {
269
+ const core = await import('@holoscript/core');
270
+ if (typeof core.parse === 'function') parser = core.parse;
271
+ } catch {
272
+ // The report fails closed with parse-evidence-missing issues below.
273
+ }
274
+ return buildSourceCanonReport({ repository, trackedFiles, now }, { sources, parser });
275
+ }
276
+
277
+ export function renderSourceCanonProjection(report) {
278
+ if (!isRecord(report) || report.schema !== HOLOSYSTEM_SOURCE_CANON_SCHEMA) {
279
+ throw new TypeError(`Expected ${HOLOSYSTEM_SOURCE_CANON_SCHEMA}.`);
280
+ }
281
+ return `composition "HoloSystemSourceCanon" {
282
+ state {
283
+ status: "${report.status}"
284
+ verified: ${report.verified}
285
+ formatVerified: ${report.formatVerified}
286
+ parserVerified: ${report.parserVerified}
287
+ scope: "${report.scope}"
288
+ trackedFiles: ${report.summary.trackedFiles}
289
+ holoScriptFiles: ${report.summary.holoScriptFiles}
290
+ foreignFiles: ${report.summary.foreignFiles}
291
+ holoFiles: ${report.summary.byFormat['.holo']}
292
+ hsFiles: ${report.summary.byFormat['.hs']}
293
+ hsplusFiles: ${report.summary.byFormat['.hsplus']}
294
+ parsePassed: ${report.summary.parsePassed}
295
+ parseFailed: ${report.summary.parseFailed}
296
+ parseMissing: ${report.summary.parseMissing}
297
+ receiptHash: "${report.receiptHash}"
298
+ }
299
+
300
+ object "LanguageSovereignty" {
301
+ canonicalFormats: ".holo,.hs,.hsplus"
302
+ callerExtensionsAccepted: false
303
+ migrationRequired: ${!report.verified}
304
+ }
305
+ }
306
+ `;
307
+ }