@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,482 @@
1
+ import { canonical, digest, fail, portable } from './portable.js';
2
+ const text = (v, p) => { if (typeof v !== 'string' || !v.trim())
3
+ fail(p, 'requires nonempty string'); };
4
+ const match = (pattern, label) => (v, p) => { text(v, p); if (!pattern.test(v))
5
+ fail(p, label); };
6
+ const id = match(/^[a-z][a-z0-9+.-]*:[^\s]+$/i, 'requires namespaced identifier');
7
+ const token = match(/^[A-Za-z0-9][A-Za-z0-9._-]*$/, 'requires revision/version token');
8
+ const hash = match(/^sha256:[a-f0-9]{64}$/, 'requires SHA-256 digest');
9
+ const time = (v, p) => {
10
+ text(v, p);
11
+ if (!/^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\dZ$/.test(v) || !Number.isFinite(Date.parse(v)) || new Date(v).toISOString() !== v.replace('Z', '.000Z'))
12
+ fail(p, 'requires real UTC timestamp with seconds');
13
+ };
14
+ const oneOf = (...values) => (v, p) => { if (!values.includes(v))
15
+ fail(p, `expected ${values.join('|')}`); };
16
+ const array = (check, min = 0) => (v, p) => {
17
+ if (!Array.isArray(v) || v.length < min)
18
+ fail(p, `requires array with at least ${min} entries`);
19
+ v.forEach((x, i) => check(x, `${p}[${i}]`));
20
+ };
21
+ function object(v, p) {
22
+ if (v === null || typeof v !== 'object' || Array.isArray(v))
23
+ fail(p, 'requires object');
24
+ return v;
25
+ }
26
+ const shape = (required, optional = {}) => (v, p) => {
27
+ const data = object(v, p);
28
+ for (const [key, check] of Object.entries(required)) {
29
+ if (!Object.hasOwn(data, key))
30
+ fail(`${p}.${key}`, 'required');
31
+ check(data[key], `${p}.${key}`);
32
+ }
33
+ for (const key of Object.keys(data)) {
34
+ if (Object.hasOwn(required, key))
35
+ continue;
36
+ if (!Object.hasOwn(optional, key))
37
+ fail(`${p}.${key}`, 'unsupported field; preserve additions in extensions');
38
+ optional[key](data[key], `${p}.${key}`);
39
+ }
40
+ };
41
+ const dictionary = (keyCheck, valueCheck) => (v, p) => {
42
+ for (const [k, value] of Object.entries(object(v, p))) {
43
+ keyCheck(k, `${p} key`);
44
+ valueCheck(value, `${p}.${k}`);
45
+ }
46
+ };
47
+ const value = (v, p) => portable(v, p);
48
+ const state = oneOf('known', 'unknown', 'omitted', 'redacted', 'absent', 'out-of-scope');
49
+ const ref = shape({ id, revision: token, digest: hash });
50
+ const revision = shape({ id: token, parent: (v, p) => { if (v !== null)
51
+ ref(v, p); }, createdAt: time, author: text, reason: text });
52
+ const common = { id, title: text, revision, scope: text, extensions: dictionary(id, value) };
53
+ const evidence = shape({ source: id, locator: text });
54
+ const location = (v, p) => {
55
+ text(v, p);
56
+ const s = v;
57
+ if (/^https?:\/\//.test(s)) {
58
+ const url = new URL(s);
59
+ if (url.username || url.password)
60
+ fail(p, 'credentials in source URL');
61
+ return;
62
+ }
63
+ if (s.startsWith('urn:')) {
64
+ id(v, p);
65
+ return;
66
+ }
67
+ if (s.startsWith('/') || /[\\:?#%]/.test(s) || s.split('/').some(part => !part || part === '.' || part === '..'))
68
+ fail(p, 'requires portable relative path or http(s)/urn reference');
69
+ };
70
+ const finding = (v, p) => {
71
+ shape({ id, subject: id, property: text, state: oneOf('known', 'unknown', 'omitted', 'redacted', 'absent', 'out-of-scope', 'conflicting'), note: text,
72
+ claims: array(shape({ value, evidence: array(evidence, 1) })) })(v, p);
73
+ const f = v;
74
+ const distinct = new Set(f.claims.map(c => canonical(c.value))).size;
75
+ if ((f.state === 'known' || f.state === 'absent') && distinct !== 1)
76
+ fail(p, 'known/absent requires one agreed claim value');
77
+ if (f.state === 'conflicting' && distinct < 2)
78
+ fail(p, 'conflicting requires differing sourced claims');
79
+ if (['unknown', 'omitted', 'redacted', 'out-of-scope'].includes(f.state) && f.claims.length)
80
+ fail(p, 'non-value state must not carry claims');
81
+ };
82
+ const research = shape({
83
+ ...common, contract: oneOf('n8r.research/0.1'),
84
+ sources: array(shape({ id, location, version: text, digest: hash, inspectedAt: time,
85
+ coverage: array(shape({ path: text, status: oneOf('inspected', 'missing', 'unreadable', 'excluded', 'truncated', 'unsupported'), note: text }), 1) })),
86
+ findings: array(finding),
87
+ alternatives: array(shape({ id, title: text, findings: array(id), requirements: array(text) })),
88
+ decisions: array(shape({ id, alternative: id, choice: oneOf('selected', 'rejected', 'deferred'), reason: text, by: text, at: time })),
89
+ questions: array(shape({ id, text, findings: array(id) })),
90
+ notes: array(shape({ by: text, at: time, text }))
91
+ });
92
+ const fact = (v, p) => {
93
+ shape({ state, note: text }, { value })(v, p);
94
+ const f = object(v, p);
95
+ if ((f.state === 'known') !== Object.hasOwn(f, 'value'))
96
+ fail(p, 'only known facts require a value');
97
+ };
98
+ const entity = { id, type: shape({ vocabulary: id, term: text }), attributes: dictionary(text, fact),
99
+ basis: array(shape({ record: id, revision: token, items: array(id, 1) })), requirements: array(id) };
100
+ const surface = shape({
101
+ ...common, contract: oneOf('n8r.surface/0.1'), kind: oneOf('definition'), records: array(ref),
102
+ vocabularies: array(shape({ id, version: token })),
103
+ nodes: array(shape({ ...entity, label: text })),
104
+ relationships: array(shape({ ...entity, from: id, to: id, claim: oneOf('declared') })),
105
+ requirements: array(shape({ id, kind: oneOf('secret', 'resource', 'configuration'), description: text, state })),
106
+ coverage: array(text), gaps: array(text)
107
+ });
108
+ function unique(ids, p) {
109
+ if (new Set(ids).size !== ids.length)
110
+ fail(p, 'duplicate identity/reference');
111
+ return new Set(ids);
112
+ }
113
+ function references(ids, available, p) {
114
+ unique(ids, p);
115
+ for (const item of ids)
116
+ if (!available.has(item))
117
+ fail(p, `unresolved reference ${item}`);
118
+ }
119
+ // Inspect only a data descriptor; never invoke an untrusted getter to dispatch.
120
+ function isDescription(input) {
121
+ return input !== null && typeof input === 'object' &&
122
+ ['n8r.surface/0.2', 'n8r.surface/0.3'].includes(Object.getOwnPropertyDescriptor(input, 'contract')?.value);
123
+ }
124
+ export function validateArtefact(input) {
125
+ try {
126
+ return validate(input);
127
+ }
128
+ catch (error) {
129
+ if (isDescription(input))
130
+ fail('$description', 'invalid structure, identity, state or reference');
131
+ throw error;
132
+ }
133
+ }
134
+ function validate(input) {
135
+ canonical(input); // checks portable values, depth and logical size before field access
136
+ const data = object(input, '$');
137
+ if (data.contract === 'n8r.research/0.1') {
138
+ research(input, '$');
139
+ const r = input;
140
+ unique([r.id, ...r.sources.map(x => x.id), ...r.findings.map(x => x.id), ...r.alternatives.map(x => x.id), ...r.decisions.map(x => x.id), ...r.questions.map(x => x.id)], '$');
141
+ const sources = new Set(r.sources.map(x => x.id)), findings = new Set(r.findings.map(x => x.id)), alternatives = new Set(r.alternatives.map(x => x.id));
142
+ for (const f of r.findings)
143
+ for (const c of f.claims)
144
+ for (const e of c.evidence)
145
+ references([e.source], sources, f.id);
146
+ for (const a of [...r.alternatives, ...r.questions])
147
+ references(a.findings, findings, a.id);
148
+ for (const d of r.decisions)
149
+ references([d.alternative], alternatives, d.id);
150
+ unique(r.decisions.map(d => d.alternative), '$.decisions'); // one current decision per option; old decisions stay in prior revisions
151
+ }
152
+ else if (data.contract === 'n8r.surface/0.1') {
153
+ surface(input, '$');
154
+ const s = input;
155
+ unique([s.id, ...s.nodes.map(x => x.id), ...s.relationships.map(x => x.id), ...s.requirements.map(x => x.id)], '$');
156
+ const nodes = new Set(s.nodes.map(x => x.id)), vocab = unique(s.vocabularies.map(x => x.id), '$.vocabularies');
157
+ const requirements = new Set(s.requirements.map(x => x.id));
158
+ const records = unique(s.records.map(x => JSON.stringify([x.id, x.revision])), '$.records');
159
+ for (const e of [...s.nodes, ...s.relationships]) {
160
+ references([e.type.vocabulary], vocab, e.id);
161
+ references(e.requirements, requirements, e.id);
162
+ for (const b of e.basis) {
163
+ references([JSON.stringify([b.record, b.revision])], records, e.id);
164
+ unique(b.items, e.id);
165
+ }
166
+ }
167
+ for (const e of s.relationships) {
168
+ references([e.from], nodes, e.id);
169
+ references([e.to], nodes, e.id);
170
+ }
171
+ }
172
+ else if (data.contract === 'n8r.surface/0.2') {
173
+ validateDescription(input);
174
+ }
175
+ else if (data.contract === 'n8r.surface/0.3') {
176
+ validateGraph(input);
177
+ }
178
+ else
179
+ fail('$.contract', 'unsupported contract/version');
180
+ const a = input;
181
+ if (a.revision.parent && (a.revision.parent.id !== a.id || a.revision.parent.revision === a.revision.id))
182
+ fail('$.revision.parent', 'requires earlier revision of the same artefact');
183
+ return a;
184
+ }
185
+ /** Explicit offline bundle verification. Missing external context is an error here. */
186
+ export function verifyReferences(input, context) {
187
+ try {
188
+ verifyContext(input, context);
189
+ }
190
+ catch (error) {
191
+ if (isDescription(input))
192
+ fail('$description', 'invalid or unavailable evidence, capability or exclusive selection');
193
+ throw error;
194
+ }
195
+ }
196
+ function verifyContext(input, context) {
197
+ const a = validateArtefact(input);
198
+ const records = context.map(validateArtefact);
199
+ unique(records.map(r => JSON.stringify([r.id, r.revision.id])), '$context');
200
+ if (a.contract === 'n8r.surface/0.2' || a.contract === 'n8r.surface/0.3') {
201
+ verifyDescriptionContext(a, records);
202
+ return;
203
+ }
204
+ if (a.contract !== 'n8r.surface/0.1')
205
+ return;
206
+ for (const ref of a.records) {
207
+ const r = records.find(x => x.id === ref.id && x.revision.id === ref.revision);
208
+ if (!r || r.contract !== 'n8r.research/0.1')
209
+ fail(ref.id, 'research revision unavailable');
210
+ if (digest(r) !== ref.digest)
211
+ fail(ref.id, 'research digest mismatch');
212
+ const items = new Set([...r.findings, ...r.alternatives, ...r.decisions, ...r.questions].map(x => x.id));
213
+ for (const e of [...a.nodes, ...a.relationships])
214
+ for (const b of e.basis) {
215
+ if (b.record === ref.id && b.revision === ref.revision)
216
+ references(b.items, items, e.id);
217
+ }
218
+ }
219
+ }
220
+ const basis = shape({ record: id, revision: token, items: array(id, 1) });
221
+ const descriptionType = shape({ vocabulary: id, term: text });
222
+ const descriptionCommon = {
223
+ ...common, contract: oneOf('n8r.surface/0.2'), extensions: shape({}),
224
+ records: array(ref), vocabularies: array(shape({ id, version: token })),
225
+ nodes: array(shape({ id, label: text, type: descriptionType })),
226
+ coverage: array(text), gaps: array(text)
227
+ };
228
+ const capabilityShape = shape({
229
+ ...descriptionCommon, kind: oneOf('capability'),
230
+ capabilities: array(shape({ id, from: id, to: id, type: descriptionType,
231
+ state: oneOf('possible', 'unknown', 'omitted', 'redacted', 'absent', 'out-of-scope', 'conflicting'),
232
+ basis: array(basis), conditions: array(text), note: text })),
233
+ exclusiveGroups: array(shape({ id, members: array(id, 2) }))
234
+ });
235
+ const boundShape = shape({
236
+ ...descriptionCommon, kind: oneOf('bound'), capability: ref,
237
+ instance: shape({ id, label: text }),
238
+ bindings: array(shape({ id, capability: id,
239
+ state: oneOf('configured', 'unknown', 'omitted', 'redacted', 'out-of-scope', 'conflicting'),
240
+ claims: array(shape({ target: id, basis: array(basis, 1) })), decisions: array(basis), note: text }))
241
+ });
242
+ function descriptionBases(a) {
243
+ return a.kind === 'capability' ? a.capabilities.flatMap(c => c.basis) :
244
+ a.bindings.flatMap(b => [...b.decisions, ...b.claims.flatMap(c => c.basis)]);
245
+ }
246
+ function validateDescription(input) {
247
+ const data = object(input, '$');
248
+ (data.kind === 'capability' ? capabilityShape : boundShape)(input, '$');
249
+ const a = input;
250
+ const entries = a.kind === 'capability' ? [...a.capabilities, ...a.exclusiveGroups] : a.bindings;
251
+ unique([a.id, ...a.nodes.map(n => n.id), ...entries.map(e => e.id), ...(a.kind === 'bound' ? [a.instance.id] : [])], '$');
252
+ const nodes = new Set(a.nodes.map(n => n.id));
253
+ const vocab = unique(a.vocabularies.map(v => v.id), '$.vocabularies');
254
+ const records = unique(a.records.map(r => JSON.stringify([r.id, r.revision])), '$.records');
255
+ for (const n of a.nodes)
256
+ references([n.type.vocabulary], vocab, '$.nodes');
257
+ for (const b of descriptionBases(a)) {
258
+ references([JSON.stringify([b.record, b.revision])], records, '$.basis');
259
+ unique(b.items, '$.basis.items');
260
+ }
261
+ if (a.kind === 'capability') {
262
+ for (const c of a.capabilities) {
263
+ references([c.from], nodes, '$.capabilities.from');
264
+ references([c.to], nodes, '$.capabilities.to');
265
+ references([c.type.vocabulary], vocab, '$.capabilities.type');
266
+ if (['possible', 'absent', 'conflicting'].includes(c.state) && !c.basis.length)
267
+ fail('$', 'evidence required');
268
+ }
269
+ for (const group of a.exclusiveGroups)
270
+ references(group.members, new Set(a.capabilities.map(c => c.id)), '$.exclusiveGroups');
271
+ }
272
+ else {
273
+ unique(a.bindings.map(b => b.capability), '$.bindings.capability');
274
+ for (const b of a.bindings) {
275
+ const count = b.claims.length;
276
+ if (b.state === 'configured' ? count !== 1 : b.state === 'conflicting' ? count < 2 : count !== 0)
277
+ fail('$', 'state/claims mismatch');
278
+ references(b.claims.map(c => c.target), nodes, '$.bindings.targets');
279
+ }
280
+ }
281
+ }
282
+ function verifyDescriptionContext(a, context) {
283
+ const research = a.records.map(ref => {
284
+ const r = context.find(r => r.id === ref.id && r.revision.id === ref.revision);
285
+ if (!r || r.contract !== 'n8r.research/0.1' || digest(r) !== ref.digest)
286
+ fail('$', 'research revision/digest unavailable');
287
+ return r;
288
+ });
289
+ const items = (bases, decisions = false) => bases.flatMap(b => {
290
+ const r = research.find(r => r.id === b.record && r.revision.id === b.revision);
291
+ return b.items.map(id => {
292
+ const item = (decisions ? r.decisions : r.findings).find(f => f.id === id);
293
+ if (!item)
294
+ fail('$', 'wrong or missing evidence item kind');
295
+ return item;
296
+ });
297
+ });
298
+ const findings = (bases) => items(bases);
299
+ if (a.contract === 'n8r.surface/0.3')
300
+ verifyRelationshipDetails(a, research);
301
+ const verifySupport = (c) => {
302
+ const fs = findings(c.basis);
303
+ if (fs.some(f => f.subject !== c.from || f.property !== 'supports'))
304
+ fail('$', 'capability evidence subject/property mismatch');
305
+ if (c.state === 'possible' && (!fs.length || fs.some(f => f.state !== 'known' || f.claims.some(cl => cl.value !== c.to))))
306
+ fail('$', 'possible requires supported target evidence');
307
+ if (c.state === 'absent' && (!fs.length || fs.some(f => f.state !== 'absent' || f.claims.some(cl => cl.value !== c.to))))
308
+ fail('$', 'absent requires scoped absence evidence');
309
+ if (c.state === 'conflicting' && !fs.some(f => f.state === 'conflicting'))
310
+ fail('$', 'conflicting requires conflicting finding');
311
+ };
312
+ if (a.kind === 'capability') {
313
+ for (const c of capabilityEdges(a))
314
+ verifySupport(c);
315
+ return;
316
+ }
317
+ const ref = a.capability;
318
+ const c = context.find(c => c.id === ref.id && c.revision.id === ref.revision);
319
+ if (!c || (c.contract !== 'n8r.surface/0.2' && c.contract !== 'n8r.surface/0.3') || c.contract !== a.contract || c.kind !== 'capability' || digest(c) !== ref.digest)
320
+ fail('$', 'capability revision/digest unavailable');
321
+ verifyDescriptionContext(c, context);
322
+ // A shared node ID denotes the same component; an instance is a distinct identity.
323
+ if ([c.id, ...c.nodes.map(n => n.id), ...capabilityEdges(c).map(e => e.id)].includes(a.instance.id))
324
+ fail('$', 'instance identity reused');
325
+ for (const n of a.nodes) {
326
+ const old = c.nodes.find(x => x.id === n.id);
327
+ if (old && canonical(old.type) !== canonical(n.type))
328
+ fail('$', 'shared component type changed');
329
+ }
330
+ if (a.contract === 'n8r.surface/0.3') {
331
+ for (const edge of a.relationships) {
332
+ const base = capabilityEdges(c).find(x => x.id === edge.id);
333
+ if (!base || edge.from !== base.from || edge.to !== base.to || canonical(edge.type) !== canonical(base.type))
334
+ fail('$', 'relationship differs from pinned Capability');
335
+ verifySupport({ ...edge, state: base.state });
336
+ }
337
+ }
338
+ for (const b of boundBindings(a)) {
339
+ const capability = capabilityEdges(c).find(c => c.id === b.capability);
340
+ if (!capability)
341
+ fail('$', 'unknown capability');
342
+ if (b.state === 'configured' && capability.state !== 'possible')
343
+ fail('$', 'configured requires established possibility');
344
+ items(b.decisions, true);
345
+ const targets = new Set(b.claims.map(cl => cl.target));
346
+ if (b.claims.length) {
347
+ // Do not allow an author to hide contrary evidence by omitting a basis item.
348
+ // Only this Bound snapshot's records apply; the pinned Capability may use
349
+ // older research revisions which must not override the current binding.
350
+ const matching = research.flatMap(r => r.findings).filter(f => f.subject === a.instance.id && f.property === b.capability &&
351
+ ['known', 'conflicting'].includes(f.state));
352
+ for (const f of matching) {
353
+ if (f.claims.some(cl => typeof cl.value !== 'string' || !targets.has(cl.value)) ||
354
+ (b.state === 'configured' && f.state === 'conflicting'))
355
+ fail('$', 'omitted contradictory configuration evidence');
356
+ }
357
+ }
358
+ for (const claim of b.claims) {
359
+ const fs = findings(claim.basis);
360
+ for (const f of fs) {
361
+ if (f.subject !== a.instance.id || f.property !== b.capability ||
362
+ !['known', 'conflicting'].includes(f.state) ||
363
+ !f.claims.some(cl => cl.value === claim.target) ||
364
+ f.claims.some(cl => typeof cl.value !== 'string' || !targets.has(cl.value)))
365
+ fail('$', 'binding evidence mismatch or suppressed conflict');
366
+ if (b.state === 'configured' && f.state !== 'known')
367
+ fail('$', 'conflict cannot establish configured');
368
+ }
369
+ }
370
+ }
371
+ for (const group of c.exclusiveGroups) {
372
+ if (boundBindings(a).filter(b => b.state === 'configured' && group.members.includes(b.capability)).length > 1)
373
+ fail('$', 'exclusive capabilities configured together');
374
+ }
375
+ }
376
+ function capabilityEdges(a) {
377
+ return a.contract === 'n8r.surface/0.3' ? a.relationships : a.capabilities;
378
+ }
379
+ function boundBindings(a) {
380
+ return a.contract === 'n8r.surface/0.3' ? a.relationships.map(e => ({ id: e.id, capability: e.id, ...e.binding })) : a.bindings;
381
+ }
382
+ const configLocation = (v, p) => {
383
+ text(v, p);
384
+ const s = v;
385
+ if (s.startsWith('/') || /[\\:?#%]/.test(s) || s.split('/').some(part => !part || part === '.' || part === '..'))
386
+ fail(p, 'requires safe relative documentation location');
387
+ };
388
+ const configReference = shape({ owner: id, mechanism: oneOf('environment-variable', 'file-key', 'argument', 'admin-setting', 'code'), key: text, location: configLocation });
389
+ const detailShape = (check) => (v, p) => {
390
+ shape({ state: oneOf('known', 'unknown', 'omitted', 'redacted', 'absent', 'out-of-scope', 'conflicting'),
391
+ claims: array(shape({ value: check, basis: array(basis, 1) })), note: text })(v, p);
392
+ const d = v;
393
+ const count = d.claims.length;
394
+ if (d.state === 'known' || d.state === 'absent') {
395
+ if (count !== 1)
396
+ fail(p, 'known/absent requires one evidenced claim');
397
+ }
398
+ else if (d.state === 'conflicting') {
399
+ if (count < 2 || new Set(d.claims.map(c => canonical(c))).size !== count)
400
+ fail(p, 'conflicting requires distinct evidenced claims');
401
+ }
402
+ else if (count)
403
+ fail(p, 'non-value state cannot carry claims');
404
+ };
405
+ const sharedRelationship = { id, from: id, to: id, type: descriptionType,
406
+ basis: array(basis), conditions: array(text), note: text,
407
+ purpose: detailShape(text), integration: detailShape(text), configuration: detailShape(array(configReference)) };
408
+ const bindingAnnotation = shape({
409
+ state: oneOf('configured', 'unknown', 'omitted', 'redacted', 'out-of-scope', 'conflicting'),
410
+ claims: array(shape({ target: id, basis: array(basis, 1) })), decisions: array(basis), note: text
411
+ });
412
+ const graphCommon = { ...descriptionCommon, contract: oneOf('n8r.surface/0.3') };
413
+ const capabilityGraphShape = shape({ ...graphCommon, kind: oneOf('capability'),
414
+ relationships: array(shape({ ...sharedRelationship, state: oneOf('possible', 'unknown', 'omitted', 'redacted', 'absent', 'out-of-scope', 'conflicting') })),
415
+ exclusiveGroups: array(shape({ id, members: array(id, 2) })) });
416
+ const boundGraphShape = shape({ ...graphCommon, kind: oneOf('bound'), capability: ref, instance: shape({ id, label: text }),
417
+ relationships: array(shape({ ...sharedRelationship, binding: bindingAnnotation })) });
418
+ const detailNames = ['purpose', 'integration', 'configuration'];
419
+ function validateGraph(input) {
420
+ const data = object(input, '$');
421
+ (data.kind === 'capability' ? capabilityGraphShape : boundGraphShape)(input, '$');
422
+ const a = input;
423
+ // Reuse the bounded 0.2 local state/identity checks without changing the input
424
+ // or the meaning/version of either serialized contract.
425
+ const legacy = a.kind === 'capability' ? {
426
+ ...a, contract: 'n8r.surface/0.2', capabilities: a.relationships.map(e => {
427
+ const { purpose, integration, configuration, ...core } = e;
428
+ return core;
429
+ })
430
+ } : { ...a, contract: 'n8r.surface/0.2', bindings: boundBindings(a) };
431
+ delete legacy.relationships;
432
+ validateDescription(legacy);
433
+ const nodes = new Set(a.nodes.map(n => n.id));
434
+ const vocab = new Set(a.vocabularies.map(v => v.id));
435
+ const records = new Set(a.records.map(r => JSON.stringify([r.id, r.revision])));
436
+ for (const e of a.relationships) {
437
+ references([e.from], nodes, '$.relationships.from');
438
+ references([e.to], nodes, '$.relationships.to');
439
+ references([e.type.vocabulary], vocab, '$.relationships.type');
440
+ const bases = [...e.basis, ...detailNames.flatMap(name => e[name].claims.flatMap(c => c.basis))];
441
+ for (const b of bases) {
442
+ references([JSON.stringify([b.record, b.revision])], records, '$.relationships.basis');
443
+ unique(b.items, '$.relationships.items');
444
+ }
445
+ for (const claim of e.configuration.claims)
446
+ for (const config of claim.value)
447
+ references([config.owner], nodes, '$.configuration.owner');
448
+ }
449
+ }
450
+ function verifyRelationshipDetails(a, research) {
451
+ for (const e of a.relationships)
452
+ for (const property of detailNames) {
453
+ const detail = e[property];
454
+ const values = new Set(detail.claims.map(c => canonical(c.value)));
455
+ const assertions = new Set();
456
+ for (const claim of detail.claims)
457
+ for (const b of claim.basis) {
458
+ const r = research.find(r => r.id === b.record && r.revision.id === b.revision);
459
+ for (const id of b.items) {
460
+ const f = r.findings.find(f => f.id === id);
461
+ if (!f || f.subject !== e.id || f.property !== property ||
462
+ (detail.state === 'known' ? f.state !== 'known' : detail.state === 'absent' ? f.state !== 'absent' : !['known', 'absent', 'conflicting'].includes(f.state)) ||
463
+ !f.claims.some(c => canonical(c.value) === canonical(claim.value)) ||
464
+ f.claims.some(c => !values.has(canonical(c.value))))
465
+ fail('$', 'relationship detail evidence mismatch');
466
+ assertions.add(f.state);
467
+ }
468
+ }
469
+ if (detail.state === 'conflicting' && values.size < 2 &&
470
+ !(assertions.has('known') && assertions.has('absent')))
471
+ fail('$', 'agreeing evidence does not establish conflict');
472
+ if (detail.claims.length) {
473
+ const matching = research.flatMap(r => r.findings).filter(f => f.subject === e.id && f.property === property);
474
+ for (const f of matching) {
475
+ if (f.claims.some(c => !values.has(canonical(c.value))) ||
476
+ (detail.state === 'known' && ['absent', 'conflicting'].includes(f.state)) ||
477
+ (detail.state === 'absent' && ['known', 'conflicting'].includes(f.state)))
478
+ fail('$', 'omitted contradictory relationship detail');
479
+ }
480
+ }
481
+ }
482
+ }
@@ -0,0 +1,133 @@
1
+ # n8r-surface capability map
2
+
3
+ `n8r-surface` is the proposed standalone tool for researching software and
4
+ authoring partial AI Surface Model descriptions with a human. Researcher is its
5
+ shared discovery skill. The tool can run independently and later exchange work
6
+ with n8r-labs.
7
+
8
+ The [shareable skill](researcher-skill.md) has bounded existing-harness evidence.
9
+ On 5 November 2026 the user prioritized standalone AI integration, then complete
10
+ save/reopen/continue verification, then npm distribution. Later n8r-labs adaptation
11
+ uses the same core in that separate repository.
12
+
13
+ **Status, 5 November 2026:** #15 supplies experimental artefact contracts,
14
+ JSON/YAML codecs, HTML ownership checks and synthetic cases. The command implements
15
+ [bare startup and vault setup](startup.md), plus a
16
+ [bounded Codex conversation](codex-research-verification.md). Saved continuation,
17
+ complete structured research and Lab integration remain outstanding.
18
+ [Skill verification](researcher-skill-verification.md) distinguishes instruction
19
+ trials from the pending Work-mode lifecycle trial. This is unreleased work;
20
+ the remaining map describes intended capabilities.
21
+
22
+ - [Experimental contracts and checks](contracts.md)
23
+ - [Command intent, outcomes and verification](command-outcomes.md)
24
+ - [Library verification evidence and remaining limits](verification.md)
25
+ - [Bare-startup installed-command evidence](startup-verification.md)
26
+ - [Synthetic acceptance cases](../../examples/researcher/README.md)
27
+ - [Host boundary and Lab review](host-contract.md)
28
+ - [Implementation decisions](implementation-decisions.md)
29
+ - [Next work and critical path](critical-path.md)
30
+ - [Design requirements and open choices](design.md)
31
+ - [Tracking issue #13](https://github.com/injectionator/n8r-surface/issues/13)
32
+ - [Map and planning issue #14](https://github.com/injectionator/n8r-surface/issues/14)
33
+
34
+ ## Capability map
35
+
36
+ The diagram shows the intended capabilities across hosts, not the delivery order.
37
+ The current skill produces working notes; structured artefacts remain later work.
38
+
39
+ ```mermaid
40
+ flowchart TD
41
+ Sources[Repository, folder or supplied documents] --> Read[Scoped source reading]
42
+ Human[Human questions and choices] --> Researcher[Researcher shared skill]
43
+ Read --> Researcher
44
+
45
+ subgraph Hosts[Entry points]
46
+ CLI[n8r-surface CLI]
47
+ Chat[Skill in a supported chat harness]
48
+ end
49
+ CLI --> Researcher
50
+ Chat --> Researcher
51
+ AI[One supported AI runner per host] --> Researcher
52
+ MoreAI[Additional AI runners] -.-> AI
53
+ Setup[Local setup and preferences] --> CLI
54
+
55
+ Researcher --> Record[HTML research record]
56
+ Record --> Compose[Select and refine a composition]
57
+ Human --> Compose
58
+ Compose --> Model[Partial AI Surface Model description]
59
+ Model --> JSON[JSON encoding]
60
+ Model --> YAML[YAML encoding]
61
+ Model -.-> Other[MessagePack and other mappings]
62
+
63
+ Record --> Corpus[Configurable artefact folder and recoverable revisions]
64
+ JSON --> Corpus
65
+ YAML --> Corpus
66
+ Corpus --> Resume[Find, read and continue saved work]
67
+ Resume --> Researcher
68
+
69
+ Corpus -.-> Exchange[Explicit import, export and handoff]
70
+ Exchange -.-> Lab[n8r-labs application: separate project]
71
+ Lab -.-> Exchange
72
+ Lab -.-> Researcher
73
+ ```
74
+
75
+ The AI runner and the destination for results are independent choices. A user
76
+ can research locally with their chosen supported AI, save files and later send
77
+ selected work to a Lab. A Lab connection is optional for standalone authoring.
78
+
79
+ ## Delivery boundaries
80
+
81
+ | Capability | Delivery position | Evidence required |
82
+ | --- | --- | --- |
83
+ | HTML research record and partial graph contract | Experimental implementation: [#15](https://github.com/injectionator/n8r-surface/issues/15) | Synthetic examples have explicit sources, decisions, gaps and stable references. |
84
+ | JSON and YAML representations | Codec and contract in #15; AI-authored output in #17 | Supported meanings survive conversion, including relationship attributes and unknowns. |
85
+ | TypeScript CLI with npm/npx distribution | Agreed; implement in #16 | Installed package and bundled assets work on supported Node.js versions without the source checkout or Bun. |
86
+ | Startup, source access and safe artefact tools | Paused remaining work: [#16](https://github.com/injectionator/n8r-surface/issues/16) | Setup, path handling, conflict detection and recovery work in temporary folders. |
87
+ | Researcher shared skill | First: [#17](https://github.com/injectionator/n8r-surface/issues/17) | Installed skill invocation, evidence-backed notes, human correction and save/reopen. |
88
+ | Standalone CLI runner | After skill and Lab adaptation | Real discovery, human correction, guarded save/resume and model output work without a Lab. |
89
+ | ChatGPT desktop / Work with supplied sources | First lifecycle-test host, in #17 | The same core skill produces usable records and descriptions in the actual host. |
90
+ | More AI runners and external harnesses | Later | Each adapter has demonstrated authentication, tool use and continuation. |
91
+ | Standalone executables | Later distribution option | Each supported platform runs the packaged tool and required assets without a separate Node.js installation. |
92
+ | MessagePack and additional encodings | Later, when a consumer needs them | A bounded mapping preserves meanings or reports unsupported information. |
93
+ | Researcher in n8r-labs and artefact exchange | Application stage after the skill trial, before standalone CLI | The application uses the versioned core and passes shared import/continuation cases. |
94
+ | Optional remote Researcher execution | Later integration | Source access, authentication and session scope are demonstrated against a named endpoint version. |
95
+ | Rich editing, automatic synchronisation and advanced corpus search | Outside the first path | A concrete use case justifies extending the simple folder workflow. |
96
+
97
+ ## Repository ownership
98
+
99
+ This repository owns the standalone CLI, portable Researcher skill, artefact
100
+ contracts, model encodings and consumer-side integration. The AI Surface Model
101
+ remains the one model; the CLI name does not rename it.
102
+
103
+ The n8r-labs project owns its application assistant, attachment UI, source access,
104
+ Lab sessions, storage, permissions and API/MCP endpoints. This repository
105
+ maintains the versioned Researcher core, artefact contracts and shared examples.
106
+ The application later consumes an identified revision through its own adapters;
107
+ it does not need to host the CLI or adopt its local folder layout.
108
+
109
+ Keep the [compatibility contract](design.md#compatibility-contract) current as
110
+ the tool develops. Review Lab requirements during #15 and carry the same
111
+ acceptance examples into application integration. Compatibility remains a goal
112
+ until a named application version passes those cases. Do not copy private
113
+ application prompts or operational records here.
114
+
115
+ ## Keeping the map useful
116
+
117
+ Issue #13 is the overview, this page is the capability map, and
118
+ [critical-path.md](critical-path.md) names the next delivery gate. Detailed
119
+ requirements live in [design.md](design.md); implementation issues carry their
120
+ own acceptance checks.
121
+
122
+ Update these documents in the PR that changes a capability or delivery decision.
123
+ Record implemented behaviour only with its verification evidence. Keep later
124
+ ideas on this map until they are ready to become bounded issues. A draft PR,
125
+ passing fixture checks and a working live integration are distinct states.
126
+
127
+ Use [State X → Input Y → verified Outcome Z](command-outcomes.md) as the acceptance
128
+ structure for each command increment. Attempt and repeat the actual product path,
129
+ and verify changed state independently. The current fixture/library checks do not
130
+ establish a successful repository-research command.
131
+
132
+ Related: [model overview](../../README.md), [draft concepts](../../spec/README.md),
133
+ [model roadmap](../roadmap.md), [interoperability](../interoperability.md).