@n8r/surface 0.0.0-stage → 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 (181) hide show
  1. package/LICENSE +202 -0
  2. package/LICENSE-DOCS +395 -0
  3. package/LICENSING.md +36 -0
  4. package/README.md +207 -2
  5. package/dist/activity.d.ts +16 -0
  6. package/dist/activity.js +44 -0
  7. package/dist/cli.d.ts +2 -0
  8. package/dist/cli.js +412 -0
  9. package/dist/codec.d.ts +6 -0
  10. package/dist/codec.js +56 -0
  11. package/dist/codex.d.ts +72 -0
  12. package/dist/codex.js +526 -0
  13. package/dist/contracts.d.ts +235 -0
  14. package/dist/contracts.js +1 -0
  15. package/dist/cyclonedx-schemas/bom-1.7.schema.json +6700 -0
  16. package/dist/cyclonedx-schemas/cryptography-defs.schema.json +576 -0
  17. package/dist/cyclonedx-schemas/jsf-0.82.schema.json +240 -0
  18. package/dist/cyclonedx-schemas/spdx.schema.json +786 -0
  19. package/dist/cyclonedx-validation.d.ts +1 -0
  20. package/dist/cyclonedx-validation.js +45 -0
  21. package/dist/html.d.ts +5 -0
  22. package/dist/html.js +74 -0
  23. package/dist/index.d.ts +7 -0
  24. package/dist/index.js +5 -0
  25. package/dist/notes.d.ts +52 -0
  26. package/dist/notes.js +184 -0
  27. package/dist/portable.d.ts +10 -0
  28. package/dist/portable.js +71 -0
  29. package/dist/recent.d.ts +9 -0
  30. package/dist/recent.js +71 -0
  31. package/dist/revisions.d.ts +2 -0
  32. package/dist/revisions.js +60 -0
  33. package/dist/settings.d.ts +23 -0
  34. package/dist/settings.js +100 -0
  35. package/dist/setup.d.ts +9 -0
  36. package/dist/setup.js +161 -0
  37. package/dist/skill-artifacts-cli.d.ts +1 -0
  38. package/dist/skill-artifacts-cli.js +13 -0
  39. package/dist/skill-artifacts.d.ts +15 -0
  40. package/dist/skill-artifacts.js +184 -0
  41. package/dist/skill-cyclonedx.d.ts +26 -0
  42. package/dist/skill-cyclonedx.js +214 -0
  43. package/dist/skill-presentation.d.ts +10 -0
  44. package/dist/skill-presentation.js +108 -0
  45. package/dist/skill-surface-cli.d.ts +1 -0
  46. package/dist/skill-surface-cli.js +37 -0
  47. package/dist/skill-surface-store.d.ts +7 -0
  48. package/dist/skill-surface-store.js +126 -0
  49. package/dist/source-tools.d.ts +102 -0
  50. package/dist/source-tools.js +201 -0
  51. package/dist/startup.d.ts +7 -0
  52. package/dist/startup.js +74 -0
  53. package/dist/surface-bom.d.ts +73 -0
  54. package/dist/surface-bom.js +139 -0
  55. package/dist/surface-draft.d.ts +22 -0
  56. package/dist/surface-draft.js +112 -0
  57. package/dist/surface-instructions.d.ts +3 -0
  58. package/dist/surface-instructions.js +32 -0
  59. package/dist/surface-overview.d.ts +15 -0
  60. package/dist/surface-overview.js +86 -0
  61. package/dist/surface-store.d.ts +34 -0
  62. package/dist/surface-store.js +213 -0
  63. package/dist/surface-view.d.ts +3 -0
  64. package/dist/surface-view.js +50 -0
  65. package/dist/validate.d.ts +4 -0
  66. package/dist/validate.js +482 -0
  67. package/docs/n8r-surface/README.md +133 -0
  68. package/docs/n8r-surface/cli-bundles.md +72 -0
  69. package/docs/n8r-surface/codex-research-transcript.md +51 -0
  70. package/docs/n8r-surface/codex-research-verification.md +176 -0
  71. package/docs/n8r-surface/command-outcomes.md +711 -0
  72. package/docs/n8r-surface/contracts.md +220 -0
  73. package/docs/n8r-surface/critical-path.md +184 -0
  74. package/docs/n8r-surface/design.md +289 -0
  75. package/docs/n8r-surface/first-use-access.md +143 -0
  76. package/docs/n8r-surface/first-use-overview.md +262 -0
  77. package/docs/n8r-surface/getting-started.md +74 -0
  78. package/docs/n8r-surface/guided-journey-verification.md +137 -0
  79. package/docs/n8r-surface/host-contract.md +176 -0
  80. package/docs/n8r-surface/implementation-decisions.md +96 -0
  81. package/docs/n8r-surface/instance-first-research.md +209 -0
  82. package/docs/n8r-surface/npm-distribution.md +107 -0
  83. package/docs/n8r-surface/npm-release.md +315 -0
  84. package/docs/n8r-surface/product-direction.md +327 -0
  85. package/docs/n8r-surface/recreation-handoff.md +117 -0
  86. package/docs/n8r-surface/researcher-discovery.md +157 -0
  87. package/docs/n8r-surface/researcher-prepared-check.md +117 -0
  88. package/docs/n8r-surface/researcher-reconciliation-trial.md +149 -0
  89. package/docs/n8r-surface/researcher-repository-check.md +95 -0
  90. package/docs/n8r-surface/researcher-skeleton-check.md +84 -0
  91. package/docs/n8r-surface/researcher-skill-verification.md +725 -0
  92. package/docs/n8r-surface/researcher-skill.md +309 -0
  93. package/docs/n8r-surface/researcher-vault.md +134 -0
  94. package/docs/n8r-surface/save-reopen-transcript.md +211 -0
  95. package/docs/n8r-surface/save-reopen-verification.md +152 -0
  96. package/docs/n8r-surface/security-assurance-strategy.md +267 -0
  97. package/docs/n8r-surface/skill-bom-verification.md +142 -0
  98. package/docs/n8r-surface/skill-cyclonedx-verification.md +144 -0
  99. package/docs/n8r-surface/skill-cyclonedx.md +85 -0
  100. package/docs/n8r-surface/skill-manual.md +129 -0
  101. package/docs/n8r-surface/skill-surface-artifacts.md +234 -0
  102. package/docs/n8r-surface/skill-usefulness-verification.md +135 -0
  103. package/docs/n8r-surface/skill-workspace-automation.md +359 -0
  104. package/docs/n8r-surface/startup-human-check.md +172 -0
  105. package/docs/n8r-surface/startup-verification.md +161 -0
  106. package/docs/n8r-surface/startup.md +151 -0
  107. package/docs/n8r-surface/surface-description-contract.md +224 -0
  108. package/docs/n8r-surface/surface-descriptions.md +173 -0
  109. package/docs/n8r-surface/surface-relationships.md +231 -0
  110. package/docs/n8r-surface/surface-revisions-transcript.md +552 -0
  111. package/docs/n8r-surface/surface-revisions-verification.md +161 -0
  112. package/docs/n8r-surface/tester-candidate.md +158 -0
  113. package/docs/n8r-surface/vault-setup-verification.md +210 -0
  114. package/docs/n8r-surface/verification.md +117 -0
  115. package/docs/n8r-surface/versioning.md +172 -0
  116. package/docs/n8r-surface/workspace-human-check.md +101 -0
  117. package/docs/n8r-surface/workspace-installation.md +68 -0
  118. package/docs/n8r-surface/workspace-verification.md +350 -0
  119. package/docs/n8r-surface/workspace.md +83 -0
  120. package/examples/researcher/README.md +100 -0
  121. package/examples/researcher/composed.surface.json +430 -0
  122. package/examples/researcher/composed.surface.yaml +283 -0
  123. package/examples/researcher/cyclonedx/configured/README.md +7 -0
  124. package/examples/researcher/cyclonedx/configured/manifest.json +10 -0
  125. package/examples/researcher/cyclonedx/configured/mapping.json +323 -0
  126. package/examples/researcher/cyclonedx/configured/surface-records.json +1275 -0
  127. package/examples/researcher/cyclonedx/configured/surface.cdx.json +79 -0
  128. package/examples/researcher/descriptions/README.md +29 -0
  129. package/examples/researcher/descriptions/bound-r1.json +141 -0
  130. package/examples/researcher/descriptions/bound-r2.json +153 -0
  131. package/examples/researcher/descriptions/capability.json +167 -0
  132. package/examples/researcher/descriptions/research-r1.json +153 -0
  133. package/examples/researcher/descriptions/research-r2.json +157 -0
  134. package/examples/researcher/descriptions/sources.md +24 -0
  135. package/examples/researcher/harbor-r1.research.html +193 -0
  136. package/examples/researcher/harbor-r2.research.html +226 -0
  137. package/examples/researcher/lumen-manual-edit.pending.html +271 -0
  138. package/examples/researcher/lumen-r1.research.html +271 -0
  139. package/examples/researcher/lumen-r2.research.html +280 -0
  140. package/examples/researcher/relationships/README.md +34 -0
  141. package/examples/researcher/relationships/bound-r1.json +346 -0
  142. package/examples/researcher/relationships/bound-r2.json +358 -0
  143. package/examples/researcher/relationships/capability.json +325 -0
  144. package/examples/researcher/relationships/config/assistant.env.example +4 -0
  145. package/examples/researcher/relationships/research-r1.json +300 -0
  146. package/examples/researcher/relationships/research-r2.json +304 -0
  147. package/examples/researcher/relationships/sources.md +38 -0
  148. package/examples/researcher/sources/harbor-api.md +16 -0
  149. package/examples/researcher/sources/harbor-auth.md +8 -0
  150. package/examples/researcher/sources/lumen-guide.md +28 -0
  151. package/examples/researcher/sources/lumen-settings.md +9 -0
  152. package/examples/researcher/surface-bom/README.md +17 -0
  153. package/examples/researcher/surface-bom/initial.cdx.json +188 -0
  154. package/package.json +45 -4
  155. package/plugins/n8r-surface/.codex-plugin/plugin.json +31 -0
  156. package/plugins/n8r-surface/LICENSE +202 -0
  157. package/plugins/n8r-surface/LICENSE-DOCS +395 -0
  158. package/plugins/n8r-surface/LICENSING.md +39 -0
  159. package/plugins/n8r-surface/assets/icon.svg +5 -0
  160. package/plugins/n8r-surface/plugin.json +27 -0
  161. package/plugins/n8r-surface/skills/research/SKILL.md +163 -0
  162. package/plugins/n8r-surface/skills/research/agents/openai.yaml +4 -0
  163. package/plugins/n8r-surface/skills/research/references/artifacts.md +152 -0
  164. package/plugins/n8r-surface/skills/research/references/cyclonedx.md +207 -0
  165. package/plugins/n8r-surface/skills/research/references/discovery-map.md +178 -0
  166. package/plugins/n8r-surface/skills/research/references/examples/bound-r1.json +346 -0
  167. package/plugins/n8r-surface/skills/research/references/examples/capability.json +325 -0
  168. package/plugins/n8r-surface/skills/research/references/examples/research-r1.json +300 -0
  169. package/plugins/n8r-surface/skills/research/references/examples/surface.cdx.json +188 -0
  170. package/plugins/n8r-surface/skills/research/references/first-use-access.md +97 -0
  171. package/plugins/n8r-surface/skills/research/references/first-use-overview.md +101 -0
  172. package/plugins/n8r-surface/skills/research/references/instance-first.md +125 -0
  173. package/plugins/n8r-surface/skills/research/references/model-meanings.md +55 -0
  174. package/plugins/n8r-surface/skills/research/references/skeleton-surface.md +105 -0
  175. package/plugins/n8r-surface/skills/research/references/working-notes.md +121 -0
  176. package/plugins/n8r-surface/skills/research/references/workspace.md +89 -0
  177. package/plugins/n8r-surface/skills/research/scripts/THIRD-PARTY-NOTICES.txt +273 -0
  178. package/plugins/n8r-surface/skills/research/scripts/artifacts.mjs +8348 -0
  179. package/plugins/n8r-surface/skills/research/scripts/cyclonedx.mjs +28113 -0
  180. package/plugins/n8r-surface/skills/research/scripts/surface.mjs +27800 -0
  181. package/plugins/n8r-surface/skills/research/scripts/workspace.mjs +120 -0
@@ -0,0 +1,32 @@
1
+ /** Content fields mirror the existing experimental contracts; host metadata is omitted. */
2
+ export const legacySurfaceInstructions = `
3
+ STRUCTURED SURFACE CAPABILITY (host adapter 0.1):
4
+ When the user asks to create or update a Surface, call surface_draft with research and surface content objects. This stages a preview only. Never claim a save. Natural-language save requests explain /save surface. /save still saves Markdown. A later user turn invalidates the preview, so restage a revised pair after answering a correction. Retain stable component IDs from notes. If an old label is not namespaced, record the explicit old-to-new mapping in research.notes and show it to the human. Do not silently rename identities.
5
+
6
+ Use these exact content fields (no contract, id, revision or records; the host assigns them):
7
+ research: {title,scope,extensions,sources,findings,alternatives,decisions,questions,notes}.
8
+ surface: {title,scope,extensions,kind:"definition",vocabularies,nodes,relationships,requirements,coverage,gaps}.
9
+ All collections are arrays except extensions and entity attributes, which are objects. Empty arrays/objects are allowed. Nonempty text is required for text fields. IDs use a URI-style namespace such as urn:lumen:app. All timestamps use real UTC seconds, YYYY-MM-DDTHH:mm:ssZ, without fractions. Only safe integer JSON numbers are supported. Do not add undocumented fields.
10
+
11
+ Source: {id,location,version,digest,inspectedAt,coverage:[{path,status,note}]}. location is a portable relative source path from an ACTUAL source_read/search or saved host evidence, never an absolute path. digest must be the exact SHA-256 from that tool or saved host evidence. Never invent a digest. status is inspected|missing|unreadable|excluded|truncated|unsupported. Record ranges and limits. On revision retain historical source objects exactly and add a new source ID for new bytes or inspection; never relabel old evidence as fresh.
12
+ Finding: {id,subject,property,state,note,claims:[{value,evidence:[{source,locator}]}]}. subject is a component ID; evidence.source references sources[].id. state is known|unknown|omitted|redacted|absent|out-of-scope|conflicting. known/absent require one agreed sourced value; conflicting requires different sourced values; other states require empty claims. Every claim requires evidence. Keep human decisions separate from sourced claims.
13
+ Alternative: {id,title,findings:[finding IDs],requirements:[text descriptions]}.
14
+ Decision: {id,alternative,choice:"selected"|"rejected"|"deferred",reason,by,at}. One current decision per alternative. Use actual human choices, never infer selection from support. Preserve prior choices unless the human changes them. Record an explicit correction's reason and date; previous revisions retain prior choices.
15
+ Question: {id,text,findings:[finding IDs]}. Note: {by,at,text}. Keep human prose, unresolved questions and recommended next investigations.
16
+ Vocabulary: {id,version}; version is a token such as 0.1. Use an explicitly scoped provisional vocabulary, not a claim of universal compatibility.
17
+ Node: {id,label,type:{vocabulary,term},attributes:{property:{state,note,value?}},basis:[{items:[finding/decision/alternative/question IDs]}],requirements:[requirement IDs]}.
18
+ Relationship: same fields as a node EXCEPT no label, plus {from:node ID,to:node ID,claim:"declared"}. Keep endpoint identity for retained relationship IDs. Only selected composition belongs in relationships. A supported option is not automatically selected. Shared components, cycles and parallel edges are allowed.
19
+ Fact state is known|unknown|omitted|redacted|absent|out-of-scope. Only known facts have a value; all other states omit value. Put conflicting values in research and unknown in the Surface with an explanatory note.
20
+ Requirement: {id,kind:"secret"|"resource"|"configuration",description,state}. Never include credential values. coverage and gaps are arrays of nonempty strings. Surface basis contains only items; the host fills the exact research record/revision and digest.
21
+
22
+ On pair reopening keep research and Surface identities via host bookkeeping. Retain all research sources, findings, alternatives, decisions, questions and component IDs. Keep historical claims and research notes exactly; append corrections and new evidence instead of removing history. Every node and relationship needs nonempty basis linking structured findings/decisions. Retained finding subjects/properties and node types cannot change; retain opaque extensions exactly. Removed selections remain rejected/deferred in research. Preserve prior unknowns unless evidence resolves them. Explain source unavailable/changed status and distinguish declared from reachable or exercised. On validation error, repair the staged content using feedback; do not discard evidence to make validation pass.
23
+ `;
24
+ export const surfaceInstructions = `
25
+ STRUCTURED SURFACE HOST ADAPTER 0.3:
26
+ For new research, use the bundled Capability/Bound contract and surface_draft with a COMPLETE bundle {research,surface,dependencies}. This tool validates and stages only; the user enters /save surface to save the preview as brief.md, research.html, surface.json, dependencies.json and manifest.json. /save still saves Markdown. A later conversational turn invalidates the preview.
27
+ Use surface_digest with {artifact: COMPLETE_ARTIFACT} to get canonical SHA-256 for exact revision references. Never guess artifact or source digests. Source digests come from source_read/search. Use real UTC timestamps in seconds. All artifact envelopes include contract, id, revision, title, scope, extensions. Revision includes id, parent (null initially), createdAt, author and reason. The current UTC time is supplied by the host.
28
+ Use the shapes below as schema examples ONLY: they are synthetic and are NOT evidence. Replace identities and findings with actual source evidence. Unknown details need explicit notes and empty claims. The host has no shell or filesystem staging tool. Use only surface_digest and surface_draft for structured work; do not attempt to invoke the standalone helper.
29
+ Compute research digest first, then set surface.records to the exact research reference. Each basis is {record:research.id,revision:research.revision.id,items:[finding IDs]}. Bound dependencies include its pinned Capability and that Capability's exact research. For a successor retain research/surface identity and kind, use new revision IDs, exact parent references, unchanged prior decisions, stable component and relationship identities, and exact historical dependencies. Retain parent research in dependencies to explain changes. Do not discard contradictory evidence.
30
+ On reopening a LEGACY n8r.surface/0.1 pair, continue that original format with the legacy content-only tool input described below; never silently migrate old files. For new work, always use n8r.surface/0.3. A saved Capability and a Bound description are distinct identities, not kind-changing successors.
31
+ LEGACY INSTRUCTIONS (only for reopened definitions):
32
+ ` + legacySurfaceInstructions;
@@ -0,0 +1,15 @@
1
+ /** Deterministic, offline overview. Layout hints never establish deployed boundaries. */
2
+ import type { Surface } from './surface-bom.js';
3
+ export declare const escapeHtml: (s: unknown) => string;
4
+ export declare const fragmentId: (kind: string, id: string) => string;
5
+ export declare const boundaryNames: {
6
+ internal: string;
7
+ external: string;
8
+ unknown: string;
9
+ };
10
+ export declare function nodeGroups(s: Surface): {
11
+ boundary: keyof typeof boundaryNames;
12
+ area: string;
13
+ nodes: any[];
14
+ }[];
15
+ export declare function renderOverview(s: Surface): string;
@@ -0,0 +1,86 @@
1
+ export const escapeHtml = (s) => String(s).replace(/[&<>"']/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]));
2
+ // No percent escapes in IDs: browsers decode fragment URLs before lookup.
3
+ // Escape underscores too so literal and encoded identifiers cannot collide.
4
+ export const fragmentId = (kind, id) => kind + '-' + Array.from(id).map(c => /[a-zA-Z0-9-]/.test(c) ? c : '_' + c.codePointAt(0).toString(16) + '_').join('');
5
+ export const boundaryNames = { internal: 'Inside the application', external: 'External integrations', unknown: 'Unclassified' };
6
+ export function nodeGroups(s) {
7
+ const groups = new Map();
8
+ for (const n of [...(s.bom.components ?? []), ...(s.bom.services ?? [])]) {
9
+ const p = s.presentation[n['bom-ref']], boundary = p?.boundary ?? 'unknown', area = p?.area ?? 'Placement not researched';
10
+ const key = JSON.stringify([boundary, area]);
11
+ if (!groups.has(key))
12
+ groups.set(key, { boundary, area, nodes: [] });
13
+ groups.get(key).nodes.push(n);
14
+ }
15
+ return [...groups.values()].sort((a, b) => Object.keys(boundaryNames).indexOf(a.boundary) - Object.keys(boundaryNames).indexOf(b.boundary));
16
+ }
17
+ function wrap(text) {
18
+ // Wrap long tokens too; preserve the full label in the accessible title.
19
+ const lines = [];
20
+ let line = '';
21
+ for (const word of text.split(/\s+/)) {
22
+ if (line && (line + ' ' + word).length > 30) {
23
+ lines.push(line);
24
+ line = '';
25
+ }
26
+ let rest = word;
27
+ while (rest.length > 30) {
28
+ lines.push(rest.slice(0, 30));
29
+ rest = rest.slice(30);
30
+ }
31
+ line += (line ? ' ' : '') + rest;
32
+ }
33
+ if (line)
34
+ lines.push(line);
35
+ return lines;
36
+ }
37
+ export function renderOverview(s) {
38
+ const h = escapeHtml, groups = nodeGroups(s);
39
+ if (!groups.length)
40
+ return '<p>No components recorded yet. Continue the source investigation.</p>';
41
+ const positions = new Map();
42
+ const columns = Object.keys(boundaryNames);
43
+ const heights = [65, 65, 65];
44
+ let labels = '';
45
+ const counts = new Map();
46
+ for (const e of s.relationships) {
47
+ const key = JSON.stringify([e.from, e.to].sort());
48
+ counts.set(key, (counts.get(key) ?? 0) + 1);
49
+ }
50
+ const columnWidth = 370 + Math.max(0, ...counts.values()) * 12, canvasWidth = columnWidth * 3 + 10;
51
+ const lanes = new Map();
52
+ for (const g of groups) {
53
+ const col = columns.indexOf(g.boundary), x = 25 + col * columnWidth;
54
+ let y = heights[col];
55
+ const areaLines = wrap(g.area);
56
+ labels += `<text x="${x}" y="${y}" class="area-label">${areaLines.map((line, i) => `<tspan x="${x}" dy="${i ? 18 : 0}">${h(line)}</tspan>`).join('')}</text>`;
57
+ y += areaLines.length * 18 + 12;
58
+ for (const n of g.nodes) {
59
+ const lines = wrap(n.name), height = Math.max(54, lines.length * 18 + 24);
60
+ positions.set(n['bom-ref'], { x, y, height, n, lines });
61
+ y += height + 24;
62
+ }
63
+ heights[col] = y + 22;
64
+ }
65
+ const edges = s.relationships.map((e, i) => {
66
+ const a = positions.get(e.from), b = positions.get(e.to);
67
+ const ay = a.y + a.height / 2, by = b.y + b.height / 2;
68
+ const key = JSON.stringify([e.from, e.to].sort()), rank = lanes.get(key) ?? 0;
69
+ lanes.set(key, rank + 1);
70
+ let d;
71
+ if (e.from === e.to)
72
+ d = `M${a.x + 280} ${ay - 9} C${a.x + 330 + rank * 12} ${ay - 28} ${a.x + 330 + rank * 12} ${ay + 28} ${a.x + 280} ${ay + 9}`;
73
+ else if (a.x === b.x) {
74
+ const lane = a.x + 300 + rank * 12;
75
+ d = `M${a.x + 280} ${ay} C${lane} ${ay} ${lane} ${by} ${b.x + 280} ${by}`;
76
+ }
77
+ else {
78
+ const right = b.x > a.x, ax = a.x + (right ? 280 : 0), bx = b.x + (right ? 0 : 280), bend = (ax + bx) / 2;
79
+ const offset = (rank + 1) * 12;
80
+ d = `M${ax} ${ay} C${bend + offset} ${ay + offset} ${bend + offset} ${by + offset} ${bx} ${by}`;
81
+ }
82
+ return `<a href="#edge-${i}"><path data-edge="${h(e.id)}" d="${d}" class="${e.selection === 'selected' ? 'selected' : 'possible'}" marker-end="url(#arrow)"><title>${h(a.n.name + ' → ' + b.n.name + ': ' + e.purpose + '; support ' + e.support + '; selection ' + e.selection)}</title></path></a>`;
83
+ }).join('');
84
+ const nodes = [...positions].map(([id, p]) => `<a href="#${fragmentId('node', id)}"><g data-node="${h(id)}"><title>${h(p.n.name)}</title><rect x="${p.x}" y="${p.y}" width="280" height="${p.height}" rx="6"/><text x="${p.x + 12}" y="${p.y + 24}">${p.lines.map((line, i) => `<tspan x="${p.x + 12}" dy="${i ? 18 : 0}">${h(line)}</tspan>`).join('')}</text></g></a>`).join('');
85
+ return `<div class="map"><svg role="img" aria-label="Connected application overview; full relationships follow" viewBox="0 0 ${canvasWidth} ${Math.max(...heights) + 20}"><defs><marker id="arrow" markerWidth="8" markerHeight="8" refX="7" refY="3" orient="auto"><path d="M0,0 L0,6 L8,3 z" fill="#64746d"/></marker></defs>${columns.map((c, i) => `<text x="${25 + i * columnWidth}" y="30" class="boundary-label">${h(boundaryNames[c])}</text>`).join('')}${labels}${edges}${nodes}</svg></div>`;
86
+ }
@@ -0,0 +1,34 @@
1
+ import { type SurfaceDraft } from './surface-draft.js';
2
+ import { type Note, type NoteContext } from './notes.js';
3
+ type Manifest = {
4
+ version: 1 | 2;
5
+ files: Record<string, string>;
6
+ provenance?: NoteContext;
7
+ note?: {
8
+ file: string;
9
+ digest: string;
10
+ };
11
+ predecessor?: {
12
+ directory: string;
13
+ manifestDigest: string;
14
+ } | string | null;
15
+ };
16
+ export type SavedSurfacePair = SurfaceDraft & {
17
+ directory: string;
18
+ researchPath: string;
19
+ surfacePath: string;
20
+ manifest: Manifest;
21
+ briefPath?: string;
22
+ bytes: Record<string, Buffer>;
23
+ };
24
+ export declare function readSurfacePair(path: string): SavedSurfacePair;
25
+ export declare class SurfaceStore {
26
+ private note?;
27
+ readonly directory: string;
28
+ previous: SavedSurfacePair | undefined;
29
+ private stamps;
30
+ constructor(vault: string, source: string, previous?: SavedSurfacePair, note?: Note | undefined);
31
+ private unchanged;
32
+ save(draft: SurfaceDraft, provenance: NoteContext): SavedSurfacePair;
33
+ }
34
+ export {};
@@ -0,0 +1,213 @@
1
+ import fs from 'node:fs';
2
+ import { readBundle, saveBundle } from './skill-artifacts.js';
3
+ import { basename, dirname, join, resolve, relative, sep } from 'node:path';
4
+ import { createHash, randomUUID } from 'node:crypto';
5
+ import { decode, encode } from './codec.js';
6
+ import { readResearchHtml, renderResearchHtml } from './html.js';
7
+ import { verifyReferences } from './validate.js';
8
+ import { canonical } from './portable.js';
9
+ import { verifyPairSuccessor, isBundle } from './surface-draft.js';
10
+ import {} from './notes.js';
11
+ import { InspectionError, readBytes } from './settings.js';
12
+ const LIMIT = 8 * 1024 * 1024;
13
+ const HASH = /^sha256:[a-f0-9]{64}$/;
14
+ const MEMBERS = ['research.html', 'surface.json', 'manifest.json'];
15
+ const hash = (bytes) => `sha256:${createHash('sha256').update(bytes).digest('hex')}`;
16
+ function fail(message) { throw new InspectionError(message); }
17
+ function safeName(value) { return typeof value === 'string' && !!value && value !== '.' && value !== '..' && !/[\\/\x00-\x1f]/.test(value); }
18
+ function checkProvenance(p) {
19
+ if (!p || typeof p.model !== 'string' || typeof p.researcher?.version !== 'string' || !HASH.test(p.researcher?.digest)
20
+ || !Array.isArray(p.evidence) || p.evidence.length > 1000 || p.evidence.some((e) => !e || typeof e.path !== 'string' || !HASH.test(e.digest)
21
+ || typeof e.inspectedAt !== 'string' || !Number.isSafeInteger(e.startLine) || !Number.isSafeInteger(e.endLine)))
22
+ fail('Invalid pair provenance');
23
+ }
24
+ function manifest(value) {
25
+ canonical(value);
26
+ if (!value || value.version !== 1 || !value.files || Object.keys(value.files).sort().join(',') !== 'research.html,surface.json'
27
+ || !HASH.test(value.files['research.html']) || !HASH.test(value.files['surface.json'])
28
+ || Object.keys(value).some(k => !['version', 'files', 'provenance', 'note', 'predecessor'].includes(k))
29
+ || (value.note && (!safeName(value.note.file) || !HASH.test(value.note.digest)))
30
+ || (value.predecessor && (!safeName(value.predecessor.directory) || !HASH.test(value.predecessor.manifestDigest))))
31
+ fail('Invalid Surface pair manifest');
32
+ checkProvenance(value.provenance);
33
+ }
34
+ /** No symlink ancestry; stable directory topology is required during synchronous operations. */
35
+ function checkDirectories(directory, stamps) {
36
+ const paths = [];
37
+ for (let p = directory;; p = dirname(p)) {
38
+ paths.unshift(p);
39
+ if (dirname(p) === p)
40
+ break;
41
+ }
42
+ for (const p of paths) {
43
+ let stat;
44
+ try {
45
+ stat = fs.lstatSync(p);
46
+ }
47
+ catch (e) {
48
+ if (e.code === 'ENOENT' && !stamps?.has(p))
49
+ continue;
50
+ throw e;
51
+ }
52
+ if (!stat.isDirectory() || stat.isSymbolicLink())
53
+ fail('Surface directory is occupied or a symbolic link');
54
+ const stamp = `${stat.dev}:${stat.ino}`, old = stamps?.get(p);
55
+ if (old && old !== stamp)
56
+ fail('Surface directory changed; reopen before saving');
57
+ stamps?.set(p, stamp);
58
+ }
59
+ }
60
+ function read(path, limit = LIMIT) {
61
+ if (fs.lstatSync(path).isSymbolicLink())
62
+ fail('Pair member cannot be a symbolic link');
63
+ return readBytes(path, limit);
64
+ }
65
+ function utf8(bytes) { try {
66
+ return new TextDecoder('utf-8', { fatal: true }).decode(bytes);
67
+ }
68
+ catch {
69
+ fail('Pair is not UTF-8');
70
+ } }
71
+ export function readSurfacePair(path) {
72
+ path = resolve(path);
73
+ if (!['research.html', 'surface.json'].includes(basename(path)))
74
+ fail('Select research.html or surface.json from a saved pair');
75
+ const directory = dirname(path);
76
+ checkDirectories(directory);
77
+ const bytes = {};
78
+ bytes['manifest.json'] = read(join(directory, 'manifest.json'), 512 * 1024);
79
+ let m;
80
+ try {
81
+ m = JSON.parse(utf8(bytes['manifest.json']));
82
+ }
83
+ catch {
84
+ fail('Invalid pair manifest JSON');
85
+ }
86
+ if (m?.files?.['dependencies.json']) {
87
+ const bundle = readBundle(directory);
88
+ for (const name of Object.keys(m.files))
89
+ bytes[name] = read(join(directory, name));
90
+ // Standalone skill manifests have no runner receipt. Re-read recorded sources
91
+ // on reopening; they remain historical evidence, not a fresh tool result.
92
+ if (m.provenance)
93
+ checkProvenance(m.provenance);
94
+ return { ...bundle, directory, researchPath: join(directory, 'research.html'), surfacePath: join(directory, 'surface.json'), briefPath: join(directory, 'brief.md'), manifest: m, bytes };
95
+ }
96
+ manifest(m);
97
+ for (const name of ['research.html', 'surface.json']) {
98
+ bytes[name] = read(join(directory, name));
99
+ if (hash(bytes[name]) !== m.files[name])
100
+ fail('Pair bytes conflict with manifest hash; preserve edits and reopen an accepted revision');
101
+ }
102
+ const research = readResearchHtml(utf8(bytes['research.html'])), surface = decode(utf8(bytes['surface.json']), 'json');
103
+ if (surface.contract !== 'n8r.surface/0.1')
104
+ fail('Expected Surface definition');
105
+ if (surface.records.length !== 1 || surface.records[0].id !== research.id)
106
+ fail('This host reopens one linked research record per pair');
107
+ verifyReferences(surface, [research]);
108
+ return { research, surface, directory, researchPath: join(directory, 'research.html'), surfacePath: join(directory, 'surface.json'), manifest: m, bytes };
109
+ }
110
+ export class SurfaceStore {
111
+ note;
112
+ directory;
113
+ previous;
114
+ stamps = new Map();
115
+ constructor(vault, source, previous, note) {
116
+ this.note = note;
117
+ source = fs.realpathSync(source);
118
+ const slug = basename(source).toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '').slice(0, 80) || 'source';
119
+ this.directory = previous ? dirname(previous.directory) : join(fs.realpathSync(vault), 'surfaces', slug);
120
+ const rel = relative(source, this.directory);
121
+ if (fs.statSync(source).isDirectory() && (!rel || (rel !== '..' && !rel.startsWith(`..${sep}`) && !rel.startsWith(sep))))
122
+ fail('Surface cannot be saved inside selected source');
123
+ this.previous = previous;
124
+ checkDirectories(this.directory, this.stamps);
125
+ }
126
+ unchanged() {
127
+ checkDirectories(this.directory, this.stamps);
128
+ if (this.note && !read(this.note.path, 256 * 1024).equals(Buffer.from(this.note.text)))
129
+ fail('Original note changed; reopen before saving');
130
+ if (this.previous)
131
+ for (const name of Object.keys(this.previous.bytes))
132
+ if (!read(join(this.previous.directory, name)).equals(this.previous.bytes[name]))
133
+ fail('Predecessor changed; reopen before saving');
134
+ }
135
+ save(draft, provenance) {
136
+ this.unchanged();
137
+ checkProvenance(provenance);
138
+ if (isBundle(draft)) {
139
+ if (this.previous)
140
+ verifyPairSuccessor(this.previous, draft);
141
+ fs.mkdirSync(this.directory, { recursive: true, mode: 0o700 });
142
+ checkDirectories(this.directory, this.stamps);
143
+ const names = fs.readdirSync(this.directory);
144
+ if (names.length > 10000)
145
+ fail('Too many Surface revisions');
146
+ const max = names.reduce((n, name) => Math.max(n, Number(/^surface-(\d+)$/.exec(name)?.[1] ?? 0)), 0);
147
+ if (!Number.isSafeInteger(max + 1))
148
+ fail('Surface numbering limit reached');
149
+ const target = join(this.directory, `surface-${String(max + 1).padStart(3, '0')}`);
150
+ saveBundle(draft, target, this.previous?.directory);
151
+ const manifestPath = join(target, 'manifest.json');
152
+ const receipt = JSON.parse(utf8(read(manifestPath)));
153
+ fs.writeFileSync(manifestPath, JSON.stringify({ ...receipt, provenance, ...(this.note ? { note: { file: basename(this.note.path), digest: this.note.digest } } : this.previous?.manifest.note ? { note: this.previous.manifest.note } : {}) }, null, 2) + '\n', { mode: 0o600 });
154
+ const saved = readSurfacePair(join(target, 'surface.json'));
155
+ this.unchanged();
156
+ this.previous = saved;
157
+ return saved;
158
+ }
159
+ verifyReferences(draft.surface, [draft.research]);
160
+ if (this.previous)
161
+ verifyPairSuccessor(this.previous, draft);
162
+ else if (draft.research.revision.parent || draft.surface.revision.parent)
163
+ fail('Initial pair cannot have an unavailable predecessor');
164
+ const html = renderResearchHtml(draft.research), json = encode(draft.surface, 'json');
165
+ if (Buffer.byteLength(html) > LIMIT)
166
+ fail('Research HTML exceeds host size limit');
167
+ const m = { version: 1, files: { 'research.html': hash(html), 'surface.json': hash(json) }, provenance,
168
+ ...(this.note ? { note: { file: basename(this.note.path), digest: this.note.digest } } : this.previous?.manifest.note ? { note: this.previous.manifest.note } : {}),
169
+ ...(this.previous ? { predecessor: { directory: basename(this.previous.directory), manifestDigest: hash(this.previous.bytes['manifest.json']) } } : {}) };
170
+ manifest(m);
171
+ const data = { 'research.html': html, 'surface.json': json, 'manifest.json': JSON.stringify(m, null, 2) + '\n' };
172
+ if (Buffer.byteLength(data['manifest.json']) > 512 * 1024)
173
+ fail('Pair manifest exceeds size limit');
174
+ fs.mkdirSync(this.directory, { recursive: true, mode: 0o700 });
175
+ checkDirectories(this.directory, this.stamps);
176
+ const names = fs.readdirSync(this.directory);
177
+ if (names.length > 10000)
178
+ fail('Too many Surface revisions');
179
+ const max = names.reduce((n, name) => Math.max(n, Number(/^surface-(\d+)$/.exec(name)?.[1] ?? 0)), 0);
180
+ if (!Number.isSafeInteger(max + 1))
181
+ fail('Surface numbering limit reached');
182
+ const target = join(this.directory, `surface-${String(max + 1).padStart(3, '0')}`);
183
+ const temporary = fs.mkdtempSync(join(this.directory, '.surface-'));
184
+ try {
185
+ for (const name of MEMBERS) {
186
+ const fd = fs.openSync(join(temporary, name), 'wx', 0o600);
187
+ try {
188
+ fs.writeFileSync(fd, data[name]);
189
+ fs.fsyncSync(fd);
190
+ }
191
+ finally {
192
+ fs.closeSync(fd);
193
+ }
194
+ }
195
+ readSurfacePair(join(temporary, 'surface.json'));
196
+ this.unchanged();
197
+ // Under the stable-topology assumption, never replace any occupied destination.
198
+ if (fs.existsSync(target))
199
+ fail('Surface destination occupied');
200
+ fs.renameSync(temporary, target);
201
+ const saved = readSurfacePair(join(target, 'surface.json'));
202
+ for (const name of MEMBERS)
203
+ if (!saved.bytes[name].equals(Buffer.from(data[name])))
204
+ fail('Saved pair readback differs; inspect output before retrying');
205
+ this.previous = saved;
206
+ return saved;
207
+ }
208
+ finally {
209
+ if (fs.existsSync(temporary))
210
+ fs.rmSync(temporary, { recursive: true });
211
+ }
212
+ }
213
+ }
@@ -0,0 +1,3 @@
1
+ /** Views are generated from the BOM and never become a second authority. */
2
+ import type { Surface } from './surface-bom.js';
3
+ export declare function renderSurface(s: Surface): Record<'brief.md' | 'investigation.md' | 'surface.html', string>;
@@ -0,0 +1,50 @@
1
+ import { escapeHtml as html, nodeGroups, boundaryNames, renderOverview, fragmentId } from './surface-overview.js';
2
+ const md = (s) => String(s).replace(/[\r\n]/g, ' ').replace(/[\\`*_{}\[\]<>|]/g, c => '\\' + c);
3
+ export function renderSurface(s) {
4
+ const nodes = [...(s.bom.components ?? []), ...(s.bom.services ?? [])];
5
+ const name = (id) => nodes.find(n => n['bom-ref'] === id)?.name ?? id;
6
+ const focus = nodes.find(n => s.presentation[n['bom-ref']]?.focus);
7
+ const title = focus ? `${focus.name} — initial security overview` : 'Surface overview';
8
+ const intro = focus?.description ?? s.objective;
9
+ const groups = nodeGroups(s);
10
+ const replaced = new Set(s.entries.flatMap(e => e.supersedes ? [e.supersedes] : []));
11
+ const current = s.entries.filter(e => !replaced.has(e.id) && e.status !== 'superseded');
12
+ const findings = current.filter(e => e.kind === 'finding' && e.status === 'done' && e.evidence.length > 0);
13
+ const next = current.filter(e => ['next', 'question'].includes(e.kind) && ['open', 'active'].includes(e.status)).sort((a, b) => Number(b.status === 'active') - Number(a.status === 'active'));
14
+ const briefFindings = findings.slice(-5).reverse();
15
+ const label = (e) => `${e.selection === 'selected' ? 'Selected' : e.support === 'supported' ? 'Possible' : 'Support ' + e.support} · ${e.kind}${e.observations.length ? ' · observed ' + e.observations.map(o => o.at).join(', ') : ''}`;
16
+ const mechanics = `BOM revision ${s.bom.version}. Generated view; propose edits for reconciliation into a successor. The BOM remains authoritative.`;
17
+ const scopeNote = 'Supported paths describe options. Selection can reflect human intent or configuration; inspect its evidence. Dated observations stand separately. Missing information does not establish absence.';
18
+ const continuation = 'Continue with the available evidence: trace a source-level path, describe an intended setup, or inspect an identified deployment. Source-only research needs no running services. Choose another direction or stop at any time.';
19
+ const interchange = 'This architecture and research record is not a comprehensive dependency SBOM. Standard dependencies contain only selected dependency relationships; profile properties retain other paths and evidence. Generic consumers may omit that detail.';
20
+ const evidence = (ids) => ids.map(id => `<a href="#${fragmentId('ev', id)}">${html(id)}</a>`).join(', ') || 'No evidence yet';
21
+ const evidenceMd = (ids) => ids.map(id => `[${md(id)}](surface.html#${fragmentId('ev', id)})`).join(', ');
22
+ const basis = (id) => { const b = s.evidence.find(e => e.id === id)?.basis; return b === 'user' ? 'user intent' : b ?? 'unknown'; };
23
+ const placementMd = (id) => { const p = s.presentation[id]; return p ? ' Placement basis: ' + (p.evidence.map(e => `${md(basis(e))} — ${evidenceMd([e])}`).join('; ') || 'Not recorded') : ''; };
24
+ const list = (xs, empty) => xs.length ? xs.map(x => '- ' + md(x)).join('\n') : '- ' + empty;
25
+ const more = (count, section) => count > 0 ? `\n\n${count} additional items in [the full ${section}](surface.html#${section}).` : '';
26
+ const briefList = (xs, empty) => list(xs.slice(0, 7), empty) + more(xs.length - 7, 'coverage');
27
+ let remaining = 12;
28
+ const grouped = groups.flatMap(g => {
29
+ const shown = g.nodes.slice(0, remaining);
30
+ remaining -= shown.length;
31
+ return shown.length ? [`### ${boundaryNames[g.boundary]} · ${md(g.area)}\n\n` + shown.map(n => `- **${md(n.name)}** · ${md(n.type ?? 'service')} · ${md(n['bom-ref'])}${placementMd(n['bom-ref'])}${s.relationships.some(e => e.from === n['bom-ref'] || e.to === n['bom-ref']) ? '' : ' · Connections unknown.'}`).join('\n')] : [];
32
+ }).join('\n\n') + more(nodes.length - 12, 'inventory');
33
+ const brief = `# ${md(title)}\n\n${md(intro)}\n\nScope: ${md(s.scope)}\n\n## Major areas and integrations\n\n${grouped || 'No components recorded yet.'}\n\n${scopeNote}\n\nOpen the [connected overview and relationship details](surface.html#overview). Grouping records placement, not verified deployed trust boundaries. Inspect its evidence basis: source, user intent, configuration or dated observation.\n\n## Security observations and investigation leads\n\n${findings.length ? briefFindings.map(e => `- **${md(e.title)}** — ${md(e.text)} Why it matters: ${md(e.reason)} Evidence: ${evidenceMd(e.evidence)}.`).join('\n') : 'No evidence-backed findings recorded. Continue research; this is not an assurance conclusion.'}${more(findings.length - 5, 'findings')}\n\n${findings.length > 5 ? 'Showing the five most recently recorded current findings, not a severity ranking.\n\n' : ''}## Coverage and gaps\n\nResearched:\n\n${briefList(s.coverage, 'No coverage recorded.')}\n\nUnknown or not researched:\n\n${briefList(s.gaps, 'No gaps recorded; completeness is not established.')}\n\n## Next step\n\n${next.slice(0, 1).map(e => `- **${md(e.title)}** — ${md(e.text)} Why: ${md(e.reason)} Access: ${md(e.access)}`).join('\n') || 'No current action recorded.'}${more(next.length - 1, 'next')}\n\n${continuation}\n\n## Evidence and saved detail\n\nOpen [the map and evidence](surface.html), [investigation journal](investigation.md), or [saved Surface](surface.cdx.json).\n\n${interchange}\n\n${mechanics}\n`;
34
+ const journal = `# Investigation journal\n\n${mechanics}\n\n## Objective and scope\n\n${md(s.objective)}\n\n${md(s.scope)}\n\n## Available evidence and coverage\n\n${list(s.coverage, 'No coverage recorded.')}\n\n## Journey\n\n` + s.entries.map(e => `### ${md(e.title)}\n\n${md(e.kind)} · ${md(e.status)} · ${md(e.at)} · ID: ${md(e.id)}\n\n${md(e.text)}\n\nWhy: ${md(e.reason)}\n\nAccess needed: ${md(e.access)}\n\nEvidence: ${evidenceMd(e.evidence) || 'None yet'}${e.parent ? '\n\nBranch of: ' + md(e.parent) : ''}${e.supersedes ? '\n\nSupersedes: ' + md(e.supersedes) : ''}\n`).join('\n') + `\n## Open gaps\n\n${list(s.gaps, 'No gaps recorded; completeness is not established.')}\n\nChoose a next step, inspect the whole map, redirect or stop.\n`;
35
+ const details = s.relationships.map((e, i) => `<details id="edge-${i}"><summary>${html(name(e.from))} → ${html(name(e.to))} <small>${html(label(e))}</small></summary><p>${html(e.purpose)}</p><p>Support: ${html(e.support)}. Selection: ${html(e.selection)}. Optionality: ${html(e.optionality)}. ${html(e.group ? 'Alternative group: ' + e.group : '')}</p><p>${html(e.note)}</p><p>Integration: ${html(e.mechanism)}. Configuration locations: ${html(e.configuration.join('; ') || 'Unknown')}</p><p>Conditions: ${html(e.conditions.join('; ') || 'None recorded')}</p><p>Support evidence: ${evidence(e.evidence)}. Selection evidence: ${evidence(e.selectionEvidence)}</p>${e.observations.map(o => `<p>Observed ${html(o.at)}: ${html(o.note)} — ${evidence(o.evidence)}</p>`).join('')}</details>`).join('');
36
+ const ev = (s.bom.annotations ?? []).map((a) => `<article id="${fragmentId('ev', a['bom-ref'])}"><h3>${html(a['bom-ref'])}</h3><p>${html(a.text)}</p><p>${html(a.timestamp)} · ${html(a.annotator.individual?.name ?? a.annotator.organization?.name ?? 'Recorded annotator')}</p>${s.evidence.filter(e => e.id === a['bom-ref']).map(e => `<p>${html(e.basis)} — ${html(e.applicability)}</p>${e.sources.map(ref => { const source = s.bom.externalReferences[ref.index]; return `<p>Source: ${html(source.url)} · ${html(ref.locator)}<br>${html(source.comment ?? '')}<br>${html((source.hashes ?? []).map((h) => h.alg + ': ' + h.content).join('; '))}</p>`; }).join('')}`).join('')}</article>`).join('');
37
+ const ul = (items) => '<ul>' + items.map(x => `<li>${html(x)}</li>`).join('') + '</ul>';
38
+ const journey = (entries) => entries.map(e => `<article><p class="tag">${html(e.kind)} · ${html(e.status)}</p><h3>${html(e.title)}</h3><p>${html(e.text)}</p><p>Why: ${html(e.reason)}<br>Access needed: ${html(e.access)}</p><p>${html(e.at)} · ${html(e.id)}${e.parent ? ' · Branch of ' + html(e.parent) : ''}${e.supersedes ? ' · Supersedes ' + html(e.supersedes) : ''}</p><p>${evidence(e.evidence)}</p></article>`).join('');
39
+ const body = `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline'; img-src 'none'; base-uri 'none'; form-action 'none'"><title>${html(title)}</title><style>
40
+ body{margin:0;background:#f5f3ec;color:#18362e;font:17px/1.6 system-ui,sans-serif}main{max-width:1200px;margin:auto;padding:40px 28px}header{border-bottom:2px solid #18362e;padding-bottom:25px}h1{font:42px/1.1 Georgia,serif;max-width:1000px}h2{font:28px Georgia,serif;margin-top:48px}a{color:#14634b}nav{display:flex;gap:22px;flex-wrap:wrap}small,.muted{color:#58675f}svg{width:100%;min-width:1000px}svg rect{fill:#e6ebe1;stroke:#a3b7a7}svg text{fill:#18362e;font:14px system-ui,sans-serif}.boundary-label{font-weight:bold}.area-label{font-style:italic}.selected{stroke:#14634b;stroke-width:3;fill:none}.possible{stroke:#64746d;stroke-width:2;stroke-dasharray:7 5;fill:none}svg a:focus{outline:3px solid #ba882f}svg path:hover{stroke:#ba882f;stroke-width:5}.map{overflow:auto;background:white;border:1px solid #d6dace;border-radius:8px;margin:24px 0}details,article{background:#fff;padding:18px 22px;margin:12px 0;border:1px solid #d6dace;border-radius:6px;overflow-wrap:anywhere}summary{cursor:pointer;font-weight:600}summary small{display:block;font-weight:400}:target{outline:3px solid #ba882f}li{margin:10px 0}.tag{letter-spacing:.12em;font-size:12px;text-transform:uppercase}p,li,h1,h2,h3{overflow-wrap:anywhere}
41
+ </style></head><body><main><header><p class="tag">Initial security overview</p><h1>${html(title)}</h1><p>${html(intro)}</p><p>Scope: ${html(s.scope)}</p><nav><a href="#overview">Overview</a><a href="#findings">Security observations</a><a href="#coverage">Coverage</a><a href="#next">Next step</a><a href="#relationships">Relationships</a><a href="#evidence">Evidence</a><a href="brief.md">Brief</a></nav></header>
42
+ <section id="overview"><h2>Connected application overview</h2><p>Each node appears once. Grouping records placement, not verified deployed trust boundaries. Inspect its evidence basis: source, user intent, configuration or dated observation. Unclassified means placement has not been established.</p><p>Solid green: selected. Dashed: selection unestablished or not selected; inspect the support state in details. Arrowheads show direction. Click a node for its record or a path for its details.</p><p>${html(scopeNote)}</p>${renderOverview(s)}</section>
43
+ <section id="findings"><h2>Security observations and investigation leads</h2>${findings.length ? journey(findings) : '<p>No evidence-backed findings recorded. Continue research; this is not an assurance conclusion.</p>'}</section>
44
+ <section id="coverage"><h2>Coverage and gaps</h2><h3>Researched</h3>${s.coverage.length ? ul(s.coverage) : '<p>No coverage recorded.</p>'}<h3>Unknown or not researched</h3>${s.gaps.length ? ul(s.gaps) : '<p>No gaps recorded; completeness is not established.</p>'}</section>
45
+ <section id="next"><h2>Next step</h2><p>${html(continuation)}</p>${journey(next)}</section>
46
+ <section id="relationships"><h2>Detailed relationships</h2>${details || '<p>No relationships recorded yet.</p>'}</section>
47
+ <section id="inventory"><h2>Component and service records</h2>${groups.map(g => `<h3>${html(boundaryNames[g.boundary])} · ${html(g.area)}</h3>${g.nodes.map(n => `<article id="${fragmentId('node', n['bom-ref'])}"><h3>${html(n.name)}</h3><p>${html(n.description ?? 'Description not recorded.')}</p><p>${html(n.type ?? 'service')} · ${html(n['bom-ref'])}${s.relationships.some(e => e.from === n['bom-ref'] || e.to === n['bom-ref']) ? '' : ' · Connections unknown.'}</p><p>Grouping evidence: ${(s.presentation[n['bom-ref']]?.evidence ?? []).map(id => html(basis(id)) + ' — ' + evidence([id])).join('; ') || 'Not recorded'}</p></article>`).join('')}`).join('')}</section>
48
+ <section id="journey"><h2>Investigation journey</h2><p>Choose a next step, inspect the full journey, redirect or stop.</p>${journey(s.entries)}</section><section id="evidence"><h2>Evidence and decisions</h2>${ev}</section><footer><h2>Saved detail and interoperability</h2><p>${html(interchange)}</p><p><a href="investigation.md">Journal file</a> · <a href="surface.cdx.json">Saved Surface</a></p><p>${html(mechanics)} This view proves neither completeness nor runtime use.</p></footer></main></body></html>\n`;
49
+ return { 'brief.md': brief, 'investigation.md': journal, 'surface.html': body };
50
+ }
@@ -0,0 +1,4 @@
1
+ import type { Artefact } from './contracts.js';
2
+ export declare function validateArtefact(input: unknown): Artefact;
3
+ /** Explicit offline bundle verification. Missing external context is an error here. */
4
+ export declare function verifyReferences(input: unknown, context: unknown[]): void;