@mnstry/atelier 0.2.0-alpha.13 → 0.2.0-alpha.15

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 (253) hide show
  1. package/CHANGELOG.md +283 -0
  2. package/README.md +102 -52
  3. package/SECURITY.md +1 -1
  4. package/TRADEMARKS.md +2 -2
  5. package/contracts/atelier-extension-manifest.v1.schema.json +412 -0
  6. package/contracts/atelier-graph-view.v1.schema.json +1085 -0
  7. package/contracts/public-api-baseline.json +298 -19
  8. package/docs/README.md +90 -0
  9. package/docs/blocks/capability-stewardship.md +1 -1
  10. package/docs/blocks/claims.md +2 -2
  11. package/docs/blocks/conformance-admission.md +1 -1
  12. package/docs/blocks/learning-harnesses.md +1 -1
  13. package/docs/{atelier-sync.md → guides/atelier-sync.md} +20 -14
  14. package/docs/{capability-stewardship.md → guides/capability-stewardship.md} +2 -4
  15. package/docs/guides/co-authoring.md +113 -0
  16. package/docs/{distributions.md → guides/distributions.md} +13 -8
  17. package/docs/guides/extensions.md +189 -0
  18. package/docs/guides/graph-refresh.md +152 -0
  19. package/docs/guides/graph-workspace.md +64 -0
  20. package/docs/{ingestion.md → guides/ingestion.md} +16 -15
  21. package/docs/{inquiry-harnesses.md → guides/inquiry-harnesses.md} +2 -3
  22. package/docs/{knowledge-workspace.md → guides/knowledge-workspace.md} +2 -2
  23. package/docs/{learning-harnesses.md → guides/learning-harnesses.md} +2 -3
  24. package/docs/{learning.md → guides/learning.md} +12 -5
  25. package/docs/{obsidian.md → guides/obsidian.md} +370 -23
  26. package/docs/guides/ontology-guide.md +111 -0
  27. package/docs/{personal-workspace.md → guides/personal-workspace.md} +17 -5
  28. package/docs/{research-starter.md → guides/research-starter.md} +1 -1
  29. package/docs/{tenant-readiness.md → guides/tenant-readiness.md} +39 -0
  30. package/docs/{upgrade.md → guides/upgrade.md} +197 -11
  31. package/docs/knowledge-health/assessment/capabilities.md +6 -15
  32. package/docs/knowledge-health/facilitator.md +15 -4
  33. package/docs/knowledge-health/{devday/participant-response.md → participant-response.md} +4 -2
  34. package/docs/knowledge-health/quickstart.md +97 -75
  35. package/docs/knowledge-health/readme.md +13 -5
  36. package/docs/maintainers/describing-the-atelier.md +173 -0
  37. package/docs/{release-engineering.md → maintainers/release-engineering.md} +5 -4
  38. package/docs/{announcements.md → reference/announcements.md} +1 -1
  39. package/docs/{architecture.md → reference/architecture.md} +31 -5
  40. package/docs/{atelier-runtime.md → reference/atelier-runtime.md} +1 -1
  41. package/docs/{connected-composition.md → reference/connected-composition.md} +7 -10
  42. package/docs/{coordination.md → reference/coordination.md} +1 -1
  43. package/docs/{decisions.md → reference/decisions.md} +3 -3
  44. package/docs/{design.md → reference/design.md} +1 -1
  45. package/docs/reference/edge-policy.md +151 -0
  46. package/docs/reference/graph-view-structured.md +119 -0
  47. package/docs/reference/graph-view.md +139 -0
  48. package/docs/reference/graph-workspace.md +37 -0
  49. package/docs/{intake-and-guides.md → reference/intake-and-guides.md} +3 -4
  50. package/docs/{knowledge-graph.md → reference/knowledge-graph.md} +120 -6
  51. package/docs/{local-services.md → reference/local-services.md} +43 -4
  52. package/docs/{obsidian-contract.md → reference/obsidian-contract.md} +29 -4
  53. package/docs/{obsidian-plugin.md → reference/obsidian-plugin.md} +218 -68
  54. package/docs/{practical-judgment.md → reference/phronesis.md} +12 -3
  55. package/docs/reference/portability.md +92 -0
  56. package/docs/{portable-presentation.md → reference/portable-presentation.md} +2 -2
  57. package/docs/{presentation-consumer-boundaries.md → reference/presentation-consumer-boundaries.md} +5 -0
  58. package/docs/{project-options.md → reference/project-options.md} +17 -0
  59. package/docs/{repo-boundary-guard.md → reference/repo-boundary-guard.md} +50 -4
  60. package/docs/{review-portability.md → reference/review-portability.md} +4 -0
  61. package/docs/{semantic-operation.md → reference/semantic-operation.md} +4 -3
  62. package/docs/{template-profile.md → reference/template-profile.md} +1 -1
  63. package/docs/{trackables.md → reference/trackables.md} +2 -2
  64. package/docs/start/independent-start.md +89 -0
  65. package/docs/{install.md → start/install.md} +9 -9
  66. package/docs/{knowledge-setup.md → start/knowledge-setup.md} +5 -5
  67. package/docs/{research-starter-quickstart.md → start/research-starter-quickstart.md} +6 -6
  68. package/fixtures/atelier-architecture/catalog/invalid/document.json +2 -2
  69. package/fixtures/atelier-architecture/catalog/valid/document.json +2 -2
  70. package/fixtures/graph-view/synthetic-authorized-input.v1.json +437 -0
  71. package/fixtures/graph-view/synthetic-view.v1.json +1 -0
  72. package/fixtures/knowledge-health/workshop/README.md +1 -1
  73. package/fixtures/templates/local-library.v1.json +3 -1
  74. package/package.json +22 -8
  75. package/plugins/obsidian/main.js +57 -8
  76. package/plugins/obsidian/manifest.json +1 -1
  77. package/skills/claude/atelier-guided-upgrade/SKILL.md +1 -1
  78. package/skills/claude/atelier-knowledge-setup/SKILL.md +2 -2
  79. package/skills/claude/atelier-local-service/SKILL.md +1 -1
  80. package/skills/claude/atelier-practical-judgment/SKILL.md +7 -2
  81. package/skills/claude/atelier-skill-steward/SKILL.md +1 -1
  82. package/skills/claude/mnstry-readiness/SKILL.md +2 -2
  83. package/skills/codex/atelier-guided-upgrade/SKILL.md +1 -1
  84. package/skills/codex/atelier-knowledge-setup/SKILL.md +2 -2
  85. package/skills/codex/atelier-local-service/SKILL.md +1 -1
  86. package/skills/codex/atelier-practical-judgment/SKILL.md +7 -2
  87. package/skills/codex/atelier-skill-steward/SKILL.md +1 -1
  88. package/skills/codex/mnstry-readiness/SKILL.md +2 -2
  89. package/src/architecture/catalog.mjs +2 -2
  90. package/src/attestation/sign.mjs +1 -1
  91. package/src/boundary/content-rules.mjs +534 -68
  92. package/src/boundary/policy.mjs +187 -60
  93. package/src/cli/run.mjs +26 -9
  94. package/src/commands/distribution.mjs +1 -1
  95. package/src/commands/extension.mjs +27 -0
  96. package/src/commands/ingest.mjs +1 -1
  97. package/src/commands/learn.mjs +1 -1
  98. package/src/commands/obsidian.mjs +49 -21
  99. package/src/commands/review.mjs +3 -0
  100. package/src/edge-policy/contract.mjs +69 -0
  101. package/src/edge-policy/policy.mjs +142 -0
  102. package/src/edge-policy/source-citation-adapter.mjs +43 -0
  103. package/src/extension-packs/loader.mjs +4 -3
  104. package/src/extensions/admission.mjs +83 -0
  105. package/src/extensions/client.d.mts +23 -0
  106. package/src/extensions/client.mjs +99 -0
  107. package/src/extensions/contracts.d.mts +29 -0
  108. package/src/extensions/contracts.mjs +163 -0
  109. package/src/extensions/convex.d.mts +24 -0
  110. package/src/extensions/convex.mjs +189 -0
  111. package/src/extensions/delegation.mjs +54 -0
  112. package/src/extensions/drizzle.d.mts +11 -0
  113. package/src/extensions/drizzle.mjs +33 -0
  114. package/src/extensions/postgres.d.mts +7 -0
  115. package/src/extensions/postgres.mjs +66 -0
  116. package/src/extensions/prisma.d.mts +13 -0
  117. package/src/extensions/prisma.mjs +63 -0
  118. package/src/extensions/project.mjs +432 -0
  119. package/src/extensions/scaffold-convex.mjs +130 -0
  120. package/src/extensions/scaffold-sql.mjs +144 -0
  121. package/src/extensions/server.d.mts +20 -0
  122. package/src/extensions/server.mjs +102 -0
  123. package/src/extensions/sql-scope.mjs +22 -0
  124. package/src/graph/graph.mjs +13 -5
  125. package/src/graph/knowledge-graph.mjs +20 -2
  126. package/src/graph/repo-contribution.mjs +405 -0
  127. package/src/graph/repo-contribution.types.mjs +32 -0
  128. package/src/graph-access/edge-decision-adapter.mjs +41 -0
  129. package/src/graph-access/edge-record-adapter.mjs +73 -0
  130. package/src/graph-access/index.mjs +253 -0
  131. package/src/graph-access/multi-contribution-record-adapter.mjs +101 -0
  132. package/src/graph-access/multi-record-projection-adapter.mjs +74 -0
  133. package/src/graph-access/source-adapter.mjs +81 -0
  134. package/src/graph-refresh/compare.mjs +110 -0
  135. package/src/graph-refresh/controller.mjs +324 -0
  136. package/src/graph-refresh/index.mjs +3 -0
  137. package/src/graph-refresh/local.mjs +78 -0
  138. package/src/graph-view/babylon/babylon-scene-port.mjs +64 -0
  139. package/src/graph-view/babylon/index.mjs +2 -0
  140. package/src/graph-view/babylon/navigator.mjs +312 -0
  141. package/src/graph-view/babylon/provenance.mjs +36 -0
  142. package/src/graph-view/babylon/scene-controller.mjs +131 -0
  143. package/src/graph-view/babylon/semantic-picking.mjs +35 -0
  144. package/src/graph-view/babylon/web-host.mjs +188 -0
  145. package/src/graph-view/cli.mjs +3 -0
  146. package/src/graph-view/command.mjs +34 -0
  147. package/src/graph-view/contracts.mjs +79 -0
  148. package/src/graph-view/errors.mjs +5 -0
  149. package/src/graph-view/index.mjs +4 -0
  150. package/src/graph-view/intents.mjs +30 -0
  151. package/src/graph-view/query.mjs +213 -0
  152. package/src/graph-view/refs.mjs +2 -0
  153. package/src/graph-view/structured/browser.mjs +262 -0
  154. package/src/graph-view/structured/styles.mjs +8 -0
  155. package/src/graph-workspace/authoring.mjs +185 -0
  156. package/src/graph-workspace/browser/constellation.mjs +369 -0
  157. package/src/graph-workspace/browser/dom.mjs +23 -0
  158. package/src/graph-workspace/browser/index.mjs +6 -0
  159. package/src/graph-workspace/browser/review.mjs +55 -0
  160. package/src/graph-workspace/browser/styles.mjs +11 -0
  161. package/src/graph-workspace/browser/workbench.mjs +351 -0
  162. package/src/graph-workspace/browser/workspace.mjs +431 -0
  163. package/src/graph-workspace/disclosure.mjs +39 -0
  164. package/src/graph-workspace/geometry.mjs +78 -0
  165. package/src/graph-workspace/index.mjs +11 -0
  166. package/src/graph-workspace/labels.mjs +50 -0
  167. package/src/graph-workspace/model.mjs +109 -0
  168. package/src/graph-workspace/recipes.mjs +88 -0
  169. package/src/graph-workspace/scale.mjs +95 -0
  170. package/src/graph-workspace/theme.mjs +156 -0
  171. package/src/ingestion/optional/graphify/graphify_supplier.py +5 -0
  172. package/src/ingestion/optional/graphify/native-closure-pins.json +275 -0
  173. package/src/ingestion/optional/graphify/native-pins.json +306 -0
  174. package/src/knowledge/ontology-guide.mjs +152 -0
  175. package/src/knowledge-health/proof/installed-check.mjs +2 -2
  176. package/src/knowledge-health/rule-identity.mjs +14 -9
  177. package/src/knowledge-health/workshop.mjs +22 -2
  178. package/src/personal-workspace/index.mjs +1 -1
  179. package/src/personal-workspace/selection.mjs +34 -3
  180. package/src/project/config.mjs +1 -1
  181. package/src/project/path-match.mjs +2 -1
  182. package/src/project/providers/github.mjs +24 -0
  183. package/src/project/repo-identity.mjs +3 -22
  184. package/src/projection/obsidian/contracts.mjs +1 -1
  185. package/src/projection/obsidian/materialize/path-registry.mjs +1 -1
  186. package/src/projection/obsidian/materialize/prepare-view.mjs +88 -6
  187. package/src/projection/obsidian/materialize/redaction.mjs +1 -1
  188. package/src/projection/obsidian/personal-workspace.mjs +84 -5
  189. package/src/projection/obsidian/plugin-bridge/channel.mjs +1 -1
  190. package/src/projection/obsidian/proposals/adapter.mjs +46 -23
  191. package/src/projection/obsidian/proposals/observation.mjs +40 -2
  192. package/src/projection/obsidian/publication/publisher.mjs +96 -8
  193. package/src/projection/project.mjs +21 -16
  194. package/src/readiness/readiness.mjs +20 -11
  195. package/src/readiness-protocols/bundled-pack.mjs +14 -766
  196. package/src/readiness-protocols/bundled-selection.mjs +32 -0
  197. package/src/readiness-protocols/evidence.mjs +2 -1
  198. package/src/readiness-protocols/packs/mnstry.mjs +766 -0
  199. package/src/readiness-protocols/runtime.mjs +43 -6
  200. package/src/runtime/obsidian/engine.mjs +387 -76
  201. package/src/runtime/obsidian/lifecycle.mjs +20 -15
  202. package/src/runtime/obsidian/login-item.mjs +6 -3
  203. package/src/runtime/obsidian/machine-settings.mjs +52 -8
  204. package/src/runtime/obsidian/observation.mjs +36 -4
  205. package/src/runtime/obsidian/opening.mjs +4 -3
  206. package/src/runtime/obsidian/personal-selection.mjs +187 -0
  207. package/src/runtime/obsidian/pipeline.mjs +115 -6
  208. package/src/runtime/obsidian/private-lock.mjs +50 -6
  209. package/src/runtime/obsidian/selection-continuity.mjs +124 -0
  210. package/src/runtime/obsidian/service-main.mjs +68 -16
  211. package/src/runtime/obsidian/service.mjs +80 -6
  212. package/src/runtime/obsidian/unheld-evidence.mjs +1 -1
  213. package/src/runtime/obsidian/view-permission.mjs +335 -0
  214. package/src/ui/control-system.mjs +201 -0
  215. package/src/ui/html-primitives.mjs +3 -0
  216. package/src/upgrade/closure-diagnostics.mjs +135 -0
  217. package/src/upgrade/closure.mjs +649 -0
  218. package/src/upgrade/upgrade.mjs +42 -11
  219. package/templates/distribution-workspace/README.md +2 -2
  220. package/templates/external-project-workspace/atelier.project.json +3 -0
  221. package/templates/external-project-workspace/pack-lifecycle.json +3 -1
  222. package/templates/knowledge-workspace/README.md +2 -2
  223. package/docs/integration-and-review-plan.md +0 -409
  224. package/docs/integration-contract-decisions.md +0 -44
  225. package/docs/knowledge-health/devday/facilitator.md +0 -16
  226. package/docs/knowledge-health/devday/public-default.md +0 -107
  227. package/docs/knowledge-health/devday/quickstart.md +0 -18
  228. package/docs/knowledge-health/devday/support.md +0 -22
  229. package/docs/knowledge-health/exercise.md +0 -17
  230. package/docs/knowledge-health/support.md +0 -34
  231. package/docs/presentation-review-disposition.md +0 -47
  232. package/docs/progressive-ingestion-acceptance.md +0 -89
  233. package/docs/progressive-ingestion.md +0 -235
  234. /package/docs/{exact-upgrades.md → guides/exact-upgrades.md} +0 -0
  235. /package/docs/{guided-upgrades.md → guides/guided-upgrades.md} +0 -0
  236. /package/docs/{local-review.md → guides/local-review.md} +0 -0
  237. /package/docs/{skill-steward.md → guides/skill-steward.md} +0 -0
  238. /package/docs/{template-upgrades.md → guides/template-upgrades.md} +0 -0
  239. /package/docs/{assurance-controls.md → maintainers/assurance-controls.md} +0 -0
  240. /package/docs/{attestation.md → reference/attestation.md} +0 -0
  241. /package/docs/{coauthor-session.md → reference/coauthor-session.md} +0 -0
  242. /package/docs/{continuity.md → reference/continuity.md} +0 -0
  243. /package/docs/{contract-stability.md → reference/contract-stability.md} +0 -0
  244. /package/docs/{decision-practices.md → reference/decision-practices.md} +0 -0
  245. /package/docs/{evidence-navigation.md → reference/evidence-navigation.md} +0 -0
  246. /package/docs/{extension-pack-lifecycle.md → reference/extension-pack-lifecycle.md} +0 -0
  247. /package/docs/{interaction-and-reflection.md → reference/interaction-and-reflection.md} +0 -0
  248. /package/docs/{local-state.md → reference/local-state.md} +0 -0
  249. /package/docs/{ontology.md → reference/ontology.md} +0 -0
  250. /package/docs/{optional-ingestion.md → reference/optional-ingestion.md} +0 -0
  251. /package/docs/{project-command-inventory.md → reference/project-command-inventory.md} +0 -0
  252. /package/docs/{semantic-ingestion.md → reference/semantic-ingestion.md} +0 -0
  253. /package/docs/{vault-service.md → reference/vault-service.md} +0 -0
@@ -0,0 +1,152 @@
1
+ import { createHash } from 'node:crypto'
2
+ import { isDeepStrictEqual } from 'node:util'
3
+ import { validateKnowledgePlan as validatePlan } from './plan.mjs'
4
+ import { knowledgeQuestionContext as contextForQuestion } from './workspace.mjs'
5
+
6
+ const sha256 = bytes => createHash('sha256').update(bytes).digest('hex')
7
+ const filled = value => typeof value === 'string' && value.trim().length > 0
8
+ const clone = value => structuredClone(value)
9
+ const object = value => value !== null && typeof value === 'object' && !Array.isArray(value)
10
+
11
+ // A draft adapter for the existing local workspace. It owns no source store,
12
+ // accepted concept identity, access decision, semantic verdict or apply path.
13
+ export function createOntologyGuide() {
14
+ function bind(workspace, questionId) {
15
+ const errors = validatePlan(workspace.plan)
16
+ if (errors.length) throw new Error(`invalid current plan: ${errors.join('; ')}`)
17
+ const question = workspace.plan.questions.find(q => q.id === questionId)
18
+ if (!question) throw new Error('unknown knowledge question')
19
+ const context = contextForQuestion(workspace, questionId)
20
+ if (context.schema !== 'atelier-knowledge-context@v1' || context.planSha256 !== workspace.sha256)
21
+ throw new Error('context is not bound to the current plan')
22
+ return { question, context }
23
+ }
24
+
25
+ function prompts(workspace, questionId, entry = 'corpus') {
26
+ const { question } = bind(workspace, questionId)
27
+ if (!['corpus', 'blank'].includes(entry)) throw new Error('unknown entry route')
28
+ return [
29
+ { id: 'decision', view: 'onboard', field: 'concept', question: 'What decision will this help?', hint: question.work },
30
+ { id: 'concept', view: 'model', field: 'concept', question: 'What is the thing you are naming?', hint: 'Choose an existing concept by identity; propose a definition only where it leaves a concrete gap.' },
31
+ { id: 'identity', view: 'model', field: 'identity', question: 'How would you recognize the same thing next week?', hint: 'Give a stable identifying observation or key, and an example. A similar name or alias alone does not establish identity.' },
32
+ { id: 'counterexample', view: 'model', field: 'identity', question: 'What close example should stay separate?', hint: 'Explain the difference and name any existing concept with which it could be confused.' },
33
+ { id: 'evidence', view: 'deepen', field: 'test', question: 'Which source supports it?', hint: entry === 'blank' ? 'Write a permitted observation or interview note, or keep the answer unknown. A synthetic practice note stays labelled synthetic.' : 'Choose a permitted source already read by the current context; inspect the exact passage and qualifications.' },
34
+ { id: 'relation', view: 'model', field: 'relation', question: 'What does this relationship mean in each direction?', hint: 'Describe the forward claim and how to read it from the other endpoint. The reverse reading creates no inverse assertion.' },
35
+ { id: 'change', view: 'apply', field: 'test', question: 'What would change your mind?', hint: 'Give a disconfirming observation, unresolved evidence and next test; return to Learn after the outcome is observed.' },
36
+ ]
37
+ }
38
+
39
+ function inspect(workspace, questionId, draft) {
40
+ const { question, context } = bind(workspace, questionId)
41
+ const diagnostics = []
42
+ const add = (code, field, next) => diagnostics.push({ code, field, next })
43
+ if (!object(draft)) return { status: 'needs-guidance', diagnostics: [{ code: 'draft-required', field: 'draft', next: 'Start a structured draft from the current question and workspace.' }], evidence: [], contextStatus: context.status }
44
+ if (draft.snapshot !== workspace.snapshot || draft.basePlanSha256 !== workspace.sha256)
45
+ add('changed-source', 'snapshot', 'Refresh the workspace, inspect changed sources and rebind a new draft; retain this one as historical.')
46
+ if (!['corpus', 'blank'].includes(draft.entry)) add('entry-required', 'entry', 'Choose an existing corpus or a blank workspace.')
47
+ if (!filled(draft.decision)) add('decision-required', 'decision', 'Name the decision this question could change.')
48
+ const proposedPlan = draft.proposedPlan ?? workspace.plan
49
+ const planErrors = validatePlan(proposedPlan)
50
+ for (const error of planErrors) add('invalid-proposed-plan', 'proposedPlan', error)
51
+ if (planErrors.length) return { status: 'needs-guidance', diagnostics, evidence: [], contextStatus: context.status }
52
+ const proposedQuestion = proposedPlan.questions.find(q => q.id === question.id)
53
+ if (!proposedQuestion) add('question-removed', 'proposedPlan', 'Retain the current question or propose its retirement separately with the source owner.')
54
+ const concepts = proposedPlan.concepts
55
+ const selected = draft.concepts ?? []
56
+ if (!Array.isArray(selected) || !selected.length) add('concept-required', 'concepts', 'Choose the smallest existing concept set needed by this question.')
57
+ const seen = new Set()
58
+ for (const choice of Array.isArray(selected) ? selected : []) {
59
+ if (!object(choice)) { add('invalid-concept-choice', 'concepts', 'Choose a concept by its exact plan identity.'); continue }
60
+ const concept = concepts.find(c => c.id === choice.id)
61
+ if (!concept) { add('unknown-concept', 'concepts', 'Select an exact concept ID; a name or alias is not an identity mapping.'); continue }
62
+ if (seen.has(choice.id)) add('duplicate-concept', 'concepts', 'Keep one selection per concept identity.')
63
+ seen.add(choice.id)
64
+ for (const field of ['example', 'counterexample', 'identityBasis']) {
65
+ if (!filled(choice[field])) add(`${field}-required`, `concepts.${choice.id}`, `Give a concrete ${field} for ${choice.id}; review its definition and identity rule.`)
66
+ }
67
+ if (filled(choice.example) && choice.example.trim() === choice.counterexample?.trim())
68
+ add('indistinguishable-example', `concepts.${choice.id}`, 'Use a near miss that the stated identity rule actually keeps separate.')
69
+ const collisions = concepts.filter(c => c.id !== concept.id && (c.definition.trim().toLowerCase() === concept.definition.trim().toLowerCase() || c.identityRule.trim().toLowerCase() === concept.identityRule.trim().toLowerCase()))
70
+ for (const other of collisions) {
71
+ if (!Array.isArray(choice.distinctFrom) || !choice.distinctFrom.some(x => object(x) && x.id === other.id && filled(x.reason)))
72
+ add('ambiguous-identity', `concepts.${choice.id}`, `Explain how ${choice.id} differs from ${other.id}, or send an explicit mapping proposal for source-owner review. Do not merge them by name.`)
73
+ }
74
+ if (!proposedQuestion?.concepts.includes(choice.id)) add('orphan-concept', `concepts.${choice.id}`, 'Link the concept to the current question or remove it from this draft.')
75
+ }
76
+ for (const id of proposedQuestion?.concepts ?? []) {
77
+ if (!seen.has(id)) add('untested-concept', 'concepts', `Inspect the definition, identity and near miss for ${id}.`)
78
+ }
79
+ for (const id of proposedQuestion?.relations ?? []) {
80
+ const relation = proposedPlan.relations.find(r => r.id === id)
81
+ const reading = Array.isArray(draft.relations) ? draft.relations.find(r => object(r) && r.id === id) : null
82
+ if (!reading || !filled(reading.forward) || !filled(reading.reverse))
83
+ add('directed-meaning-required', 'relations', `Explain ${relation.from} → ${relation.to} (${relation.predicate}) and its reverse reading; neither wording proves an accepted edge.`)
84
+ }
85
+ const evidence = []
86
+ if (!Array.isArray(draft.evidence)) add('evidence-list-required', 'evidence', 'Select exact passages, or retain an empty list for an honest unknown.')
87
+ for (const pin of Array.isArray(draft.evidence) ? draft.evidence : []) {
88
+ if (!object(pin)) { add('invalid-evidence-pin', 'evidence', 'Choose a current source digest and exact passage.'); continue }
89
+ const matches = context.sources.filter(s => s.id === pin.id)
90
+ if (matches.length !== 1) { add('unavailable-or-ambiguous-source', 'evidence', 'Select one unambiguous source from the current permitted context; inspect omissions separately.'); continue }
91
+ const source = matches[0]
92
+ if (pin.sha256 !== source.sha256) { add('stale-evidence', 'evidence', 'Read the changed source and propose new pins explicitly; keep the old pin in history.'); continue }
93
+ const lines = source.text.split('\n')
94
+ if (source.text.endsWith('\n')) lines.pop()
95
+ if (!Number.isSafeInteger(pin.start) || !Number.isSafeInteger(pin.end) || pin.start < 1 || pin.end < pin.start || pin.end > lines.length) {
96
+ add('invalid-passage', 'evidence', 'Choose a complete existing line range in the selected source.'); continue
97
+ }
98
+ const passage = lines.slice(pin.start - 1, pin.end).join('\n')
99
+ if (!filled(pin.quote) || pin.quote !== passage) { add('changed-passage', 'evidence', 'Copy the exact current passage, including its qualifications.'); continue }
100
+ evidence.push({ id: source.id, repo: source.repo, path: source.path, sha256: source.sha256, start: pin.start, end: pin.end, quote: passage })
101
+ }
102
+ if (!['supported', 'abstain'].includes(draft.answer?.status) || !filled(draft.answer?.text))
103
+ add('answer-required', 'answer', 'Write a source-supported proposed answer or an explicit abstention.')
104
+ if (draft.answer?.status === 'supported' && !evidence.length)
105
+ add('unsupported-answer', 'answer', 'Narrow the claim to an inspected source, or abstain and name the evidence needed.')
106
+ if (draft.answer?.certainty !== 'qualified' || !filled(draft.answer?.limits))
107
+ add('unsupported-certainty', 'answer', 'State scope, uncertainty, contradictions and alternative explanations. A retrieved passage alone does not establish certainty.')
108
+ if (!filled(draft.wouldChangeMind)) add('disconfirmation-required', 'wouldChangeMind', 'Name an observation that could disconfirm the answer, or the next evidence needed to resolve the unknown.')
109
+ return {
110
+ status: diagnostics.length ? 'needs-guidance' : 'ready-for-human-review', diagnostics, evidence,
111
+ contextStatus: context.status, coverage: clone(context.coverage), omissions: clone(context.omissions),
112
+ abstention: draft.answer?.status === 'abstain' ? context.coverage.omitted ? 'unverified-omissions' : 'proposed' : null,
113
+ assurance: 'Draft completeness and source binding only. The reviewer judges support, identity, meaning and the decision.',
114
+ }
115
+ }
116
+
117
+ function prepareReview(workspace, questionId, draft, { basePlanText, planRef = 'knowledge-plan.json' }) {
118
+ const result = inspect(workspace, questionId, draft)
119
+ if (result.diagnostics.length) return { ok: false, ...result }
120
+ if (sha256(Buffer.from(basePlanText, 'utf8')) !== workspace.sha256 || !isDeepStrictEqual(JSON.parse(basePlanText.replace(/^\uFEFF/, '')), workspace.plan))
121
+ throw new Error('raw plan bytes differ from the bound source')
122
+ if (!filled(planRef) || planRef.length > 500 || planRef.includes('\\') || planRef.split('/').some(p => !p || p.startsWith('.')) || planRef.startsWith('/'))
123
+ throw new Error('visible workspace-relative plan reference required')
124
+ const next = draft.proposedPlan ?? workspace.plan
125
+ const proposedText = isDeepStrictEqual(next, workspace.plan) ? basePlanText : `${JSON.stringify(next, null, 2)}\n`
126
+ const changes = Object.keys(next).filter(key => !isDeepStrictEqual(workspace.plan[key], next[key])).sort()
127
+ .map(key => ({ path: `/${key}`, before: clone(workspace.plan[key]), after: clone(next[key]) }))
128
+ const payload = {
129
+ kind: 'ontology-guidance-draft', basePlanSha256: workspace.sha256, snapshot: workspace.snapshot, questionId,
130
+ sourceEditsApplied: false, canonicalAdmission: false, reviewRequired: true,
131
+ beforeText: basePlanText, proposedText, changes, evidence: result.evidence,
132
+ retrieval: { status: result.contextStatus, coverage: result.coverage, omissions: result.omissions, abstention: result.abstention },
133
+ guidance: { entry: draft.entry, decision: draft.decision, concepts: draft.concepts, relations: draft.relations ?? [], answer: draft.answer, wouldChangeMind: draft.wouldChangeMind },
134
+ coauthor: { flow: 'model', fields: {
135
+ concept: JSON.stringify({ decision: draft.decision, concepts: draft.concepts }),
136
+ identity: JSON.stringify(draft.concepts.map(({ id, identityBasis, example, counterexample, distinctFrom }) => ({ id, identityBasis, example, counterexample, distinctFrom: distinctFrom ?? [] }))),
137
+ relation: JSON.stringify(draft.relations ?? []),
138
+ test: JSON.stringify({ evidence: result.evidence, answer: draft.answer, wouldChangeMind: draft.wouldChangeMind }),
139
+ } },
140
+ }
141
+ // Existing copy-only store bounds each JSON/text payload at 50,000 chars.
142
+ // Refuse rather than silently truncate the reviewable source or draft.
143
+ const diff = JSON.stringify(changes, null, 2)
144
+ if (JSON.stringify(payload).length > 49000 || diff.length > 49000) throw new Error('review draft exceeds the copy-only store bound; narrow the change')
145
+ return { ok: true, status: 'proposed', sourceEditsApplied: false,
146
+ body: { path: planRef, action: 'copy.repoPath', intent: 'Review a source-bound knowledge-plan proposal',
147
+ capability: 'proposal.copy-only', directWrite: false, applyEndpoint: null,
148
+ diff, proposal: payload },
149
+ }
150
+ }
151
+ return { prompts, inspect, prepareReview }
152
+ }
@@ -33,7 +33,7 @@ await scenario('public-quickstart',async()=>{
33
33
  return {init,graph:shellBin(['graph'],workspace),project:shellBin(['project'],workspace),scope:'Exact macOS public quickstart command sequence, supported baseline only'};
34
34
  });
35
35
  await scenario('installed-document-references',async()=>{
36
- const references=['README.md','docs/knowledge-graph.md','docs/coauthor-session.md'];
36
+ const references=['README.md','docs/reference/knowledge-graph.md','docs/reference/coauthor-session.md'];
37
37
  const documents=references.map(relativePath=>{const file=fs.realpathSync(path.join(packageRoot,relativePath));assert.ok(file.startsWith(fs.realpathSync(packageRoot)+path.sep));const bytes=fs.readFileSync(file);return {relativePath,bytes:bytes.length,sha256:digest(bytes)};});
38
38
  for(const subpath of ['./graph','./project','./coauthor']){assert.ok(pkg.exports[subpath]);await import(pkg.name+'/'+subpath.slice(2));}
39
39
  return {documents,apiReferences:['@mnstry/atelier/graph','@mnstry/atelier/project','@mnstry/atelier/coauthor'],scope:'Public quickstart references resolve in the actual installed package'};
@@ -72,7 +72,7 @@ await scenario('installed-public-graph-project-baseline',async()=>{
72
72
  assert.ok(Array.isArray(graph.nodes));assert.ok(Array.isArray(graph.edges));assert.ok(Array.isArray(graph.errors));assert.equal(graph.errors.length,0);
73
73
  return {init,graphCommand,projectCommand,graph,agentCIOutput:'Actual machine-readable canonical graph; structural evidence only, no model/provider or semantic acceptance.'};
74
74
  });
75
- const docs=[];for(const candidate of ['README.md','docs/knowledge.md','docs/coauthor.md','docs/coauthor-session.md','docs/upgrade.md','docs/graph.md','docs/knowledge-graph.md','docs/release-engineering.md']){const full=path.join(packageRoot,candidate);if(fs.existsSync(full)){const b=fs.readFileSync(full);docs.push({path:candidate,sha256:digest(b),bytes:b.length});}}
75
+ const docs=[];for(const candidate of ['README.md','docs/guides/knowledge-workspace.md','docs/reference/coauthor-session.md','docs/guides/upgrade.md','docs/reference/knowledge-graph.md','docs/maintainers/release-engineering.md']){const full=path.join(packageRoot,candidate);if(fs.existsSync(full)){const b=fs.readFileSync(full);docs.push({path:candidate,sha256:digest(b),bytes:b.length});}}
76
76
  for(const id of ['assessment-to-guided-authoring','full-native-finding-reference','preview-owning-source-correction-readback-reassessment','keyboard-focus-undo','native-withdrawal-recovery','participant-usefulness'])gaps.push({id,owner:id==='participant-usefulness'?'Facilitator/participant':'Foundation and existing AW Integration/Journey',action:'Receive exact public Dev Day installed entry/bindings and native allocation; inherited native or fixture proof cannot satisfy installed or human evidence.'});
77
77
  const bindings=profile.deliveryBindings;
78
78
  if(bindings){
@@ -1,7 +1,7 @@
1
1
  export const ruleIdentity = Object.freeze({
2
2
  "profile": "source-qualified-required-direction/r1",
3
- "commit": "fcfdbe5ded672d41c717f49e0cfdf68b6060834d",
4
- "tree": "400e181cd0cb55976f3ef7ab1a1405531dacd521",
3
+ "commit": "43a6d6a9d4718e735576bdd878cbea1f9b7e454a",
4
+ "tree": "306d92f8f3a36fa8938419bf8df32cb3b30f6704",
5
5
  "files": [
6
6
  {
7
7
  "path": "src/knowledge/plan.mjs",
@@ -17,15 +17,15 @@ export const ruleIdentity = Object.freeze({
17
17
  },
18
18
  {
19
19
  "path": "src/graph/graph.mjs",
20
- "bytes": 9934,
21
- "sha256": "1f01fbeab7e6fcaed127c8fe84c9a48b08f1fec13d21f486c48726715a5bbb92",
22
- "blob": "6bce4e1213098457097e025f8ead53e6b4403336"
20
+ "bytes": 10397,
21
+ "sha256": "0200feb42d59aa8e0f2e9b0e7dd49bbd68f25bc09710aa1ecce92bc82596adb5",
22
+ "blob": "96f6f05f8df1b3cf9c75d050359d11e9bad39aa2"
23
23
  },
24
24
  {
25
25
  "path": "src/graph/knowledge-graph.mjs",
26
- "bytes": 65477,
27
- "sha256": "94895598d388e80a70b7bbe92ad57138dccd94896699a6c472e635d885b1139f",
28
- "blob": "283ef86ad08a6360bc006cc75ca5cedb2718e14d"
26
+ "bytes": 66377,
27
+ "sha256": "ee62867895c7a8ab16295365120c2b4a69754066bf2258fc9156708d9b61bc96",
28
+ "blob": "fc6c543d08bef5de449709674122994cb1c51b75"
29
29
  },
30
30
  {
31
31
  "path": "templates/knowledge-workspace/knowledge-plan.schema.json",
@@ -39,6 +39,11 @@ export const ruleIdentity = Object.freeze({
39
39
  "supersededQualification": {
40
40
  "commit": "b1bdc9279a99f7c884ddfdfe0e2a24315d6f3666",
41
41
  "tree": "2797f44baf89007d3808675d897e55a943fc0d65",
42
- "note": "The supplier qualified against this earlier commit. The five pinned files are byte-identical there and at the source authority above."
42
+ "note": "The supplier qualified against this earlier commit. The pinned plan, context and schema files are byte-identical there and at the source authority above; src/graph/graph.mjs and src/graph/knowledge-graph.mjs are not, and that qualification does not cover their current bytes."
43
+ },
44
+ "previousSourceAuthority": {
45
+ "commit": "0a77cdc0f4f949d9aef29e3567303e2a6588a610",
46
+ "tree": "4aaadc77690868921c1748e15079e0965e06faae",
47
+ "note": "Regenerated by the source owner for a changed candidate: workspace link resolution in src/graph/knowledge-graph.mjs now recognises Markdown sources by the existing isMarkdownPath test, so links from uppercase .MD files resolve. One line changes; the file keeps its size. The other pinned files are byte-identical; the Knowledge Health tests pass on the current bytes."
43
48
  }
44
49
  })
@@ -28,12 +28,32 @@ function read(file) {
28
28
  } finally { fs.closeSync(fd) }
29
29
  }
30
30
 
31
+ // Every directory is created by this copy and every file is created exclusively, so an entry that arrived meanwhile
32
+ // refuses instead of being merged. Nothing is overwritten, and nothing unknown is cleaned up on failure.
33
+ function copyExclusive(source, target) {
34
+ const stat = fs.lstatSync(source)
35
+ if (stat.isDirectory()) {
36
+ fs.mkdirSync(target, { recursive: false })
37
+ for (const name of fs.readdirSync(source)) copyExclusive(path.join(source, name), path.join(target, name))
38
+ } else if (stat.isFile()) fs.copyFileSync(source, target, fs.constants.COPYFILE_EXCL)
39
+ else throw Error('The workshop fixture holds an entry that is neither a file nor a directory')
40
+ }
41
+
42
+ // Entries created inside these new directories after they were made are not ours either; refuse before Git sees them.
43
+ function assertExactCopy(source, target) {
44
+ const expected = fs.readdirSync(source).sort(), actual = fs.readdirSync(target).sort()
45
+ if (expected.length !== actual.length || expected.some((name, index) => name !== actual[index]))
46
+ throw Object.assign(Error('The new workshop holds entries this initialization did not create'), { code: 'EEXIST' })
47
+ for (const name of expected) if (fs.lstatSync(path.join(source, name)).isDirectory()) assertExactCopy(path.join(source, name), path.join(target, name))
48
+ }
49
+
31
50
  /** Copy the actual shipped public fixture into a NEW disposable workspace. */
32
51
  export function initializeKnowledgeHealthWorkshop(destination) {
33
52
  const root = path.resolve(destination)
34
53
  fs.mkdirSync(root, { recursive: false })
35
- fs.cpSync(fileURLToPath(new URL('../../fixtures/knowledge-health/workshop/', import.meta.url)), root,
36
- { recursive: true, force: false, errorOnExist: true })
54
+ const fixture = fileURLToPath(new URL('../../fixtures/knowledge-health/workshop/', import.meta.url))
55
+ for (const name of fs.readdirSync(fixture)) copyExclusive(path.join(fixture, name), path.join(root, name))
56
+ assertExactCopy(fixture, root)
37
57
  fs.writeFileSync(path.join(root, '.gitignore'), '.atelier-local/\noutput/\n', { flag: 'wx' })
38
58
  const git = spawnSync('git', ['init', '--quiet'], { cwd: root, encoding: 'utf8', timeout: 15000,
39
59
  env: { PATH: process.env.PATH, GIT_CONFIG_NOSYSTEM: '1',
@@ -26,7 +26,7 @@ const resolvedInputs = new WeakSet()
26
26
  const plans = new WeakSet()
27
27
  const freeze = (v) => { if (v && typeof v === 'object') { Object.values(v).forEach(freeze); Object.freeze(v) } return v }
28
28
 
29
- export { selectPersonalGeneration, selectionConfirmDigest, readPersonalSelection, inventoryPersonalHome, planPersonalRestore, restorePersonalInputs } from './selection.mjs'
29
+ export { selectPersonalGeneration, selectionConfirmDigest, readPersonalSelection, readPersonalSelectionHead, inventoryPersonalHome, planPersonalRestore, restorePersonalInputs } from './selection.mjs'
30
30
  export class PersonalWorkspaceRefusal extends Error {
31
31
  constructor(code) { super(`Personal workspace refused: ${code}`); this.name = 'PersonalWorkspaceRefusal'; this.code = code }
32
32
  }
@@ -19,6 +19,7 @@ const MAX_RECORD = 1024 * 1024
19
19
  const MAX_GENERATION_FILE = 8 * 1024 * 1024
20
20
  const MAX_RESTORE_RECORD = 4 * MAX_RECORD
21
21
  const ID = /^[0-9a-f]{64}$/
22
+ const CONFIRMATION_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/
22
23
  // Every temporary name this module writes, including restore aside copies
23
24
  // (.restore-<uuid>-aside-<key>.json).
24
25
  const TEMPORARY = /^\.(selection|restore|validate)-[0-9a-f-]{36}(-[a-z]+)*\.json$/
@@ -45,8 +46,10 @@ function privateHome(personalHome) {
45
46
  if (stat.uid !== process.getuid() || (stat.mode & 0o022) !== 0) refuse('not-private-location')
46
47
  return personalHome
47
48
  }
49
+ // Opened without waiting on it: a FIFO or a device in a record's place refuses instead of blocking the caller, which
50
+ // may be a service's event loop.
48
51
  function readBytes(file, limit, tooLarge = 'malformed-input') {
49
- const fd = fs.openSync(file, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW)
52
+ const fd = fs.openSync(file, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW | (fs.constants.O_NONBLOCK ?? 0))
50
53
  try {
51
54
  const stat = fs.fstatSync(fd)
52
55
  if (!stat.isFile()) refuse('malformed-input')
@@ -134,7 +137,7 @@ function readHistory(personalHome) {
134
137
  let record
135
138
  try { record = JSON.parse(bytes) } catch { refuse('selection-history-corrupt') }
136
139
  if (record?.schema !== SELECTION_SCHEMA || record.sequence !== index + 1 || record.previous !== head || !ID.test(record.generationId ?? '') ||
137
- canonical(record) !== bytes.toString('utf8')) refuse('selection-history-corrupt')
140
+ (record.confirmationId !== undefined && !CONFIRMATION_ID.test(record.confirmationId)) || canonical(record) !== bytes.toString('utf8')) refuse('selection-history-corrupt')
138
141
  records.push(record)
139
142
  head = hash(bytes)
140
143
  files.push({ name, bytes: bytes.length, digest: head })
@@ -161,7 +164,7 @@ export function selectPersonalGeneration(options) {
161
164
  const { eligible, reason } = eligibility(personalHome, generationId)
162
165
  if (!eligible) refuse(reason)
163
166
  const dir = privateDir(personalHome, 'selections', true)
164
- const record = { schema: SELECTION_SCHEMA, sequence: records.length + 1, previous: head, generationId }
167
+ const record = { schema: SELECTION_SCHEMA, sequence: records.length + 1, previous: head, generationId, confirmationId: randomUUID() }
165
168
  const content = canonical(record)
166
169
  try { publishExclusive(dir, `${String(record.sequence).padStart(6, '0')}.json`, content, 'selection') }
167
170
  catch (error) { if (error.code === 'EEXIST') refuse('selection-concurrent'); throw error }
@@ -182,6 +185,34 @@ export function readPersonalSelection(options) {
182
185
  }, 'personal-home-unavailable')
183
186
  }
184
187
 
188
+ // The current selection from the history alone. Nothing is composed, so nothing
189
+ // here says whether the selected generation is still eligible: a host that
190
+ // composes elsewhere reads this first, and reads it again to compare the head it
191
+ // acted on with the head now. `sequence` is 0 while nothing is selected.
192
+ export function readPersonalSelectionHead(options) {
193
+ return safe(() => {
194
+ const { personalHome } = options ?? {}
195
+ privateHome(personalHome)
196
+ const { records, head } = readHistory(personalHome)
197
+ const current = records.at(-1) ?? null
198
+ return freeze({ selected: current?.generationId ?? null, sequence: current?.sequence ?? 0, head })
199
+ }, 'personal-home-unavailable')
200
+ }
201
+
202
+ // Package-internal snapshot for a host that keeps continuity across starts. The public head reader keeps its exact
203
+ // result. These identities come from the same verified history read, including every previously confirmed prefix.
204
+ export function readPersonalSelectionSnapshot(options) {
205
+ return safe(() => {
206
+ const { personalHome } = options ?? {}
207
+ privateHome(personalHome)
208
+ const { records, files, head } = readHistory(personalHome)
209
+ const current = records.at(-1) ?? null
210
+ return freeze({ selected: current?.generationId ?? null, sequence: current?.sequence ?? 0, head,
211
+ confirmationId: current?.confirmationId ?? null, recordDigests: files.map((file) => file.digest),
212
+ confirmationIds: records.flatMap((record) => record.confirmationId === undefined ? [] : [record.confirmationId]) })
213
+ }, 'personal-home-unavailable')
214
+ }
215
+
185
216
  // Lists entries without failing the whole inventory: a concurrently removed entry
186
217
  // is skipped; an unreadable, foreign or oversized one is reported without a digest.
187
218
  function listFiles(dir, keep) {
@@ -557,7 +557,7 @@ export function validateProjectConfigDoc(doc, { neutralTemplate = false } = {})
557
557
  errors.push('setup.profile is invalid')
558
558
  }
559
559
  // The census reads these as path patterns relative to the project config's
560
- // folder (docs/knowledge-graph.md), under the contract's pathString rules.
560
+ // folder (docs/reference/knowledge-graph.md), under the contract's pathString rules.
561
561
  for (const key of ['include', 'exclude']) {
562
562
  errors.push(...stringPathErrors(setup[key], `setup.${key}`))
563
563
  if (typeof setup[key] === 'string' && (setup[key].includes('\u0000') || /^[a-z][a-z0-9+.-]*:/.test(setup[key]) || path.isAbsolute(setup[key]))) {
@@ -47,7 +47,8 @@ export function matchesPathPattern(pattern, rel) {
47
47
  const normalizedRel = normalizeRelPath(rel)
48
48
  if (!normalizedPattern || !normalizedRel) return false
49
49
  const candidate = normalizedPattern.includes('/') ? normalizedRel : path.posix.basename(normalizedRel)
50
- return new RegExp(`^${globSource(normalizedPattern)}$`, 'i').test(candidate)
50
+ // dotAll: `**` crosses a newline in a name as well as a slash.
51
+ return new RegExp(`^${globSource(normalizedPattern)}$`, 'is').test(candidate)
51
52
  }
52
53
 
53
54
  export const basenameOf = (pattern) => path.posix.basename(normalizeRelPath(pattern))
@@ -0,0 +1,24 @@
1
+ import { execFileSync } from 'node:child_process'
2
+
3
+ export const PROVIDER_HOSTS = Object.freeze({ 'github.com': 'github' })
4
+
5
+ const lower = (value) => String(value ?? '').trim().toLowerCase()
6
+
7
+ // Ask the provider to resolve the recorded ref. GitHub follows the rename redirect
8
+ // and answers with the stable numeric id plus the current full name, which is the
9
+ // only signal that survives a rename.
10
+ export function githubLookup({ owner, name }) {
11
+ try {
12
+ const out = execFileSync('gh', ['api', `repos/${owner}/${name}`, '--jq', '{id: .id, fullName: .full_name}'], {
13
+ encoding: 'utf8',
14
+ stdio: ['ignore', 'pipe', 'ignore'],
15
+ })
16
+ const parsed = JSON.parse(out)
17
+ if (!parsed?.id) return null
18
+ return { id: String(parsed.id), fullName: lower(parsed.fullName), currentName: lower(String(parsed.fullName).split('/').pop()) }
19
+ } catch {
20
+ return null
21
+ }
22
+ }
23
+
24
+ export const defaultLookups = { github: githubLookup }
@@ -1,9 +1,9 @@
1
- import { execFileSync } from 'node:child_process'
2
1
  import { SUPPORTED_IDENTITY_PROVIDERS, gitRemoteUrl, remoteHost } from './config.mjs'
2
+ import { PROVIDER_HOSTS, defaultLookups } from './providers/github.mjs'
3
3
 
4
- export const SUPPORTED_PROVIDERS = Object.freeze([...SUPPORTED_IDENTITY_PROVIDERS])
4
+ export { githubLookup } from './providers/github.mjs'
5
5
 
6
- const PROVIDER_HOSTS = Object.freeze({ 'github.com': 'github' })
6
+ export const SUPPORTED_PROVIDERS = Object.freeze([...SUPPORTED_IDENTITY_PROVIDERS])
7
7
 
8
8
  const lower = (value) => String(value ?? '').trim().toLowerCase()
9
9
 
@@ -23,25 +23,6 @@ export function parseRemoteRef(url) {
23
23
  return { host, provider: PROVIDER_HOSTS[host] ?? null, owner: lower(parts[0]), name: lower(parts.slice(1).join('/')) }
24
24
  }
25
25
 
26
- // Ask the provider to resolve the recorded ref. GitHub follows the rename redirect
27
- // and answers with the stable numeric id plus the current full name, which is the
28
- // only signal that survives a rename.
29
- export function githubLookup({ owner, name }) {
30
- try {
31
- const out = execFileSync('gh', ['api', `repos/${owner}/${name}`, '--jq', '{id: .id, fullName: .full_name}'], {
32
- encoding: 'utf8',
33
- stdio: ['ignore', 'pipe', 'ignore'],
34
- })
35
- const parsed = JSON.parse(out)
36
- if (!parsed?.id) return null
37
- return { id: String(parsed.id), fullName: lower(parsed.fullName), currentName: lower(String(parsed.fullName).split('/').pop()) }
38
- } catch {
39
- return null
40
- }
41
- }
42
-
43
- const defaultLookups = { github: githubLookup }
44
-
45
26
  /**
46
27
  * Resolve a clone's stable identity.
47
28
  *
@@ -404,7 +404,7 @@ export function cutName(value, maxBytes) {
404
404
 
405
405
  // One file or folder name made safe for macOS, Linux, Windows and Obsidian
406
406
  // links, or '' when nothing is left of it. The author's casing and words are
407
- // kept; see "File names" in docs/obsidian-contract.md for each rule.
407
+ // kept; see "File names" in docs/reference/obsidian-contract.md for each rule.
408
408
  export function vaultName(value, maxBytes = NAME_BYTES) {
409
409
  const cleaned = trimName(String(value ?? '')
410
410
  .toWellFormed()
@@ -7,7 +7,7 @@ import { refuse } from './byte-lens.mjs'
7
7
 
8
8
  export { collisionKey }
9
9
 
10
- // Vault paths of layout 2 (see "Vault layout" in docs/obsidian-contract.md):
10
+ // Vault paths of layout 2 (see "Vault layout" in docs/reference/obsidian-contract.md):
11
11
  //
12
12
  // <repository folder>/<source directory>/<title>.md a Markdown note
13
13
  // <repository folder>/<source directory>/<file name> a wrapped file