@n8r/surface 0.1.0-experimental.2 → 0.1.0-experimental.5

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 (118) hide show
  1. package/README.md +30 -4
  2. package/dist/cli.js +174 -22
  3. package/dist/codec.d.ts +2 -0
  4. package/dist/codec.js +22 -7
  5. package/dist/codex.d.ts +1 -1
  6. package/dist/codex.js +26 -10
  7. package/dist/contracts.d.ts +108 -2
  8. package/dist/cyclonedx-schemas/bom-1.7.schema.json +6700 -0
  9. package/dist/cyclonedx-schemas/cryptography-defs.schema.json +576 -0
  10. package/dist/cyclonedx-schemas/jsf-0.82.schema.json +240 -0
  11. package/dist/cyclonedx-schemas/spdx.schema.json +786 -0
  12. package/dist/cyclonedx-validation.d.ts +1 -0
  13. package/dist/cyclonedx-validation.js +45 -0
  14. package/dist/html.js +27 -3
  15. package/dist/recent.d.ts +9 -0
  16. package/dist/recent.js +71 -0
  17. package/dist/revisions.js +42 -0
  18. package/dist/skill-artifacts-cli.d.ts +1 -0
  19. package/dist/skill-artifacts-cli.js +13 -0
  20. package/dist/skill-artifacts.d.ts +15 -0
  21. package/dist/skill-artifacts.js +184 -0
  22. package/dist/skill-cyclonedx.d.ts +26 -0
  23. package/dist/skill-cyclonedx.js +214 -0
  24. package/dist/skill-presentation.d.ts +10 -0
  25. package/dist/skill-presentation.js +108 -0
  26. package/dist/skill-surface-cli.d.ts +1 -0
  27. package/dist/skill-surface-cli.js +37 -0
  28. package/dist/skill-surface-store.d.ts +7 -0
  29. package/dist/skill-surface-store.js +126 -0
  30. package/dist/surface-bom.d.ts +73 -0
  31. package/dist/surface-bom.js +139 -0
  32. package/dist/surface-draft.d.ts +6 -3
  33. package/dist/surface-draft.js +20 -3
  34. package/dist/surface-instructions.d.ts +2 -1
  35. package/dist/surface-instructions.js +10 -1
  36. package/dist/surface-overview.d.ts +15 -0
  37. package/dist/surface-overview.js +86 -0
  38. package/dist/surface-store.d.ts +5 -7
  39. package/dist/surface-store.js +34 -2
  40. package/dist/surface-view.d.ts +3 -0
  41. package/dist/surface-view.js +50 -0
  42. package/dist/validate.js +298 -0
  43. package/docs/n8r-surface/cli-bundles.md +72 -0
  44. package/docs/n8r-surface/command-outcomes.md +151 -4
  45. package/docs/n8r-surface/contracts.md +10 -0
  46. package/docs/n8r-surface/critical-path.md +21 -0
  47. package/docs/n8r-surface/first-use-access.md +143 -0
  48. package/docs/n8r-surface/first-use-overview.md +262 -0
  49. package/docs/n8r-surface/getting-started.md +55 -88
  50. package/docs/n8r-surface/guided-journey-verification.md +137 -0
  51. package/docs/n8r-surface/implementation-decisions.md +14 -14
  52. package/docs/n8r-surface/instance-first-research.md +209 -0
  53. package/docs/n8r-surface/npm-distribution.md +39 -69
  54. package/docs/n8r-surface/npm-release.md +141 -14
  55. package/docs/n8r-surface/product-direction.md +327 -0
  56. package/docs/n8r-surface/researcher-skill.md +105 -51
  57. package/docs/n8r-surface/security-assurance-strategy.md +267 -0
  58. package/docs/n8r-surface/skill-bom-verification.md +142 -0
  59. package/docs/n8r-surface/skill-cyclonedx-verification.md +144 -0
  60. package/docs/n8r-surface/skill-cyclonedx.md +85 -0
  61. package/docs/n8r-surface/skill-manual.md +129 -0
  62. package/docs/n8r-surface/skill-surface-artifacts.md +234 -0
  63. package/docs/n8r-surface/skill-usefulness-verification.md +135 -0
  64. package/docs/n8r-surface/skill-workspace-automation.md +359 -0
  65. package/docs/n8r-surface/surface-description-contract.md +224 -0
  66. package/docs/n8r-surface/surface-descriptions.md +173 -0
  67. package/docs/n8r-surface/surface-relationships.md +231 -0
  68. package/docs/n8r-surface/tester-candidate.md +158 -0
  69. package/docs/n8r-surface/versioning.md +55 -66
  70. package/docs/n8r-surface/workspace-human-check.md +101 -0
  71. package/docs/n8r-surface/workspace-installation.md +68 -0
  72. package/docs/n8r-surface/workspace-verification.md +350 -0
  73. package/docs/n8r-surface/workspace.md +83 -0
  74. package/examples/researcher/README.md +8 -0
  75. package/examples/researcher/cyclonedx/configured/README.md +7 -0
  76. package/examples/researcher/cyclonedx/configured/manifest.json +10 -0
  77. package/examples/researcher/cyclonedx/configured/mapping.json +323 -0
  78. package/examples/researcher/cyclonedx/configured/surface-records.json +1275 -0
  79. package/examples/researcher/cyclonedx/configured/surface.cdx.json +79 -0
  80. package/examples/researcher/descriptions/README.md +29 -0
  81. package/examples/researcher/descriptions/bound-r1.json +141 -0
  82. package/examples/researcher/descriptions/bound-r2.json +153 -0
  83. package/examples/researcher/descriptions/capability.json +167 -0
  84. package/examples/researcher/descriptions/research-r1.json +153 -0
  85. package/examples/researcher/descriptions/research-r2.json +157 -0
  86. package/examples/researcher/descriptions/sources.md +24 -0
  87. package/examples/researcher/relationships/README.md +34 -0
  88. package/examples/researcher/relationships/bound-r1.json +346 -0
  89. package/examples/researcher/relationships/bound-r2.json +358 -0
  90. package/examples/researcher/relationships/capability.json +325 -0
  91. package/examples/researcher/relationships/config/assistant.env.example +4 -0
  92. package/examples/researcher/relationships/research-r1.json +300 -0
  93. package/examples/researcher/relationships/research-r2.json +304 -0
  94. package/examples/researcher/relationships/sources.md +38 -0
  95. package/examples/researcher/surface-bom/README.md +17 -0
  96. package/examples/researcher/surface-bom/initial.cdx.json +188 -0
  97. package/package.json +1 -1
  98. package/plugins/n8r-surface/.codex-plugin/plugin.json +2 -2
  99. package/plugins/n8r-surface/LICENSING.md +3 -0
  100. package/plugins/n8r-surface/plugin.json +2 -2
  101. package/plugins/n8r-surface/skills/research/SKILL.md +79 -38
  102. package/plugins/n8r-surface/skills/research/references/artifacts.md +152 -0
  103. package/plugins/n8r-surface/skills/research/references/cyclonedx.md +207 -0
  104. package/plugins/n8r-surface/skills/research/references/discovery-map.md +20 -2
  105. package/plugins/n8r-surface/skills/research/references/examples/bound-r1.json +346 -0
  106. package/plugins/n8r-surface/skills/research/references/examples/capability.json +325 -0
  107. package/plugins/n8r-surface/skills/research/references/examples/research-r1.json +300 -0
  108. package/plugins/n8r-surface/skills/research/references/examples/surface.cdx.json +188 -0
  109. package/plugins/n8r-surface/skills/research/references/first-use-access.md +97 -0
  110. package/plugins/n8r-surface/skills/research/references/first-use-overview.md +101 -0
  111. package/plugins/n8r-surface/skills/research/references/instance-first.md +125 -0
  112. package/plugins/n8r-surface/skills/research/references/working-notes.md +6 -1
  113. package/plugins/n8r-surface/skills/research/references/workspace.md +89 -0
  114. package/plugins/n8r-surface/skills/research/scripts/THIRD-PARTY-NOTICES.txt +273 -0
  115. package/plugins/n8r-surface/skills/research/scripts/artifacts.mjs +8348 -0
  116. package/plugins/n8r-surface/skills/research/scripts/cyclonedx.mjs +28113 -0
  117. package/plugins/n8r-surface/skills/research/scripts/surface.mjs +27800 -0
  118. package/plugins/n8r-surface/skills/research/scripts/workspace.mjs +120 -0
@@ -0,0 +1 @@
1
+ export declare function validateBom(bom: unknown): void;
@@ -0,0 +1,45 @@
1
+ /** Standard validation shared by the legacy reader and BOM authority. */
2
+ import { Ajv } from 'ajv';
3
+ import formats from 'ajv-formats';
4
+ import extendedFormats from 'ajv-formats-draft2019';
5
+ import bomSchema from './cyclonedx-schemas/bom-1.7.schema.json' with { type: 'json' };
6
+ import cryptoSchema from './cyclonedx-schemas/cryptography-defs.schema.json' with { type: 'json' };
7
+ import spdxSchema from './cyclonedx-schemas/spdx.schema.json' with { type: 'json' };
8
+ import jsfSchema from './cyclonedx-schemas/jsf-0.82.schema.json' with { type: 'json' };
9
+ function refuse() { throw Error('CycloneDX 1.7 schema/reference validation failed. Input values withheld.'); }
10
+ function guard(fn) { try {
11
+ return fn();
12
+ }
13
+ catch {
14
+ return refuse();
15
+ } }
16
+ const ajv = new Ajv({ strict: false, allErrors: false, validateFormats: true });
17
+ // ajv-formats CJS interop is normalized by Node and the standalone bundler.
18
+ formats(ajv);
19
+ extendedFormats(ajv);
20
+ // The upstream IRI checker rejects URNs because uri-js parses their NSS
21
+ // separately from path. Accept only the standard BOM-Link grammar here.
22
+ const iriReference = ajv.formats['iri-reference'];
23
+ ajv.addFormat('iri-reference', (value) => /^urn:cdx:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\/[1-9][0-9]*(?:#[^\s]+)?$/.test(value) || iriReference(value));
24
+ ajv.addSchema(spdxSchema);
25
+ ajv.addSchema(jsfSchema);
26
+ ajv.addSchema(cryptoSchema);
27
+ const schemaCheck = ajv.compile(bomSchema);
28
+ export function validateBom(bom) {
29
+ guard(() => {
30
+ if (!schemaCheck(bom))
31
+ refuse();
32
+ const b = bom;
33
+ if (b.bomFormat !== 'CycloneDX' || b.specVersion !== '1.7')
34
+ refuse();
35
+ const refs = new Set();
36
+ for (const n of [...(b.components ?? []), ...(b.services ?? [])]) {
37
+ if (typeof n['bom-ref'] !== 'string' || refs.has(n['bom-ref']))
38
+ refuse();
39
+ refs.add(n['bom-ref']);
40
+ }
41
+ for (const d of b.dependencies ?? [])
42
+ if (!refs.has(d.ref) || d.dependsOn?.some((r) => !refs.has(r)))
43
+ refuse();
44
+ });
45
+ }
package/dist/html.js CHANGED
@@ -1,6 +1,7 @@
1
- import { decode, encode } from './codec.js';
2
- import { checkSize, fail } from './portable.js';
3
- import { validateArtefact } from './validate.js';
1
+ import { decode, encode, decodeJsonValue } from './codec.js';
2
+ import { checkSize, fail, canonical } from './portable.js';
3
+ import { validateArtefact, verifyReferences } from './validate.js';
4
+ import { renderBundleHtml } from './skill-presentation.js';
4
5
  const escape = (text) => text.replace(/[&<>"']/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]));
5
6
  function display(value) {
6
7
  if (Array.isArray(value))
@@ -44,6 +45,29 @@ export function readResearchHtml(html) {
44
45
  const record = decode(blocks[0][1], 'json');
45
46
  if (record.contract !== 'n8r.research/0.1')
46
47
  fail('$html', 'requires research record');
48
+ if (html.includes('<meta name="n8r-presentation" content="1">')) {
49
+ const contexts = [...html.matchAll(/<script type="application\/json" id="n8r-context">([\s\S]*?)<\/script>/g)];
50
+ if (contexts.length !== 1)
51
+ fail('$html', 'requires exactly one presentation context');
52
+ const context = decodeJsonValue(contexts[0][1]);
53
+ if (!context || Object.keys(context).sort().join(',') !== 'dependencies,surface' || !Array.isArray(context.dependencies))
54
+ fail('$html', 'invalid presentation context');
55
+ const surface = validateArtefact(context.surface);
56
+ if (surface.contract !== 'n8r.surface/0.3')
57
+ fail('$html', 'requires Surface 0.3');
58
+ const dependencies = context.dependencies.map(validateArtefact);
59
+ const all = [record, surface, ...dependencies].filter((a, i, items) => {
60
+ const first = items.findIndex(x => x.id === a.id && x.revision.id === a.revision.id);
61
+ if (canonical(items[first]) !== canonical(a))
62
+ fail('$html', 'ambiguous presentation revision');
63
+ return first === i;
64
+ });
65
+ for (const item of all)
66
+ verifyReferences(item, all);
67
+ if (renderBundleHtml({ research: record, surface, dependencies }) !== html)
68
+ fail('$html', 'HTML/data conflict');
69
+ return record;
70
+ }
47
71
  if (renderResearchHtml(record) !== html)
48
72
  fail('$html', 'HTML/data conflict; preserve bytes and review the edit before regenerating');
49
73
  return record;
@@ -0,0 +1,9 @@
1
+ export type RecentWork = {
2
+ id: string;
3
+ title: string;
4
+ source: string;
5
+ path: string;
6
+ model: string;
7
+ };
8
+ export declare function recentWork(home: string): RecentWork[];
9
+ export declare function rememberWork(home: string, item: RecentWork): void;
package/dist/recent.js ADDED
@@ -0,0 +1,71 @@
1
+ /** Local navigation metadata only; never included in portable research artifacts. */
2
+ import fs from 'node:fs';
3
+ import { join, resolve, relative, sep } from 'node:path';
4
+ import { randomUUID } from 'node:crypto';
5
+ import { InspectionError, readBytes } from './settings.js';
6
+ function location(home) {
7
+ const directory = join(fs.realpathSync(home), '.n8r');
8
+ try {
9
+ const stat = fs.lstatSync(directory);
10
+ if (!stat.isDirectory() || stat.isSymbolicLink())
11
+ throw new InspectionError('Recent-work folder is not a regular directory');
12
+ }
13
+ catch (e) {
14
+ if (e.code !== 'ENOENT')
15
+ throw e;
16
+ }
17
+ return { directory, file: join(directory, 'recent.json') };
18
+ }
19
+ function valid(item) {
20
+ return item && ['id', 'title', 'source', 'path', 'model'].every(k => typeof item[k] === 'string' && item[k].length > 0 && item[k].length < 16000)
21
+ && resolve(item.source) === item.source && resolve(item.path) === item.path;
22
+ }
23
+ export function recentWork(home) {
24
+ const { file } = location(home);
25
+ let bytes;
26
+ try {
27
+ bytes = readBytes(file, 1024 * 1024);
28
+ }
29
+ catch (e) {
30
+ if (e.code === 'ENOENT')
31
+ return [];
32
+ throw e;
33
+ }
34
+ let value;
35
+ try {
36
+ value = JSON.parse(bytes.toString('utf8'));
37
+ }
38
+ catch {
39
+ throw new InspectionError('Recent-work list is damaged; saved research is unchanged');
40
+ }
41
+ if (!Array.isArray(value) || value.length > 100 || !value.every(valid))
42
+ throw new InspectionError('Invalid recent-work list; saved research is unchanged');
43
+ return value;
44
+ }
45
+ export function rememberWork(home, item) {
46
+ if (!valid(item))
47
+ throw new InspectionError('Invalid saved-work reference');
48
+ const { directory, file } = location(home);
49
+ const rel = relative(item.source, directory);
50
+ if (!rel || (!rel.startsWith(`..${sep}`) && rel !== '..' && !rel.startsWith(sep)))
51
+ throw new InspectionError('Recent-work list cannot be written inside the research source');
52
+ fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
53
+ const lock = file + '.lock';
54
+ const fd = fs.openSync(lock, 'wx', 0o600);
55
+ const temporary = file + '.' + randomUUID();
56
+ try {
57
+ const previous = recentWork(home);
58
+ const next = [item, ...previous.filter(p => p.id !== item.id)].slice(0, 100);
59
+ fs.writeFileSync(temporary, JSON.stringify(next, null, 2) + '\n', { flag: 'wx', mode: 0o600 });
60
+ // Check again while holding our lock; refuse user edits during this save.
61
+ if (JSON.stringify(recentWork(home)) !== JSON.stringify(previous))
62
+ throw new InspectionError('Recent-work list changed; saved research is unchanged');
63
+ fs.renameSync(temporary, file);
64
+ }
65
+ finally {
66
+ fs.closeSync(fd);
67
+ fs.unlinkSync(lock);
68
+ if (fs.existsSync(temporary))
69
+ fs.unlinkSync(temporary);
70
+ }
71
+ }
package/dist/revisions.js CHANGED
@@ -8,6 +8,48 @@ export function verifySuccessor(previous, next) {
8
8
  const parent = after.revision.parent;
9
9
  if (!parent || parent.id !== before.id || parent.revision !== before.revision.id || parent.digest !== digest(before) || after.revision.id === before.revision.id)
10
10
  fail('$revision', 'parent revision/digest mismatch or reused revision');
11
+ if (before.contract === 'n8r.surface/0.2' && after.contract === 'n8r.surface/0.2') {
12
+ if (before.kind !== after.kind)
13
+ fail('$revision', 'description kind changed');
14
+ for (const n of after.nodes) {
15
+ const old = before.nodes.find(x => x.id === n.id);
16
+ if (old && canonical(old.type) !== canonical(n.type))
17
+ fail('$revision', 'retained node type changed');
18
+ }
19
+ if (before.kind === 'capability' && after.kind === 'capability') {
20
+ for (const c of after.capabilities) {
21
+ const old = before.capabilities.find(x => x.id === c.id);
22
+ if (old && (old.from !== c.from || old.to !== c.to || canonical(old.type) !== canonical(c.type)))
23
+ fail('$revision', 'retained capability endpoints/type changed');
24
+ }
25
+ }
26
+ if (before.kind === 'bound' && after.kind === 'bound') {
27
+ if (before.instance.id !== after.instance.id || before.capability.id !== after.capability.id)
28
+ fail('$revision', 'instance/capability identity changed');
29
+ for (const b of after.bindings) {
30
+ const old = before.bindings.find(x => x.id === b.id);
31
+ if (old && old.capability !== b.capability)
32
+ fail('$revision', 'retained binding slot changed');
33
+ }
34
+ }
35
+ }
36
+ if (before.contract === 'n8r.surface/0.3' && after.contract === 'n8r.surface/0.3') {
37
+ if (before.kind !== after.kind)
38
+ fail('$revision', 'description kind changed');
39
+ for (const n of after.nodes) {
40
+ const old = before.nodes.find(x => x.id === n.id);
41
+ if (old && canonical(old.type) !== canonical(n.type))
42
+ fail('$revision', 'retained node type changed');
43
+ }
44
+ for (const edge of after.relationships) {
45
+ const old = before.relationships.find(x => x.id === edge.id);
46
+ if (old && (old.from !== edge.from || old.to !== edge.to || canonical(old.type) !== canonical(edge.type)))
47
+ fail('$revision', 'retained relationship endpoints/type changed');
48
+ }
49
+ if (before.kind === 'bound' && after.kind === 'bound' &&
50
+ (before.instance.id !== after.instance.id || before.capability.id !== after.capability.id))
51
+ fail('$revision', 'instance/capability identity changed');
52
+ }
11
53
  if (before.contract === 'n8r.surface/0.1' && after.contract === 'n8r.surface/0.1') {
12
54
  for (const edge of after.relationships) {
13
55
  const old = before.relationships.find(x => x.id === edge.id);
@@ -0,0 +1 @@
1
+ export * from './skill-artifacts.js';
@@ -0,0 +1,13 @@
1
+ import { resolve } from 'node:path';
2
+ import { pathToFileURL } from 'node:url';
3
+ import { main } from './skill-artifacts.js';
4
+ export * from './skill-artifacts.js';
5
+ if (process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1])).href) {
6
+ try {
7
+ main(process.argv.slice(2));
8
+ }
9
+ catch {
10
+ console.error('Surface operation failed. Check input contracts, privacy, references, predecessor and destination; no success claimed.');
11
+ process.exitCode = 1;
12
+ }
13
+ }
@@ -0,0 +1,15 @@
1
+ import type { Artefact, ResearchRecord, SurfaceGraph } from './contracts.js';
2
+ export interface Bundle {
3
+ research: ResearchRecord;
4
+ surface: SurfaceGraph;
5
+ dependencies: Artefact[];
6
+ }
7
+ /** Conservative defense in depth; host must omit sensitive values before staging. */
8
+ export declare function privacy(value: unknown): void;
9
+ export declare function checkBundle(input: unknown): Bundle;
10
+ export declare function readBundle(directory: string): Bundle;
11
+ export declare function verifyBundleSuccessor(parent: Bundle, b: Bundle): void;
12
+ export declare function saveBundle(input: unknown, destination: string, predecessor?: string): Bundle;
13
+ export declare function main(args: string[]): void;
14
+ /** Same canonical digest, with value-free diagnostics at the skill boundary. */
15
+ export declare function digest(value: unknown): string;
@@ -0,0 +1,184 @@
1
+ /** Standalone installed-skill persistence. No CLI, runner, network or source reads. */
2
+ import fs from 'node:fs';
3
+ import { resolve, dirname, join } from 'node:path';
4
+ import { createHash } from 'node:crypto';
5
+ import { decode, encode, decodeJsonValue } from './codec.js';
6
+ import { validateArtefact, verifyReferences } from './validate.js';
7
+ import { digest as artifactDigest, canonical } from './portable.js';
8
+ import { readResearchHtml, renderResearchHtml } from './html.js';
9
+ import { renderBrief, renderBundleHtml } from './skill-presentation.js';
10
+ import { verifySuccessor } from './revisions.js';
11
+ const members = ['research.html', 'surface.json', 'dependencies.json', 'brief.md'];
12
+ const hash = (s) => `sha256:${createHash('sha256').update(s).digest('hex')}`;
13
+ function fail(code) { throw new Error(`Surface operation refused: ${code}. No input values are included.`); }
14
+ function guarded(code, fn) { try {
15
+ return fn();
16
+ }
17
+ catch {
18
+ return fail(code);
19
+ } }
20
+ /** Conservative defense in depth; host must omit sensitive values before staging. */
21
+ export function privacy(value) {
22
+ const text = canonical(value);
23
+ if (/(?:https?|mongodb(?:\+srv)?|postgres(?:ql)?):\/\/|\b(?:\d{1,3}\.){3}\d{1,3}\b|\b(?:[a-f0-9]{1,4}:){2,}[a-f0-9:]+\b|(?:password|passwd|token|secret|username|api[_-]?key)\s*[=:]\s*[^\s"},]+|(?:\/Users\/|\/home\/|[A-Z]:\\\\)|[\w.+-]+@[\w.-]+\.[a-z]{2,}/i.test(text))
24
+ fail('privacy review required');
25
+ }
26
+ export function checkBundle(input) {
27
+ return guarded('bundle validation or privacy check', () => {
28
+ if (!input || typeof input !== 'object' || Object.keys(input).sort().join(',') !== 'dependencies,research,surface')
29
+ fail('bundle shape');
30
+ const b = input;
31
+ privacy(b);
32
+ const research = validateArtefact(b.research), surface = validateArtefact(b.surface);
33
+ if (research.contract !== 'n8r.research/0.1' || surface.contract !== 'n8r.surface/0.3' || !Array.isArray(b.dependencies) || b.dependencies.length > 100)
34
+ fail('contracts');
35
+ const context = [];
36
+ for (const a of [research, surface, ...b.dependencies.map(validateArtefact)]) {
37
+ if (Object.keys(a.extensions).length)
38
+ fail('opaque extensions unsupported by this host');
39
+ const old = context.find(x => x.id === a.id && x.revision.id === a.revision.id);
40
+ if (old && digest(old) !== digest(a))
41
+ fail('ambiguous revision');
42
+ if (!old)
43
+ context.push(a);
44
+ }
45
+ if (!surface.records.some(r => r.id === research.id && r.revision === research.revision.id && r.digest === digest(research)))
46
+ fail('unlinked research');
47
+ for (const a of context)
48
+ verifyReferences(a, context);
49
+ return { research, surface, dependencies: b.dependencies };
50
+ });
51
+ }
52
+ function directories(dir) {
53
+ for (let p = resolve(dir);; p = dirname(p)) {
54
+ if (fs.existsSync(p)) {
55
+ const s = fs.lstatSync(p);
56
+ if (s.isSymbolicLink() || !s.isDirectory())
57
+ fail('unsafe directory');
58
+ }
59
+ else {
60
+ try {
61
+ fs.lstatSync(p);
62
+ fail('unsafe directory');
63
+ }
64
+ catch (e) {
65
+ if (e.code !== 'ENOENT')
66
+ throw e;
67
+ }
68
+ }
69
+ if (p === dirname(p))
70
+ break;
71
+ }
72
+ }
73
+ function read(path) {
74
+ const fd = fs.openSync(path, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
75
+ try {
76
+ const stat = fs.fstatSync(fd);
77
+ if (!stat.isFile() || stat.size > 8 * 1024 * 1024)
78
+ fail('file size/type');
79
+ return new TextDecoder('utf-8', { fatal: true }).decode(fs.readFileSync(fd));
80
+ }
81
+ finally {
82
+ fs.closeSync(fd);
83
+ }
84
+ }
85
+ const plain = (s) => s.replace(/[\r\n]/g, ' ').replace(/[<>\[\]`|]/g, '');
86
+ function brief(b) {
87
+ const s = b.surface, name = (id) => plain(s.nodes.find(n => n.id === id)?.label ?? id);
88
+ return `# ${plain(s.title)}\n\n${s.kind === 'bound' ? 'Bound' : 'Capability'} Surface · ${plain(s.revision.id)}\n\n${plain(s.scope)}\n\n` +
89
+ s.relationships.map(e => {
90
+ const state = 'binding' in e ? e.binding.state : e.state;
91
+ const target = 'binding' in e ? e.binding.claims.map(c => name(c.target)).join(', ') : 'supported possibility';
92
+ const detail = (d) => `${d.state}: ${d.claims.map(c => plain(c.value)).join(' / ') || plain(d.note)}`;
93
+ return `- **${name(e.from)} → ${name(e.to)}** (role endpoints; ${state})\n Target: ${target || 'unknown'}. Purpose: ${detail(e.purpose)}. Integration: ${detail(e.integration)}.\n`;
94
+ }).join('') + `\n## Evidence and decisions\n\nSee research.html for exact sources, findings, competing claims and human decisions; surface.json retains property-level evidence and configuration references. Configured does not establish runtime use.\n\n## Gaps\n\n${s.gaps.map(g => '- ' + plain(g)).join('\n') || 'No additional gaps recorded; coverage remains bounded.'}\n\n## Recommended next steps\n\n${b.research.questions.map(q => '- ' + plain(q.text)).join('\n') || 'Review the evidence and confirm the next question before expanding coverage.'}\n`;
95
+ }
96
+ export function readBundle(directory) {
97
+ return guarded('saved-byte verification', () => {
98
+ directory = resolve(directory);
99
+ directories(directory);
100
+ const m = JSON.parse(read(join(directory, 'manifest.json')));
101
+ if (![1, 2].includes(m.version) || Object.keys(m.files ?? {}).sort().join(',') !== [...members].sort().join(','))
102
+ fail('manifest');
103
+ if (fs.readdirSync(directory).sort().join(',') !== [...members, 'manifest.json'].sort().join(','))
104
+ fail('unexpected members');
105
+ const data = Object.fromEntries(members.map(n => { const text = read(join(directory, n)); if (hash(text) !== m.files[n])
106
+ fail('changed bytes'); return [n, text]; }));
107
+ const b = checkBundle({ research: readResearchHtml(data['research.html']), surface: decode(data['surface.json'], 'json'), dependencies: decodeJsonValue(data['dependencies.json']) });
108
+ if (data['brief.md'] !== (m.version === 1 ? brief(b) : renderBrief(b)))
109
+ fail('brief conflict');
110
+ if (data['research.html'] !== (m.version === 1 ? renderResearchHtml(b.research) : renderBundleHtml(b)))
111
+ fail('presentation context conflict');
112
+ return b;
113
+ });
114
+ }
115
+ export function verifyBundleSuccessor(parent, b) {
116
+ for (const old of [parent.research, parent.surface, ...parent.dependencies]) {
117
+ const next = [b.research, b.surface, ...b.dependencies].find(a => a.id === old.id && a.revision.id === old.revision.id);
118
+ if (next && digest(next) !== digest(old))
119
+ fail('rewritten pinned revision');
120
+ }
121
+ verifySuccessor(parent.research, b.research);
122
+ verifySuccessor(parent.surface, b.surface);
123
+ for (const d of parent.research.decisions)
124
+ if (!b.research.decisions.some(n => canonical(n) === canonical(d)))
125
+ fail('lost human decision');
126
+ for (const n of parent.surface.nodes)
127
+ if (!b.surface.nodes.some(x => x.id === n.id))
128
+ fail('lost component identity');
129
+ }
130
+ export function saveBundle(input, destination, predecessor) {
131
+ return guarded('save validation, predecessor or destination check', () => {
132
+ const b = checkBundle(input);
133
+ destination = resolve(destination);
134
+ directories(dirname(destination));
135
+ const parent = predecessor ? readBundle(predecessor) : undefined;
136
+ const previousBytes = predecessor ? read(join(predecessor, 'manifest.json')) : undefined;
137
+ if (parent) {
138
+ verifyBundleSuccessor(parent, b);
139
+ }
140
+ else if (b.research.revision.parent || b.surface.revision.parent)
141
+ fail('predecessor required');
142
+ const data = { 'research.html': renderBundleHtml(b), 'surface.json': encode(b.surface, 'json'), 'dependencies.json': JSON.stringify(b.dependencies, null, 2) + '\n', 'brief.md': renderBrief(b) };
143
+ const manifest = JSON.stringify({ version: 2, files: Object.fromEntries(members.map(n => [n, hash(data[n])])), predecessor: previousBytes ? hash(previousBytes) : null }, null, 2) + '\n';
144
+ // Parent must exist; exclusive mkdir never replaces an occupied destination.
145
+ fs.mkdirSync(destination, { mode: 0o700 });
146
+ try {
147
+ for (const [name, bytes] of Object.entries({ ...data, 'manifest.json': manifest }))
148
+ fs.writeFileSync(join(destination, name), bytes, { flag: 'wx', mode: 0o600 });
149
+ const result = readBundle(destination);
150
+ if (predecessor) {
151
+ readBundle(predecessor);
152
+ if (read(join(predecessor, 'manifest.json')) !== previousBytes)
153
+ fail('predecessor changed');
154
+ }
155
+ return result;
156
+ }
157
+ catch (e) {
158
+ // Retain incomplete output for inspection; never call it a successful save.
159
+ throw e;
160
+ }
161
+ });
162
+ }
163
+ export function main(args) {
164
+ const [op, file, destination, previous] = args;
165
+ if (op === 'help' && args.length === 1) {
166
+ console.log('Standalone Surface helper: check BUNDLE.json | save BUNDLE.json NEW_DIRECTORY [PREDECESSOR_DIRECTORY] | read DIRECTORY. JSON bundle: {research,surface,dependencies}. Save only with user authorization. Node 22/24/26; no network or n8r-surface CLI.');
167
+ return;
168
+ }
169
+ if (op === 'read' && args.length === 2) {
170
+ const b = readBundle(file);
171
+ console.log(JSON.stringify({ status: 'checked', kind: b.surface.kind, research: b.research, surface: b.surface, dependencies: b.dependencies }, null, 2));
172
+ return;
173
+ }
174
+ if ((op === 'check' && args.length === 2) || (op === 'save' && (args.length === 3 || args.length === 4))) {
175
+ const b = checkBundle(decodeJsonValue(read(file)));
176
+ if (op === 'save')
177
+ saveBundle(b, destination, previous);
178
+ console.log(JSON.stringify({ status: op === 'save' ? 'saved-and-read-back' : 'checked', kind: b.surface.kind, researchDigest: digest(b.research), surfaceDigest: digest(b.surface) }));
179
+ return;
180
+ }
181
+ fail('arguments; run help');
182
+ }
183
+ /** Same canonical digest, with value-free diagnostics at the skill boundary. */
184
+ export function digest(value) { return guarded('digest input', () => artifactDigest(value)); }
@@ -0,0 +1,26 @@
1
+ import type { Bundle } from './skill-artifacts.js';
2
+ export interface MappingNode {
3
+ id: string;
4
+ kind: 'component' | 'service';
5
+ type?: string;
6
+ rationale: string;
7
+ }
8
+ export interface Mapping {
9
+ nodes: MappingNode[];
10
+ dependencies: string[];
11
+ }
12
+ type Bom = Record<string, any>;
13
+ export interface Exchange {
14
+ bundle: Bundle;
15
+ mapping: Mapping;
16
+ bom: Bom;
17
+ report: Record<string, any>;
18
+ predecessor: string | null;
19
+ }
20
+ export { validateBom } from './cyclonedx-validation.js';
21
+ export declare function projectBundle(input: unknown, mapping: unknown, previous?: Exchange): {
22
+ bom: Bom;
23
+ report: Record<string, any>;
24
+ };
25
+ export declare function readExchange(directory: string): Exchange;
26
+ export declare function saveExchange(bundleDirectory: string, mapping: unknown, destination: string, previousDirectory?: string): Exchange;