graphlin 0.1.3 → 0.2.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.
Files changed (106) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.codex-plugin/plugin.json +1 -1
  3. package/README.md +12 -3
  4. package/docs/decision-service.md +393 -0
  5. package/docs/extension-authoring.md +553 -0
  6. package/docs/model-api.md +293 -0
  7. package/docs/usage.md +472 -0
  8. package/docs/visualizer-views.md +240 -0
  9. package/node_modules/@vscode/tree-sitter-wasm/LICENSE +21 -0
  10. package/node_modules/@vscode/tree-sitter-wasm/README.md +36 -0
  11. package/node_modules/@vscode/tree-sitter-wasm/SECURITY.md +41 -0
  12. package/node_modules/@vscode/tree-sitter-wasm/cgmanifest.json +16 -0
  13. package/node_modules/@vscode/tree-sitter-wasm/package.json +42 -0
  14. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-bash.wasm +0 -0
  15. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-c-sharp.wasm +0 -0
  16. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-cpp.wasm +0 -0
  17. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-css.wasm +0 -0
  18. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-go.wasm +0 -0
  19. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-ini.wasm +0 -0
  20. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-java.wasm +0 -0
  21. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-javascript.wasm +0 -0
  22. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-php.wasm +0 -0
  23. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-powershell.wasm +0 -0
  24. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-python.wasm +0 -0
  25. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-regex.wasm +0 -0
  26. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-ruby.wasm +0 -0
  27. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-rust.wasm +0 -0
  28. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-tsx.wasm +0 -0
  29. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-typescript.wasm +0 -0
  30. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter.js +4075 -0
  31. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter.wasm +0 -0
  32. package/node_modules/@vscode/tree-sitter-wasm/wasm/web-tree-sitter.d.ts +1027 -0
  33. package/package.json +78 -9
  34. package/plugin.json +4 -2
  35. package/runtime/architecture/analysis.mjs +344 -0
  36. package/runtime/architecture/controller.mjs +209 -0
  37. package/runtime/architecture/evidence.mjs +108 -0
  38. package/runtime/architecture/profile.mjs +56 -0
  39. package/runtime/core/evidence.mjs +43 -9
  40. package/runtime/core/graph.mjs +11 -6
  41. package/runtime/core/privacy.mjs +1 -0
  42. package/runtime/daemon/auth.mjs +7 -3
  43. package/runtime/daemon/diagnostics.mjs +1 -1
  44. package/runtime/daemon/extension-api.mjs +203 -0
  45. package/runtime/daemon/lineage.mjs +70 -0
  46. package/runtime/daemon/manager.mjs +9 -6
  47. package/runtime/daemon/model-api.mjs +728 -0
  48. package/runtime/daemon/model-persistence.mjs +220 -0
  49. package/runtime/daemon/server.mjs +81 -14
  50. package/runtime/daemon/settings.mjs +11 -3
  51. package/runtime/decisions/broker.mjs +349 -0
  52. package/runtime/decisions/contracts.mjs +179 -0
  53. package/runtime/decisions/evaluation.mjs +305 -0
  54. package/runtime/decisions/faults.mjs +32 -0
  55. package/runtime/decisions/index.mjs +818 -0
  56. package/runtime/decisions/profiles.mjs +93 -0
  57. package/runtime/decisions/questions.mjs +268 -0
  58. package/runtime/discovery/index.mjs +2 -0
  59. package/runtime/discovery/inventory.mjs +160 -0
  60. package/runtime/discovery/parser.mjs +40 -0
  61. package/runtime/discovery/structure.mjs +232 -0
  62. package/runtime/extensions/contracts.mjs +59 -0
  63. package/runtime/extensions/frame.mjs +64 -0
  64. package/runtime/extensions/index.mjs +9 -0
  65. package/runtime/extensions/manifest.mjs +95 -0
  66. package/runtime/extensions/packages.mjs +222 -0
  67. package/runtime/extensions/profiles.mjs +36 -0
  68. package/runtime/extensions/projection.mjs +130 -0
  69. package/runtime/extensions/registry.mjs +285 -0
  70. package/runtime/extensions/scene.mjs +105 -0
  71. package/runtime/extensions/sdk.d.ts +205 -0
  72. package/runtime/extensions/sdk.mjs +88 -0
  73. package/runtime/jev/index.mjs +13 -777
  74. package/runtime/jev/provider.mjs +101 -0
  75. package/runtime/jev/questions.mjs +16 -258
  76. package/runtime/jev/wire.mjs +17 -25
  77. package/runtime/model/changes.mjs +42 -0
  78. package/runtime/model/history.mjs +124 -0
  79. package/runtime/model/index.mjs +2 -0
  80. package/runtime/model/project-model.mjs +1020 -0
  81. package/runtime/model/records.mjs +240 -0
  82. package/runtime/pipeline.mjs +267 -55
  83. package/runtime/platform.mjs +254 -0
  84. package/runtime/visualizers/blocks.mjs +5 -0
  85. package/runtime/visualizers/c4.mjs +154 -0
  86. package/runtime/visualizers/changes.mjs +24 -0
  87. package/runtime/visualizers/code.mjs +5 -0
  88. package/runtime/visualizers/index.mjs +23 -0
  89. package/runtime/visualizers/structure.mjs +120 -0
  90. package/runtime/visualizers/timeline.mjs +66 -0
  91. package/runtime/web/app.js +225 -63
  92. package/runtime/web/extension-frame.js +128 -0
  93. package/runtime/web/index.html +38 -1
  94. package/runtime/web/model-client.js +162 -0
  95. package/runtime/web/platform.js +445 -0
  96. package/runtime/web/scene.js +111 -0
  97. package/runtime/web/style.css +51 -0
  98. package/schemas/graph.schema.json +4 -1
  99. package/scripts/arguments.mjs +5 -1
  100. package/scripts/build-packages.mjs +6 -2
  101. package/scripts/control.mjs +1 -1
  102. package/scripts/daemon.mjs +2 -1
  103. package/scripts/extensions.mjs +44 -0
  104. package/scripts/graphlin.mjs +23 -3
  105. package/scripts/onboarding.mjs +10 -3
  106. package/scripts/validate-packages.mjs +54 -8
@@ -7,6 +7,7 @@ import { publicPackageFiles, validatePackage } from './validate-packages.mjs';
7
7
 
8
8
  const SOURCE = fileURLToPath(new URL('../', import.meta.url));
9
9
  const SOURCE_DIRECTORIES = ['.claude-plugin', '.codex-plugin', 'skills', 'scripts', 'runtime', 'schemas', 'adapters'];
10
+ const PROTECTED_DIRECTORIES = [...SOURCE_DIRECTORIES, 'docs', 'node_modules'];
10
11
 
11
12
  async function assertDestination(destination, { directory = false } = {}) {
12
13
  const absolute = path.resolve(destination), root = path.parse(absolute).root;
@@ -57,7 +58,7 @@ async function copy(source, destination) {
57
58
  export async function buildPackages({ outputDir = path.join(SOURCE, 'dist'), sourceDir = SOURCE } = {}) {
58
59
  const source = path.resolve(sourceDir), output = path.resolve(outputDir);
59
60
  if (output === source || source.startsWith(`${output}${path.sep}`)) throw new Error('unsafe_output_directory');
60
- if (SOURCE_DIRECTORIES.some(file => output === path.join(source, file) || output.startsWith(`${path.join(source, file)}${path.sep}`))) {
61
+ if (PROTECTED_DIRECTORIES.some(file => output === path.join(source, file) || output.startsWith(`${path.join(source, file)}${path.sep}`))) {
61
62
  throw new Error('unsafe_output_directory');
62
63
  }
63
64
  // Preflight every output before replacing even the first generated profile.
@@ -92,6 +93,9 @@ export async function buildPackages({ outputDir = path.join(SOURCE, 'dist'), sou
92
93
  await mkdir(temporary);
93
94
  try {
94
95
  const profileFiles = files.filter(file => profile === 'claude' || file !== '.claude-plugin/plugin.json');
96
+ // publicPackageFiles includes every reviewed file of the full pinned
97
+ // parser under Graphlin's own node_modules. Never resolve a hoisted or
98
+ // inspected-project dependency, and never copy an arbitrary dependency tree.
95
99
  for (const file of profileFiles) await copy(path.join(source, file), path.join(temporary, file));
96
100
  const metadata = JSON.parse(await readFile(path.join(temporary, 'package.json'), 'utf8'));
97
101
  metadata.files = metadata.files.filter(file => profileFiles.includes(file.slice(2)));
@@ -114,7 +118,7 @@ export async function buildPackages({ outputDir = path.join(SOURCE, 'dist'), sou
114
118
  await chmod(path.join(temporary, 'scripts', file), 0o755);
115
119
  }
116
120
  await writeFile(path.join(temporary, 'PACKAGE-NOTES.md'),
117
- `# Graphlin ${profile} package\n\nNode.js 22.14+; macOS/Linux. Self-contained source bundle; no dependency install.\n` +
121
+ `# Graphlin ${profile} package\n\nNode.js 22.14+; macOS/Linux. Self-contained runtime and parser bundle; no dependency install.\n` +
118
122
  'Run scripts/graphlin.mjs --help for local controls. This package does not install itself.\n' +
119
123
  'Hook activation/trust has not been certified. Kiro is inactive and experimental.\n' +
120
124
  'See adapters/README.md and skills/graphlin/SKILL.md for privacy and coverage.\n');
@@ -39,7 +39,7 @@ async function handle(line) {
39
39
  const supported = ['2024-11-05', '2025-03-26', '2025-06-18'];
40
40
  return result({ protocolVersion: supported.includes(message.params?.protocolVersion) ? message.params.protocolVersion : '2025-06-18',
41
41
  capabilities: { tools: { listChanged: false } },
42
- serverInfo: { name: 'graphlin', version: '0.1.3' },
42
+ serverInfo: { name: 'graphlin', version: '0.2.1' },
43
43
  instructions: 'Controls only. Passive host hooks provide observations when separately activated. No drawing calls after each action.' });
44
44
  }
45
45
  if (message.method === 'ping') return result({});
@@ -5,7 +5,8 @@ process.umask(0o077);
5
5
  let server;
6
6
  try {
7
7
  const options = parseArguments(process.argv.slice(2), { worker: true });
8
- const policy = { transmitSource: options.allowSource, persistEvidence: options.persistEvidence,
8
+ const policy = { readSource: options.localSource || options.allowSource,
9
+ transmitSource: options.allowSource, persistEvidence: options.persistEvidence,
9
10
  displayEvidence: options.displayEvidence };
10
11
  let decisionService;
11
12
  if (options.mode === 'demo') {
@@ -0,0 +1,44 @@
1
+ import path from 'node:path';
2
+ import { projectPaths, defaultDataDir } from '../runtime/daemon/paths.mjs';
3
+ import { createExtensionRegistry, extensionError } from '../runtime/extensions/index.mjs';
4
+
5
+ /** Parent CLI dispatches its already-parsed extension arguments here. This
6
+ * module neither parses global CLI options nor starts/stops the daemon. */
7
+ export async function runExtensions(args, {
8
+ projectRoot, dataDir = defaultDataDir(), signal, transport, output = process.stdout,
9
+ registry: suppliedRegistry,
10
+ } = {}) {
11
+ if (!Array.isArray(args) || args.some(value => typeof value !== 'string')) throw extensionError('invalid_extension_arguments');
12
+ const [command = 'list', source, ...rest] = args;
13
+ if (!['list', 'add', 'update', 'remove', 'doctor', 'dev'].includes(command) || rest.length ||
14
+ (['add', 'update', 'remove', 'dev'].includes(command) ? !source : source !== undefined)) {
15
+ throw extensionError('usage_extensions_list_add_update_remove_doctor_dev');
16
+ }
17
+ let registry = suppliedRegistry;
18
+ if (!registry) {
19
+ const paths = await projectPaths(projectRoot ?? process.cwd(), dataDir);
20
+ registry = await createExtensionRegistry({ dataDir: paths.dataDir, projectId: paths.projectId, transport });
21
+ }
22
+ const resolveSource = value => value.startsWith('.') || path.isAbsolute(value)
23
+ ? path.resolve(projectRoot ?? process.cwd(), value) : value;
24
+ let result;
25
+ if (command === 'list') result = await registry.list();
26
+ if (command === 'doctor') result = await registry.doctor();
27
+ if (command === 'remove') result = await registry.remove(source);
28
+ if (command === 'add' || command === 'update') result = await registry.install(resolveSource(source), { signal });
29
+ if (command === 'dev') {
30
+ if (!(source.startsWith('.') || path.isAbsolute(source))) throw extensionError('development_directory_required');
31
+ result = await registry.dev(resolveSource(source), { signal });
32
+ }
33
+ if (output) {
34
+ if (Array.isArray(result)) output.write(result.length ? result.map(row =>
35
+ `${row.id} ${row.version} ${row.digest.slice(0, 12)}${row.development ? ' development' : ''} ${row.grant ? 'granted' : 'approval required'}`
36
+ ).join('\n') + '\n' : 'No visualizer extensions installed.\n');
37
+ else if (command === 'doctor') output.write(`${result.ok ? 'Extension checks passed.' : 'Extension checks failed.'}\n${
38
+ result.results.map(row => `${row.id ?? 'catalogue'}: ${row.code}`).join('\n')}${result.results.length ? '\n' : ''}`);
39
+ else output.write(command === 'remove' ? `Removed ${result.id} and its grants.\n` :
40
+ `Installed ${result.id} ${result.version}${result.development ? ' (development snapshot)' : ''}. ${
41
+ result.grant ? 'Existing digest approval retained.' : 'Project approval is required before data delivery.'}\n`);
42
+ }
43
+ return result;
44
+ }
@@ -4,11 +4,12 @@ import { parseArguments } from './arguments.mjs';
4
4
  import { initOnboarding, uninstallOnboarding, needsOnboarding, openViewer, agentInstructions } from './onboarding.mjs';
5
5
  import { readSettings } from '../runtime/daemon/settings.mjs';
6
6
  import { projectPaths } from '../runtime/daemon/paths.mjs';
7
+ import { runExtensions } from './extensions.mjs';
7
8
 
8
9
  const HELP = `Graphlin — local architecture and activity viewer (Node.js 22.14+, macOS/Linux).
9
10
  graphlin Guided setup if needed, then foreground start
10
- init [--host claude|codex|both] [--allow-source|--no-source] [--replace-key]
11
- start --project PATH [--allow-source|--no-source] [--persist-evidence] [--no-display-evidence] [--background] [--no-open]
11
+ init [--host claude|codex|both] [--allow-source|--local-source|--no-source] [--replace-key]
12
+ start --project PATH [--allow-source|--local-source|--no-source] [--persist-evidence] [--no-display-evidence] [--background] [--no-open]
12
13
  open --project PATH Open/reopen the viewer; start detached if needed
13
14
  uninstall [--host claude|codex|both]
14
15
  stop --project PATH
@@ -17,10 +18,16 @@ const HELP = `Graphlin — local architecture and activity viewer (Node.js 22.14
17
18
  demo [--data-dir PATH] [--background]
18
19
  export --project PATH
19
20
  logs --project PATH [--file PATH]
21
+ extensions list List installed visualizers and approvals
22
+ extensions add PACKAGE@VERSION Install without running package scripts
23
+ extensions dev ./DIRECTORY Install a local development snapshot
24
+ extensions update PACKAGE@VERSION Replace an installed visualizer
25
+ extensions remove ID
26
+ extensions doctor
20
27
  All commands accept --project PATH and --data-dir PATH (or GRAPHLIN_DATA_DIR).
21
28
  init installs for your user account using the host CLIs; it saves project consent
22
29
  and offers a masked key prompt only in a terminal. Non-interactive init requires
23
- --host and --allow-source or --no-source; source also needs a saved/environment key.
30
+ --host and one source mode; remote source also needs a saved/environment key.
24
31
  init --replace-key replaces a saved key at the masked prompt; it requires a terminal.
25
32
  uninstall removes only Graphlin host plugins for all projects, retaining keys,
26
33
  history, packages and marketplace registrations; it resets current project consent.
@@ -31,6 +38,10 @@ Omitted policy flags reuse current/saved consent. Without consent: metadata only
31
38
  --no-source explicitly opts out; --allow-source permits sanitized source/public
32
39
  intent to TypeSafe using TYPESAFE_API_KEY or the privately saved key. Approved evidence
33
40
  is displayed by default; excerpts are persisted only with --persist-evidence.
41
+ --local-source parses supported source on this machine, without remote decisions.
42
+ Metadata mode inventories paths without opening source files. Visualizers require
43
+ project approval in the viewer before receiving data; custom renderers run in a
44
+ sandboxed frame. Install or mount does not run decision profiles.
34
45
  Credentials, environment files, excluded paths, binary and oversized files are
35
46
  filtered locally. Replay history is bounded to 7 days; an expired stopped-daemon
36
47
  snapshot is deleted on the next startup. Current snapshots are private and bounded.
@@ -47,6 +58,15 @@ artifact identity, including files since deleted.
47
58
  try {
48
59
  const args = process.argv.slice(2);
49
60
  if (args.includes('--help') || args[0] === 'help') process.stdout.write(HELP);
61
+ else if (args[0] === 'extensions') {
62
+ const globalArgs = [], extensionArgs = [];
63
+ for (let index = 1; index < args.length; index++) {
64
+ if (['--project', '--data-dir'].includes(args[index])) globalArgs.push(args[index], args[++index]);
65
+ else extensionArgs.push(args[index]);
66
+ }
67
+ const { projectRoot, dataDir } = parseArguments(['status', ...globalArgs]);
68
+ await runExtensions(extensionArgs, { projectRoot, dataDir });
69
+ }
50
70
  else {
51
71
  const options = parseArguments(args);
52
72
  const print = result => process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
@@ -313,13 +313,19 @@ export async function initOnboarding(options, dependencies = {}) {
313
313
  write(`Project: ${JSON.stringify(paths.projectRoot)}\n`);
314
314
  if (upgrading) write('Updating every recorded Graphlin host to keep the installed version consistent.\n');
315
315
  let allowSource = options.allowSource ?? (resuming ? saved.policy?.allowSource : undefined);
316
+ let localSource = options.localSource === true ||
317
+ (resuming && options.allowSource === undefined && saved.policy?.localSource === true);
316
318
  if (allowSource === undefined) {
317
- write('Source mode sends locally filtered source excerpts, user prompts, and public agent messages to TypeSafe. Metadata mode sends none of these and needs no key.\n');
318
- allowSource = await choose('For this project, choose source or metadata: ', ['source', 'metadata'], prompt) === 'source';
319
+ write('Source mode sends locally filtered source excerpts and public messages to the decision provider (Jev by default). Local mode parses code on this machine without sending it. Metadata mode reads no source content. Local and metadata need no key.\n');
320
+ const mode = await choose('For this project, choose source, local, or metadata: ', ['source', 'local', 'metadata'], prompt);
321
+ allowSource = mode === 'source';
322
+ localSource = mode === 'local';
319
323
  }
320
324
  const policy = { ...defaults, ...saved.policy, allowSource,
321
325
  persistEvidence: options.persistEvidence ?? saved.policy?.persistEvidence ?? false,
322
326
  displayEvidence: options.displayEvidence ?? saved.policy?.displayEvidence ?? true };
327
+ if (localSource && !allowSource) policy.localSource = true;
328
+ else delete policy.localSource;
323
329
  // Explicit setup plus source consent authorizes saving the supplied key so
324
330
  // later launches do not depend on this terminal's environment.
325
331
  let apiKey = allowSource && env.TYPESAFE_API_KEY ? env.TYPESAFE_API_KEY : undefined;
@@ -399,7 +405,8 @@ export async function uninstallOnboarding(options, dependencies = {}) {
399
405
  await record();
400
406
  write(`Graphlin removed from ${host}.\n`);
401
407
  }
402
- await saveSettings(paths, { policy: { ...defaults, ...saved.policy, allowSource: false, persistEvidence: false } });
408
+ await saveSettings(paths, { policy: { ...defaults, ...saved.policy, allowSource: false,
409
+ ...(saved.policy?.localSource ? { localSource: false } : {}), persistEvidence: false } });
403
410
  write('Source and evidence-persistence consent reset for this project. A running viewer keeps its current policy until stopped; use graphlin stop.\n');
404
411
  return { removed, ...(cancelledPending.length ? { cancelledPending } : {}), retainedData: true };
405
412
  }
@@ -4,6 +4,51 @@ import path from 'node:path';
4
4
  import { fileURLToPath } from 'node:url';
5
5
  import assert from 'node:assert/strict';
6
6
 
7
+ const PARSER_NAME = '@vscode/tree-sitter-wasm';
8
+ const PARSER_VERSION = '0.3.1';
9
+ // The complete published 0.3.1 package, not a pruned grammar subset. Keep this
10
+ // explicit: arbitrary new files under node_modules must never enter a release.
11
+ const PARSER_FILES = [
12
+ 'LICENSE', 'README.md', 'SECURITY.md', 'cgmanifest.json', 'package.json',
13
+ 'wasm/tree-sitter.js', 'wasm/tree-sitter.wasm', 'wasm/web-tree-sitter.d.ts',
14
+ ...['bash', 'c-sharp', 'cpp', 'css', 'go', 'ini', 'java', 'javascript', 'php',
15
+ 'powershell', 'python', 'regex', 'ruby', 'rust', 'tsx', 'typescript']
16
+ .map(language => `wasm/tree-sitter-${language}.wasm`),
17
+ ];
18
+ const SDK_EXPORTS = {
19
+ './extensions/sdk': {
20
+ types: './runtime/extensions/sdk.d.ts',
21
+ import: './runtime/extensions/sdk.mjs',
22
+ default: './runtime/extensions/sdk.mjs',
23
+ },
24
+ './extensions/scene': './runtime/extensions/scene.mjs',
25
+ };
26
+
27
+ async function assertRegularFile(root, file) {
28
+ let current = root;
29
+ for (const component of file.split('/')) {
30
+ current = path.join(current, component);
31
+ assert.equal((await lstat(current)).isSymbolicLink(), false, 'package_symlink_rejected');
32
+ }
33
+ assert.ok((await lstat(current)).isFile(), 'package_file_must_be_regular');
34
+ }
35
+
36
+ export async function bundledPackageFiles(root, metadata) {
37
+ metadata ??= JSON.parse(await readFile(path.join(root, 'package.json'), 'utf8'));
38
+ assert.deepEqual(metadata.bundledDependencies, [PARSER_NAME], 'parser_bundle_required');
39
+ assert.equal(metadata.dependencies?.[PARSER_NAME], PARSER_VERSION, 'parser_dependency_must_be_pinned');
40
+ const base = `node_modules/${PARSER_NAME}`;
41
+ await assertRegularFile(root, `${base}/package.json`);
42
+ const parser = JSON.parse(await readFile(path.join(root, base, 'package.json'), 'utf8'));
43
+ assert.equal(parser.name, PARSER_NAME, 'unexpected_parser_package');
44
+ assert.equal(parser.version, PARSER_VERSION, 'unexpected_parser_version');
45
+ assert.equal(parser.license, 'MIT', 'unexpected_parser_license');
46
+ assert.deepEqual(Object.keys(parser.dependencies ?? {}), [], 'unexpected_parser_dependencies');
47
+ const files = PARSER_FILES.map(file => `${base}/${file}`);
48
+ for (const file of files) await assertRegularFile(root, file);
49
+ return files;
50
+ }
51
+
7
52
  // Dependency-free checks for this package's deliberately small manifest profile.
8
53
  // This is not a replacement for a host's schema validation or activation/trust.
9
54
  export async function publicPackageFiles(root) {
@@ -22,21 +67,22 @@ export async function publicPackageFiles(root) {
22
67
  assert.ok(typeof file === 'string' && file.length > 0 && !file.includes('\\') &&
23
68
  !path.posix.isAbsolute(file) && !/[*?[\]{}!\u0000-\u001f]/.test(file) &&
24
69
  file.split('/').every(part => part && part !== '.' && part !== '..'), 'invalid_package_file');
25
- assert.ok(/^(?:package\.json|LICENSE|README\.md|plugin\.json|mcp\.json|\.mcp\.json|\.claude-plugin\/plugin\.json|\.codex-plugin\/plugin\.json|adapters\/(?:README\.md|(?:claude|codex|kiro)\/(?:hooks|profile)\.json)|skills\/graphlin\/SKILL\.md|runtime\/(?:[a-z0-9-]+\/)*[a-z0-9-]+\.(?:mjs|js|css|html)|schemas\/[a-z0-9-]+\.schema\.json|scripts\/(?:arguments|build-packages|collector|control|daemon|graphlin|onboarding|validate-packages)\.mjs|scripts\/collect\.sh)$/.test(file),
70
+ assert.ok(/^(?:package\.json|LICENSE|README\.md|plugin\.json|mcp\.json|\.mcp\.json|\.claude-plugin\/plugin\.json|\.codex-plugin\/plugin\.json|adapters\/(?:README\.md|(?:claude|codex|kiro)\/(?:hooks|profile)\.json)|skills\/graphlin\/SKILL\.md|runtime\/(?:[a-z0-9-]+\/)*[a-z0-9-]+\.(?:mjs|js|css|html|d\.ts)|docs\/(?:decision-service|extension-authoring|model-api|visualizer-views|usage)\.md|schemas\/[a-z0-9-]+\.schema\.json|scripts\/(?:arguments|build-packages|collector|control|daemon|extensions|graphlin|onboarding|validate-packages)\.mjs|scripts\/collect\.sh)$/.test(file),
26
71
  'unexpected_public_package_file');
27
- let current = root;
28
- for (const component of file.split('/')) {
29
- current = path.join(current, component);
30
- assert.equal((await lstat(current)).isSymbolicLink(), false, 'package_symlink_rejected');
31
- }
32
- assert.ok((await lstat(current)).isFile(), 'package_file_must_be_regular');
72
+ await assertRegularFile(root, file);
33
73
  }
34
- return files;
74
+ return [...files, ...await bundledPackageFiles(root, metadata)];
35
75
  }
36
76
 
37
77
  export async function validatePackage(root) {
38
78
  const json = async name => JSON.parse(await readFile(path.join(root, name), 'utf8'));
39
79
  const metadata = await json('package.json');
80
+ await bundledPackageFiles(root, metadata);
81
+ assert.deepEqual(metadata.exports, SDK_EXPORTS, 'invalid_sdk_exports');
82
+ for (const file of ['runtime/extensions/sdk.mjs', 'runtime/extensions/sdk.d.ts', 'runtime/extensions/scene.mjs']) {
83
+ assert.ok(metadata.files.includes(`./${file}`), 'sdk_export_must_be_packaged');
84
+ await assertRegularFile(root, file);
85
+ }
40
86
  const manifest = await json('plugin.json'), portable = await json('mcp.json');
41
87
  assert.equal(manifest.$schema, 'https://agent-plugins.org/schemas/1.0.0/plugin.schema.json');
42
88
  assert.equal(manifest.name, 'graphlin');