@holoscript/holosystem 0.2.5 → 0.2.7
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 +45 -0
- package/bin/holosystem.mjs +62 -0
- package/package.json +10 -2
- package/src/index.mjs +8 -0
- package/src/source-canon.mjs +307 -0
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.15`; 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
|
package/bin/holosystem.mjs
CHANGED
|
@@ -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.
|
|
3
|
+
"version": "0.2.7",
|
|
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.15"
|
|
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
|
+
}
|