@mnstry/atelier 0.2.0-alpha.14 → 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.
- package/CHANGELOG.md +50 -0
- package/README.md +80 -47
- package/SECURITY.md +1 -1
- package/TRADEMARKS.md +2 -2
- package/contracts/atelier-extension-manifest.v1.schema.json +412 -0
- package/contracts/atelier-graph-view.v1.schema.json +1085 -0
- package/contracts/public-api-baseline.json +298 -19
- package/docs/README.md +90 -0
- package/docs/blocks/claims.md +2 -2
- package/docs/blocks/conformance-admission.md +1 -1
- package/docs/{atelier-sync.md → guides/atelier-sync.md} +20 -14
- package/docs/guides/co-authoring.md +113 -0
- package/docs/{distributions.md → guides/distributions.md} +13 -8
- package/docs/guides/extensions.md +189 -0
- package/docs/guides/graph-refresh.md +152 -0
- package/docs/guides/graph-workspace.md +64 -0
- package/docs/{ingestion.md → guides/ingestion.md} +13 -12
- package/docs/{knowledge-workspace.md → guides/knowledge-workspace.md} +2 -2
- package/docs/{learning.md → guides/learning.md} +12 -5
- package/docs/{obsidian.md → guides/obsidian.md} +134 -47
- package/docs/guides/ontology-guide.md +111 -0
- package/docs/{personal-workspace.md → guides/personal-workspace.md} +7 -4
- package/docs/{research-starter.md → guides/research-starter.md} +1 -1
- package/docs/{tenant-readiness.md → guides/tenant-readiness.md} +39 -0
- package/docs/{upgrade.md → guides/upgrade.md} +23 -9
- package/docs/knowledge-health/assessment/capabilities.md +6 -15
- package/docs/knowledge-health/facilitator.md +15 -4
- package/docs/knowledge-health/{devday/participant-response.md → participant-response.md} +4 -2
- package/docs/knowledge-health/quickstart.md +97 -75
- package/docs/knowledge-health/readme.md +13 -5
- package/docs/maintainers/describing-the-atelier.md +173 -0
- package/docs/{release-engineering.md → maintainers/release-engineering.md} +5 -5
- package/docs/{announcements.md → reference/announcements.md} +1 -1
- package/docs/{architecture.md → reference/architecture.md} +31 -5
- package/docs/{atelier-runtime.md → reference/atelier-runtime.md} +1 -1
- package/docs/{connected-composition.md → reference/connected-composition.md} +7 -10
- package/docs/{coordination.md → reference/coordination.md} +1 -1
- package/docs/{decisions.md → reference/decisions.md} +3 -3
- package/docs/{design.md → reference/design.md} +1 -1
- package/docs/reference/edge-policy.md +151 -0
- package/docs/reference/graph-view-structured.md +119 -0
- package/docs/reference/graph-view.md +139 -0
- package/docs/reference/graph-workspace.md +37 -0
- package/docs/{intake-and-guides.md → reference/intake-and-guides.md} +3 -4
- package/docs/{knowledge-graph.md → reference/knowledge-graph.md} +120 -6
- package/docs/{local-services.md → reference/local-services.md} +1 -1
- package/docs/{obsidian-contract.md → reference/obsidian-contract.md} +6 -4
- package/docs/{obsidian-plugin.md → reference/obsidian-plugin.md} +5 -66
- package/docs/{practical-judgment.md → reference/phronesis.md} +12 -3
- package/docs/reference/portability.md +92 -0
- package/docs/{portable-presentation.md → reference/portable-presentation.md} +2 -2
- package/docs/{presentation-consumer-boundaries.md → reference/presentation-consumer-boundaries.md} +5 -0
- package/docs/{project-options.md → reference/project-options.md} +17 -0
- package/docs/{repo-boundary-guard.md → reference/repo-boundary-guard.md} +27 -4
- package/docs/{review-portability.md → reference/review-portability.md} +4 -0
- package/docs/{template-profile.md → reference/template-profile.md} +1 -1
- package/docs/{trackables.md → reference/trackables.md} +2 -2
- package/docs/start/independent-start.md +89 -0
- package/docs/{install.md → start/install.md} +6 -6
- package/docs/{knowledge-setup.md → start/knowledge-setup.md} +5 -5
- package/docs/{research-starter-quickstart.md → start/research-starter-quickstart.md} +6 -6
- package/fixtures/atelier-architecture/catalog/invalid/document.json +2 -2
- package/fixtures/atelier-architecture/catalog/valid/document.json +2 -2
- package/fixtures/graph-view/synthetic-authorized-input.v1.json +437 -0
- package/fixtures/graph-view/synthetic-view.v1.json +1 -0
- package/fixtures/knowledge-health/workshop/README.md +1 -1
- package/fixtures/templates/local-library.v1.json +2 -1
- package/package.json +21 -8
- package/skills/claude/atelier-guided-upgrade/SKILL.md +1 -1
- package/skills/claude/atelier-knowledge-setup/SKILL.md +2 -2
- package/skills/claude/atelier-local-service/SKILL.md +1 -1
- package/skills/claude/atelier-practical-judgment/SKILL.md +7 -2
- package/skills/claude/atelier-skill-steward/SKILL.md +1 -1
- package/skills/claude/mnstry-readiness/SKILL.md +2 -2
- package/skills/codex/atelier-guided-upgrade/SKILL.md +1 -1
- package/skills/codex/atelier-knowledge-setup/SKILL.md +2 -2
- package/skills/codex/atelier-local-service/SKILL.md +1 -1
- package/skills/codex/atelier-practical-judgment/SKILL.md +7 -2
- package/skills/codex/atelier-skill-steward/SKILL.md +1 -1
- package/skills/codex/mnstry-readiness/SKILL.md +2 -2
- package/src/architecture/catalog.mjs +2 -2
- package/src/attestation/sign.mjs +1 -1
- package/src/boundary/content-rules.mjs +5 -4
- package/src/boundary/policy.mjs +60 -12
- package/src/cli/run.mjs +24 -8
- package/src/commands/distribution.mjs +1 -1
- package/src/commands/extension.mjs +27 -0
- package/src/commands/ingest.mjs +1 -1
- package/src/commands/learn.mjs +1 -1
- package/src/commands/obsidian.mjs +28 -16
- package/src/commands/review.mjs +3 -0
- package/src/edge-policy/contract.mjs +69 -0
- package/src/edge-policy/policy.mjs +142 -0
- package/src/edge-policy/source-citation-adapter.mjs +43 -0
- package/src/extension-packs/loader.mjs +4 -3
- package/src/extensions/admission.mjs +83 -0
- package/src/extensions/client.d.mts +23 -0
- package/src/extensions/client.mjs +99 -0
- package/src/extensions/contracts.d.mts +29 -0
- package/src/extensions/contracts.mjs +163 -0
- package/src/extensions/convex.d.mts +24 -0
- package/src/extensions/convex.mjs +189 -0
- package/src/extensions/delegation.mjs +54 -0
- package/src/extensions/drizzle.d.mts +11 -0
- package/src/extensions/drizzle.mjs +33 -0
- package/src/extensions/postgres.d.mts +7 -0
- package/src/extensions/postgres.mjs +66 -0
- package/src/extensions/prisma.d.mts +13 -0
- package/src/extensions/prisma.mjs +63 -0
- package/src/extensions/project.mjs +432 -0
- package/src/extensions/scaffold-convex.mjs +130 -0
- package/src/extensions/scaffold-sql.mjs +144 -0
- package/src/extensions/server.d.mts +20 -0
- package/src/extensions/server.mjs +102 -0
- package/src/extensions/sql-scope.mjs +22 -0
- package/src/graph/knowledge-graph.mjs +1 -1
- package/src/graph/repo-contribution.mjs +405 -0
- package/src/graph/repo-contribution.types.mjs +32 -0
- package/src/graph-access/edge-decision-adapter.mjs +41 -0
- package/src/graph-access/edge-record-adapter.mjs +73 -0
- package/src/graph-access/index.mjs +253 -0
- package/src/graph-access/multi-contribution-record-adapter.mjs +101 -0
- package/src/graph-access/multi-record-projection-adapter.mjs +74 -0
- package/src/graph-access/source-adapter.mjs +81 -0
- package/src/graph-refresh/compare.mjs +110 -0
- package/src/graph-refresh/controller.mjs +324 -0
- package/src/graph-refresh/index.mjs +3 -0
- package/src/graph-refresh/local.mjs +78 -0
- package/src/graph-view/babylon/babylon-scene-port.mjs +64 -0
- package/src/graph-view/babylon/index.mjs +2 -0
- package/src/graph-view/babylon/navigator.mjs +312 -0
- package/src/graph-view/babylon/provenance.mjs +36 -0
- package/src/graph-view/babylon/scene-controller.mjs +131 -0
- package/src/graph-view/babylon/semantic-picking.mjs +35 -0
- package/src/graph-view/babylon/web-host.mjs +188 -0
- package/src/graph-view/cli.mjs +3 -0
- package/src/graph-view/command.mjs +34 -0
- package/src/graph-view/contracts.mjs +79 -0
- package/src/graph-view/errors.mjs +5 -0
- package/src/graph-view/index.mjs +4 -0
- package/src/graph-view/intents.mjs +30 -0
- package/src/graph-view/query.mjs +213 -0
- package/src/graph-view/refs.mjs +2 -0
- package/src/graph-view/structured/browser.mjs +262 -0
- package/src/graph-view/structured/styles.mjs +8 -0
- package/src/graph-workspace/authoring.mjs +185 -0
- package/src/graph-workspace/browser/constellation.mjs +369 -0
- package/src/graph-workspace/browser/dom.mjs +23 -0
- package/src/graph-workspace/browser/index.mjs +6 -0
- package/src/graph-workspace/browser/review.mjs +55 -0
- package/src/graph-workspace/browser/styles.mjs +11 -0
- package/src/graph-workspace/browser/workbench.mjs +351 -0
- package/src/graph-workspace/browser/workspace.mjs +431 -0
- package/src/graph-workspace/disclosure.mjs +39 -0
- package/src/graph-workspace/geometry.mjs +78 -0
- package/src/graph-workspace/index.mjs +11 -0
- package/src/graph-workspace/labels.mjs +50 -0
- package/src/graph-workspace/model.mjs +109 -0
- package/src/graph-workspace/recipes.mjs +88 -0
- package/src/graph-workspace/scale.mjs +95 -0
- package/src/graph-workspace/theme.mjs +156 -0
- package/src/knowledge/ontology-guide.mjs +152 -0
- package/src/knowledge-health/proof/installed-check.mjs +2 -2
- package/src/knowledge-health/rule-identity.mjs +7 -7
- package/src/knowledge-health/workshop.mjs +22 -2
- package/src/personal-workspace/selection.mjs +17 -2
- package/src/project/config.mjs +1 -1
- package/src/project/providers/github.mjs +24 -0
- package/src/project/repo-identity.mjs +3 -22
- package/src/projection/obsidian/contracts.mjs +1 -1
- package/src/projection/obsidian/materialize/path-registry.mjs +1 -1
- package/src/projection/obsidian/materialize/prepare-view.mjs +1 -1
- package/src/projection/obsidian/materialize/redaction.mjs +1 -1
- package/src/projection/obsidian/personal-workspace.mjs +24 -28
- package/src/projection/obsidian/plugin-bridge/channel.mjs +1 -1
- package/src/projection/obsidian/publication/publisher.mjs +25 -4
- package/src/projection/project.mjs +18 -14
- package/src/readiness/readiness.mjs +20 -11
- package/src/readiness-protocols/bundled-pack.mjs +14 -766
- package/src/readiness-protocols/bundled-selection.mjs +32 -0
- package/src/readiness-protocols/evidence.mjs +2 -1
- package/src/readiness-protocols/packs/mnstry.mjs +766 -0
- package/src/readiness-protocols/runtime.mjs +43 -6
- package/src/runtime/obsidian/engine.mjs +99 -26
- package/src/runtime/obsidian/lifecycle.mjs +3 -3
- package/src/runtime/obsidian/login-item.mjs +2 -2
- package/src/runtime/obsidian/machine-settings.mjs +52 -8
- package/src/runtime/obsidian/opening.mjs +2 -2
- package/src/runtime/obsidian/personal-selection.mjs +88 -26
- package/src/runtime/obsidian/pipeline.mjs +75 -7
- package/src/runtime/obsidian/private-lock.mjs +11 -2
- package/src/runtime/obsidian/selection-continuity.mjs +124 -0
- package/src/runtime/obsidian/service-main.mjs +15 -9
- package/src/runtime/obsidian/service.mjs +14 -0
- package/src/runtime/obsidian/unheld-evidence.mjs +1 -1
- package/src/runtime/obsidian/view-permission.mjs +2 -1
- package/src/upgrade/upgrade.mjs +23 -4
- package/templates/distribution-workspace/README.md +2 -2
- package/templates/external-project-workspace/atelier.project.json +3 -0
- package/templates/external-project-workspace/pack-lifecycle.json +2 -1
- package/templates/knowledge-workspace/README.md +2 -2
- package/docs/integration-and-review-plan.md +0 -409
- package/docs/integration-contract-decisions.md +0 -44
- package/docs/knowledge-health/devday/facilitator.md +0 -16
- package/docs/knowledge-health/devday/public-default.md +0 -107
- package/docs/knowledge-health/devday/quickstart.md +0 -18
- package/docs/knowledge-health/devday/support.md +0 -22
- package/docs/knowledge-health/exercise.md +0 -17
- package/docs/knowledge-health/support.md +0 -34
- package/docs/presentation-review-disposition.md +0 -47
- package/docs/progressive-ingestion-acceptance.md +0 -89
- package/docs/progressive-ingestion.md +0 -235
- /package/docs/{capability-stewardship.md → guides/capability-stewardship.md} +0 -0
- /package/docs/{exact-upgrades.md → guides/exact-upgrades.md} +0 -0
- /package/docs/{guided-upgrades.md → guides/guided-upgrades.md} +0 -0
- /package/docs/{inquiry-harnesses.md → guides/inquiry-harnesses.md} +0 -0
- /package/docs/{learning-harnesses.md → guides/learning-harnesses.md} +0 -0
- /package/docs/{local-review.md → guides/local-review.md} +0 -0
- /package/docs/{skill-steward.md → guides/skill-steward.md} +0 -0
- /package/docs/{template-upgrades.md → guides/template-upgrades.md} +0 -0
- /package/docs/{assurance-controls.md → maintainers/assurance-controls.md} +0 -0
- /package/docs/{attestation.md → reference/attestation.md} +0 -0
- /package/docs/{coauthor-session.md → reference/coauthor-session.md} +0 -0
- /package/docs/{continuity.md → reference/continuity.md} +0 -0
- /package/docs/{contract-stability.md → reference/contract-stability.md} +0 -0
- /package/docs/{decision-practices.md → reference/decision-practices.md} +0 -0
- /package/docs/{evidence-navigation.md → reference/evidence-navigation.md} +0 -0
- /package/docs/{extension-pack-lifecycle.md → reference/extension-pack-lifecycle.md} +0 -0
- /package/docs/{interaction-and-reflection.md → reference/interaction-and-reflection.md} +0 -0
- /package/docs/{local-state.md → reference/local-state.md} +0 -0
- /package/docs/{ontology.md → reference/ontology.md} +0 -0
- /package/docs/{optional-ingestion.md → reference/optional-ingestion.md} +0 -0
- /package/docs/{project-command-inventory.md → reference/project-command-inventory.md} +0 -0
- /package/docs/{semantic-ingestion.md → reference/semantic-ingestion.md} +0 -0
- /package/docs/{semantic-operation.md → reference/semantic-operation.md} +0 -0
- /package/docs/{vault-service.md → reference/vault-service.md} +0 -0
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// Workspace model: the shared read model behind every graph-workspace room.
|
|
2
|
+
//
|
|
3
|
+
// Input is an Atelier knowledge graph (`mnstry.atelier-knowledge-graph@v1`) that a
|
|
4
|
+
// host has already limited to what the reader may see, plus an optional knowledge
|
|
5
|
+
// plan. Output is plain data the browser rooms can draw. This module reads no files,
|
|
6
|
+
// grants nothing and infers no relation:
|
|
7
|
+
// - zones come only from the owner's folder structure (a zone rule),
|
|
8
|
+
// - concept membership comes only from a record's own `concept:<id>` tag,
|
|
9
|
+
// - edges keep their declared direction and fall into three classes.
|
|
10
|
+
|
|
11
|
+
const SCHEMA = 'mnstry.atelier-knowledge-graph@v1'
|
|
12
|
+
export const EDGE_CLASSES = Object.freeze(['link', 'containment', 'relation'])
|
|
13
|
+
|
|
14
|
+
/** `related` is a written link, `belongs_to` is folder containment, anything else is a declared relation. */
|
|
15
|
+
export function edgeClass(type) {
|
|
16
|
+
if (type === 'related') return 'link'
|
|
17
|
+
if (type === 'belongs_to') return 'containment'
|
|
18
|
+
return 'relation'
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const humanize = folder => folder.replace(/^[_\d-]+/, '').replace(/[-_]+/g, ' ').replace(/^\w/, c => c.toUpperCase()) || 'Other'
|
|
22
|
+
|
|
23
|
+
export function validateZoneRule(rule = {}) {
|
|
24
|
+
const prefix = rule.prefix ?? ''
|
|
25
|
+
const depth = rule.depth ?? 1
|
|
26
|
+
if (typeof prefix !== 'string' || prefix.includes('..') || prefix.startsWith('/')) throw new TypeError('Zone rule prefix must be a relative path prefix')
|
|
27
|
+
if (!Number.isInteger(depth) || depth < 1 || depth > 4) throw new TypeError('Zone rule depth must be an integer from 1 to 4')
|
|
28
|
+
const labels = rule.labels ?? {}
|
|
29
|
+
if (typeof labels !== 'object' || Array.isArray(labels) || Object.values(labels).some(v => typeof v !== 'string' || v.length > 80)) throw new TypeError('Zone rule labels must map folders to short names')
|
|
30
|
+
return { prefix, depth, labels: Object.assign(Object.create(null), labels), rootLabel: typeof rule.rootLabel === 'string' ? rule.rootLabel.slice(0, 80) : 'Workspace root' }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** The zone of records outside the folder rule. No folder id can be '/'. */
|
|
34
|
+
export const ROOT_ZONE = '/'
|
|
35
|
+
|
|
36
|
+
/** The owner-configured zone of a record path. Records outside the prefix share one root zone. */
|
|
37
|
+
export function zoneOf(path, rule) {
|
|
38
|
+
const r = validateZoneRule(rule)
|
|
39
|
+
if (typeof path !== 'string' || !path.startsWith(r.prefix)) return { id: ROOT_ZONE, label: r.rootLabel }
|
|
40
|
+
const parts = path.slice(r.prefix.length).split('/')
|
|
41
|
+
if (parts.length <= r.depth) return { id: ROOT_ZONE, label: r.rootLabel }
|
|
42
|
+
const folder = parts.slice(0, r.depth).join('/')
|
|
43
|
+
return { id: folder, label: Object.hasOwn(r.labels, folder) ? r.labels[folder] : humanize(folder.split('/').at(-1)) }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function buildWorkspaceModel({ graph, plan = null, zoneRule = {}, summaryLength = 280 }) {
|
|
47
|
+
if (graph?.schema !== SCHEMA || !Array.isArray(graph.nodes) || !Array.isArray(graph.edges)) throw new TypeError('An Atelier knowledge graph v1 is required')
|
|
48
|
+
const rule = validateZoneRule(zoneRule)
|
|
49
|
+
const seenIds = new Set()
|
|
50
|
+
const nodes = graph.nodes.map(n => {
|
|
51
|
+
if (typeof n.id !== 'string' || !n.id || seenIds.has(n.id)) throw new TypeError('Knowledge graph node ids must be unique non-empty strings')
|
|
52
|
+
seenIds.add(n.id)
|
|
53
|
+
const zone = zoneOf(n.path ?? '', rule)
|
|
54
|
+
const tags = Array.isArray(n.tags) ? n.tags.filter(t => typeof t === 'string') : []
|
|
55
|
+
return {
|
|
56
|
+
id: n.id, title: typeof n.title === 'string' && n.title ? n.title : n.id,
|
|
57
|
+
summary: typeof n.summary === 'string' ? n.summary.split('\n')[0].slice(0, summaryLength) : '',
|
|
58
|
+
type: n.type ?? 'document', status: n.status ?? null, audience: n.audience ?? null, path: n.path ?? null,
|
|
59
|
+
tags: tags.filter(t => !t.startsWith('concept:')).slice(0, 8),
|
|
60
|
+
concepts: tags.filter(t => t.startsWith('concept:')).map(t => t.slice(8)),
|
|
61
|
+
zone: zone.id, zoneLabel: zone.label, degree: 0, relationDegree: 0,
|
|
62
|
+
}
|
|
63
|
+
})
|
|
64
|
+
const byId = new Map(nodes.map(n => [n.id, n]))
|
|
65
|
+
const edges = []
|
|
66
|
+
const seen = new Set()
|
|
67
|
+
let outside = 0
|
|
68
|
+
for (const e of graph.edges) {
|
|
69
|
+
if (!byId.has(e.source) || !byId.has(e.target)) { outside++; continue }
|
|
70
|
+
if (e.source === e.target) continue
|
|
71
|
+
const key = `${e.source}\u0000${e.target}\u0000${e.type}`
|
|
72
|
+
if (seen.has(key)) continue
|
|
73
|
+
seen.add(key)
|
|
74
|
+
const kind = edgeClass(e.type)
|
|
75
|
+
edges.push({ id: 'e' + edges.length, source: e.source, target: e.target, type: e.type, kind, declared: e.declared !== false })
|
|
76
|
+
for (const id of [e.source, e.target]) { const n = byId.get(id); n.degree++; if (kind === 'relation') n.relationDegree++ }
|
|
77
|
+
}
|
|
78
|
+
const counts = new Map()
|
|
79
|
+
for (const n of nodes) counts.set(n.zone, { id: n.zone, label: n.zoneLabel, count: (counts.get(n.zone)?.count ?? 0) + 1 })
|
|
80
|
+
const zones = [...counts.values()].sort((a, b) => b.count - a.count || (a.id < b.id ? -1 : 1)).map((z, index) => ({ ...z, index }))
|
|
81
|
+
return {
|
|
82
|
+
schema: 'atelier.graph-workspace-model@v1',
|
|
83
|
+
coverage: { records: nodes.length, links: edges.length, linksOutsideReadableSet: outside, byClass: Object.fromEntries(EDGE_CLASSES.map(k => [k, edges.filter(e => e.kind === k).length])) },
|
|
84
|
+
nodes, edges, zones,
|
|
85
|
+
plan: plan ? planModel(plan, nodes) : null,
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function planModel(plan, nodes) {
|
|
90
|
+
const concepts = Array.isArray(plan.concepts) ? plan.concepts.filter(c => typeof c?.id === 'string') : []
|
|
91
|
+
const ids = new Set(concepts.map(c => c.id))
|
|
92
|
+
return {
|
|
93
|
+
purpose: typeof plan.purpose === 'string' ? plan.purpose : null,
|
|
94
|
+
concepts: concepts.map(c => ({
|
|
95
|
+
id: c.id, title: typeof c.title === 'string' ? c.title : c.id.replace(/-/g, ' ').replace(/^\w/, x => x.toUpperCase()),
|
|
96
|
+
definition: c.definition ?? '', identityRule: c.identityRule ?? '',
|
|
97
|
+
declaredBy: nodes.filter(n => n.concepts.includes(c.id)).map(n => n.id),
|
|
98
|
+
})),
|
|
99
|
+
relations: (plan.relations ?? []).filter(r => ids.has(r.from) && ids.has(r.to)).map(r => ({ id: r.id, predicate: r.predicate, from: r.from, to: r.to, meaning: r.meaning ?? '' })),
|
|
100
|
+
questions: (plan.questions ?? []).map(q => ({ id: q.id, question: q.question ?? q.title ?? q.id, concepts: (q.concepts ?? []).filter(id => ids.has(id)), relations: q.relations ?? [] })),
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Neighbour ids of a record, in either direction, without duplicates. */
|
|
105
|
+
export function neighbours(model, id) {
|
|
106
|
+
const out = new Set()
|
|
107
|
+
for (const e of model.edges) { if (e.source === id) out.add(e.target); else if (e.target === id) out.add(e.source) }
|
|
108
|
+
return [...out]
|
|
109
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// Work recipes, layered: Atelier ships defaults, a cohort coach adds or adjusts,
|
|
2
|
+
// and the owner has the final say. A recipe is data that chooses the room, the lens
|
|
3
|
+
// and the one primary action. Every resolved field records which layer set it.
|
|
4
|
+
|
|
5
|
+
export const ROOMS = Object.freeze(['constellation', 'workbench', 'review'])
|
|
6
|
+
export const LENSES = Object.freeze([null, 'question', 'concept', 'zone'])
|
|
7
|
+
export const SUBJECTS = Object.freeze([null, 'record', 'concept', 'relation'])
|
|
8
|
+
export const LAYERS = Object.freeze(['atelier', 'coach', 'owner'])
|
|
9
|
+
/** The single emphasised action of a recipe. The default shell implements each one. */
|
|
10
|
+
export const PRIMARY_ACTIONS = Object.freeze(['search', 'open-evidence', 'next-step', 'walk', 'review', 'pick'])
|
|
11
|
+
|
|
12
|
+
/** The primary actions the default shell honours for a recipe's room, lens and subject. */
|
|
13
|
+
export function primaryActionsFor({ room, lens = null, subject = null }) {
|
|
14
|
+
if (room === 'review') return ['review']
|
|
15
|
+
if (room === 'workbench') return subject === 'record' ? ['walk'] : ['next-step']
|
|
16
|
+
if (lens === 'zone') return ['pick', 'search']
|
|
17
|
+
if (lens) return ['open-evidence', 'pick', 'search']
|
|
18
|
+
return ['search']
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const DEFAULT_RECIPES = Object.freeze([
|
|
22
|
+
{ id: 'explore', label: 'Explore', room: 'constellation', lens: null, subject: null, primary: 'search', description: 'Wander the whole graph. Zones on, search ready.' },
|
|
23
|
+
{ id: 'question', label: 'Answer a question', room: 'constellation', lens: 'question', subject: null, primary: 'open-evidence', description: 'Light the records that declare a question’s concepts.' },
|
|
24
|
+
{ id: 'refine', label: 'Refine a concept', room: 'workbench', lens: null, subject: 'concept', primary: 'next-step', description: 'One concept, one step at a time.' },
|
|
25
|
+
{ id: 'inspect', label: 'Inspect a record', room: 'workbench', lens: null, subject: 'record', primary: 'walk', description: 'A record and its closest links, with room to read.' },
|
|
26
|
+
].map(Object.freeze))
|
|
27
|
+
|
|
28
|
+
const FIELDS = ['label', 'room', 'lens', 'subject', 'primary', 'description']
|
|
29
|
+
const text = (v, max) => typeof v === 'string' && v.trim().length > 0 && v.length <= max
|
|
30
|
+
|
|
31
|
+
export function validateRecipe(recipe, { partial = false } = {}) {
|
|
32
|
+
const problems = []
|
|
33
|
+
if (!recipe || typeof recipe !== 'object' || Array.isArray(recipe)) return ['recipe must be an object']
|
|
34
|
+
if (!/^[a-z][a-z0-9-]{0,47}$/.test(recipe.id ?? '')) problems.push('id must be a short lowercase slug')
|
|
35
|
+
for (const key of Object.keys(recipe)) if (!['id', 'hidden', ...FIELDS].includes(key)) problems.push(`unknown field ${key}`)
|
|
36
|
+
const need = !partial
|
|
37
|
+
if ((need || 'label' in recipe) && !text(recipe.label, 60)) problems.push('label must be 1–60 characters')
|
|
38
|
+
if ((need || 'description' in recipe) && !text(recipe.description, 200)) problems.push('description must be 1–200 characters')
|
|
39
|
+
if ((need || 'room' in recipe) && !ROOMS.includes(recipe.room)) problems.push(`room must be one of ${ROOMS.join(', ')}`)
|
|
40
|
+
if ('lens' in recipe && !LENSES.includes(recipe.lens)) problems.push('unknown lens')
|
|
41
|
+
if ('subject' in recipe && !SUBJECTS.includes(recipe.subject)) problems.push('unknown subject')
|
|
42
|
+
if ((need || 'primary' in recipe) && !PRIMARY_ACTIONS.includes(recipe.primary)) problems.push(`primary must be one of ${PRIMARY_ACTIONS.join(', ')}`)
|
|
43
|
+
if ('hidden' in recipe && typeof recipe.hidden !== 'boolean') problems.push('hidden must be true or false')
|
|
44
|
+
return problems
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Compose recipe layers. Each layer is a list; an entry with an existing id adjusts
|
|
49
|
+
* it (partial), a new id adds a complete recipe, and `hidden: true` removes it from
|
|
50
|
+
* what the owner sees. Later layers win. Throws on any invalid entry.
|
|
51
|
+
*/
|
|
52
|
+
export function composeRecipes({ atelier = DEFAULT_RECIPES, coach = [], owner = [] } = {}) {
|
|
53
|
+
const resolved = new Map()
|
|
54
|
+
const provenance = new Map()
|
|
55
|
+
for (const [layer, list] of [['atelier', atelier], ['coach', coach], ['owner', owner]]) {
|
|
56
|
+
if (!Array.isArray(list)) throw new TypeError(`${layer} recipes must be a list`)
|
|
57
|
+
for (const entry of list) {
|
|
58
|
+
const existing = resolved.get(entry?.id)
|
|
59
|
+
const problems = validateRecipe(entry, { partial: !!existing })
|
|
60
|
+
if (problems.length) throw new TypeError(`${layer} recipe ${entry?.id ?? '(no id)'}: ${problems.join('; ')}`)
|
|
61
|
+
const next = { lens: null, subject: null, hidden: false, ...(existing ?? {}), ...entry }
|
|
62
|
+
// Only combinations a shell can honour: lenses belong to the constellation,
|
|
63
|
+
// subjects to the workbench, and the review room takes neither.
|
|
64
|
+
if (next.room === 'workbench' && !next.subject) throw new TypeError(`${layer} recipe ${entry.id}: a workbench recipe needs a subject`)
|
|
65
|
+
if (next.room !== 'workbench' && next.subject) throw new TypeError(`${layer} recipe ${entry.id}: only a workbench recipe takes a subject`)
|
|
66
|
+
if (next.room !== 'constellation' && next.lens) throw new TypeError(`${layer} recipe ${entry.id}: only a constellation recipe takes a lens`)
|
|
67
|
+
const actions = primaryActionsFor(next)
|
|
68
|
+
if (!actions.includes(next.primary)) throw new TypeError(`${layer} recipe ${entry.id}: primary ${next.primary} is not available here; use ${actions.join(' or ')}`)
|
|
69
|
+
resolved.set(entry.id, next)
|
|
70
|
+
const fields = provenance.get(entry.id) ?? {}
|
|
71
|
+
for (const key of Object.keys(entry)) if (key !== 'id') fields[key] = layer
|
|
72
|
+
if (!existing) for (const key of FIELDS) fields[key] ??= layer
|
|
73
|
+
provenance.set(entry.id, fields)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
const visible = [...resolved.values()].filter(r => !r.hidden)
|
|
77
|
+
return { recipes: visible.map(r => ({ ...r, setBy: { ...provenance.get(r.id) } })), hidden: [...resolved.values()].filter(r => r.hidden).map(r => r.id) }
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Recipes a workspace can actually run: concept work needs a plan with concepts. */
|
|
81
|
+
export function availableRecipes(recipes, model) {
|
|
82
|
+
const concepts = !!model.plan?.concepts.length, relations = !!model.plan?.relations.length, questions = !!model.plan?.questions.length
|
|
83
|
+
return recipes.filter(r =>
|
|
84
|
+
(r.subject !== 'concept' || concepts) && (r.subject !== 'relation' || relations) &&
|
|
85
|
+
(r.lens !== 'question' || questions) && (r.lens !== 'concept' || concepts) &&
|
|
86
|
+
(r.room !== 'review' || concepts) && (r.subject !== 'record' || model.nodes.length > 0) &&
|
|
87
|
+
(r.lens !== 'zone' || model.zones?.length > 0))
|
|
88
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// Scale primitives for workspaces with tens of thousands of records.
|
|
2
|
+
//
|
|
3
|
+
// A constellation draws every readable record while that stays legible and fast.
|
|
4
|
+
// Beyond a budget, the whole is shown as zones (with honest counts and directed
|
|
5
|
+
// bundles between them) and a person opens one zone at a time. Nothing here hides
|
|
6
|
+
// a record silently: every bounded result states what it left out.
|
|
7
|
+
|
|
8
|
+
export const DEFAULT_BUDGET = Object.freeze({ records: 12000, links: 60000 })
|
|
9
|
+
|
|
10
|
+
/** Decide how the whole should be drawn for this model and budget. */
|
|
11
|
+
export function planConstellation(model, budget = DEFAULT_BUDGET) {
|
|
12
|
+
const records = model.nodes.length, links = model.edges.length
|
|
13
|
+
if (records <= budget.records && links <= budget.links) return { mode: 'records', records, links }
|
|
14
|
+
return { mode: 'zones', records, links, reason: records > budget.records ? 'records-over-budget' : 'links-over-budget', budget: { ...budget } }
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Zone-level summary of the whole: counts by type and directed bundles by edge class. */
|
|
18
|
+
export function aggregateZones(model) {
|
|
19
|
+
const zoneOfId = new Map(model.nodes.map(n => [n.id, n.zone]))
|
|
20
|
+
const zones = model.zones.map(z => ({ id: z.id, label: z.label, index: z.index, count: z.count, types: Object.create(null), declaredRelations: 0 }))
|
|
21
|
+
const byId = new Map(zones.map(z => [z.id, z]))
|
|
22
|
+
for (const n of model.nodes) { const z = byId.get(n.zone); z.types[n.type] = (z.types[n.type] ?? 0) + 1 }
|
|
23
|
+
const bundles = new Map()
|
|
24
|
+
let internal = 0
|
|
25
|
+
for (const e of model.edges) {
|
|
26
|
+
const a = zoneOfId.get(e.source), b = zoneOfId.get(e.target)
|
|
27
|
+
if (a === b) { internal++; if (e.kind === 'relation') byId.get(a).declaredRelations++; continue }
|
|
28
|
+
const key = a + '\u0000' + b
|
|
29
|
+
const bundle = bundles.get(key) ?? { from: a, to: b, total: 0, link: 0, containment: 0, relation: 0 }
|
|
30
|
+
bundle.total++; bundle[e.kind]++
|
|
31
|
+
bundles.set(key, bundle)
|
|
32
|
+
}
|
|
33
|
+
return {
|
|
34
|
+
schema: 'atelier.graph-workspace-zones@v1',
|
|
35
|
+
coverage: { records: model.nodes.length, zones: zones.length, linksInsideZones: internal, linksBetweenZones: model.edges.length - internal, linksOutsideReadableSet: model.coverage.linksOutsideReadableSet },
|
|
36
|
+
zones,
|
|
37
|
+
bundles: [...bundles.values()].sort((x, y) => y.total - x.total || (x.from + x.to < y.from + y.to ? -1 : 1)),
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Records ranked by connection count, then title, for budgets and label priority. */
|
|
42
|
+
export function rankRecords(model) {
|
|
43
|
+
return [...model.nodes].sort((a, b) => b.degree - a.degree || (a.title < b.title ? -1 : a.title > b.title ? 1 : 0))
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* A bounded sub-model for one zone. Members are kept by rank up to `limit`.
|
|
48
|
+
* Every link touching the zone is accounted for exactly once:
|
|
49
|
+
* drawn (both ends kept) + omittedInsideZone + toOtherZones + omittedToOtherZones = links touching the zone.
|
|
50
|
+
* Plan concept declarations are projected onto the kept records, with omissions counted.
|
|
51
|
+
*/
|
|
52
|
+
export function zoneSlice(model, zoneId, { limit = DEFAULT_BUDGET.records } = {}) {
|
|
53
|
+
const members = rankRecords(model).filter(n => n.zone === zoneId)
|
|
54
|
+
const kept = members.slice(0, limit)
|
|
55
|
+
const keep = new Set(kept.map(n => n.id))
|
|
56
|
+
const inZone = new Set(members.map(n => n.id))
|
|
57
|
+
const zoneOfId = new Map(model.nodes.map(n => [n.id, n.zone]))
|
|
58
|
+
const edges = []
|
|
59
|
+
const boundary = new Map()
|
|
60
|
+
let omittedInsideZone = 0, omittedToOtherZones = 0, touching = 0
|
|
61
|
+
for (const e of model.edges) {
|
|
62
|
+
const s = inZone.has(e.source), t = inZone.has(e.target)
|
|
63
|
+
if (!s && !t) continue
|
|
64
|
+
touching++
|
|
65
|
+
if (keep.has(e.source) && keep.has(e.target)) { edges.push(e); continue }
|
|
66
|
+
if (s && t) { omittedInsideZone++; continue }
|
|
67
|
+
const memberEnd = s ? e.source : e.target
|
|
68
|
+
if (!keep.has(memberEnd)) { omittedToOtherZones++; continue }
|
|
69
|
+
const other = zoneOfId.get(s ? e.target : e.source)
|
|
70
|
+
boundary.set(other, (boundary.get(other) ?? 0) + 1)
|
|
71
|
+
}
|
|
72
|
+
const toOtherZones = [...boundary.values()].reduce((a, b) => a + b, 0)
|
|
73
|
+
const byClass = { link: 0, containment: 0, relation: 0 }
|
|
74
|
+
for (const e of edges) byClass[e.kind]++
|
|
75
|
+
const plan = model.plan ? {
|
|
76
|
+
...model.plan,
|
|
77
|
+
concepts: model.plan.concepts.map(c => {
|
|
78
|
+
const declaredBy = c.declaredBy.filter(id => keep.has(id))
|
|
79
|
+
return { ...c, declaredBy, omittedDeclarations: c.declaredBy.length - declaredBy.length }
|
|
80
|
+
}),
|
|
81
|
+
} : null
|
|
82
|
+
return {
|
|
83
|
+
...model,
|
|
84
|
+
nodes: kept.map(n => ({ ...n })),
|
|
85
|
+
edges,
|
|
86
|
+
zones: model.zones.filter(z => z.id === zoneId).map(z => ({ ...z, count: kept.length })),
|
|
87
|
+
plan,
|
|
88
|
+
coverage: { records: kept.length, links: edges.length, byClass, linksOutsideReadableSet: model.coverage.linksOutsideReadableSet },
|
|
89
|
+
slice: {
|
|
90
|
+
zone: zoneId, membersInZone: members.length, omittedRecords: members.length - kept.length,
|
|
91
|
+
linksTouchingZone: touching, omittedLinksInsideZone: omittedInsideZone, omittedLinksToOtherZones: omittedToOtherZones, linksToOtherZonesCount: toOtherZones,
|
|
92
|
+
linksToOtherZones: [...boundary.entries()].map(([zone, count]) => ({ zone, count })).sort((a, b) => b.count - a.count),
|
|
93
|
+
},
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
// Graph-workspace themes: resolved token data, never stylesheets.
|
|
2
|
+
// Atelier ships two neutral defaults, `night` (the default) and `paper`. A coach or
|
|
3
|
+
// owner theme overrides values by name; the result is validated, including text
|
|
4
|
+
// contrast, before any room applies it as CSS custom properties.
|
|
5
|
+
|
|
6
|
+
const CSS_VARS = ['--canvas', '--surface', '--surface-2', '--surface-glass', '--ink', '--ink-2', '--ink-3', '--line', '--line-strong', '--accent', '--accent-ink', '--focus', '--warn', '--danger', '--ok', '--font-sans', '--font-display', '--radius', '--radius-lg', '--shadow', '--motion', '--ease']
|
|
7
|
+
const GRAPH = ['canvas', 'edge', 'edgeStructural', 'dim', 'label', 'labelHalo', 'selection']
|
|
8
|
+
|
|
9
|
+
const night = {
|
|
10
|
+
vars: {
|
|
11
|
+
'--canvas': '#0b0e14', '--surface': '#131823', '--surface-2': '#1a2030', '--surface-glass': 'rgba(19,24,35,0.82)',
|
|
12
|
+
'--ink': '#eef1f7', '--ink-2': '#b9c1d3', '--ink-3': '#8d97ad', '--line': 'rgba(238,241,247,0.12)', '--line-strong': 'rgba(238,241,247,0.26)',
|
|
13
|
+
'--accent': '#9cc4ff', '--accent-ink': '#08101d', '--focus': '#ffd479', '--warn': '#ffc46b', '--danger': '#ff9a8a', '--ok': '#8fe0b0',
|
|
14
|
+
'--font-sans': 'Inter, "SF Pro Text", -apple-system, system-ui, sans-serif', '--font-display': '"SF Pro Display", Inter, -apple-system, system-ui, sans-serif',
|
|
15
|
+
'--radius': '10px', '--radius-lg': '16px', '--shadow': '0 12px 40px rgba(0,0,0,0.45)', '--motion': '240ms', '--ease': 'cubic-bezier(0.2, 0, 0, 1)',
|
|
16
|
+
},
|
|
17
|
+
graph: { canvas: '#0b0e14', edge: '#7d88a3', edgeStructural: '#c9d2e6', dim: '#283044', label: '#e6eaf2', labelHalo: '#0b0e14', selection: '#ffd479',
|
|
18
|
+
zones: ['#7aa7ff', '#5fd1b0', '#f2a65a', '#c792ea', '#ff8fa3', '#e6d36a', '#6fd3f2', '#a4e07a', '#f28fd2', '#b0b8c8'] },
|
|
19
|
+
}
|
|
20
|
+
const paper = {
|
|
21
|
+
vars: {
|
|
22
|
+
...night.vars,
|
|
23
|
+
'--canvas': '#f5f3ee', '--surface': '#ffffff', '--surface-2': '#f0ede6', '--surface-glass': 'rgba(255,255,255,0.86)',
|
|
24
|
+
'--ink': '#1a1d24', '--ink-2': '#454b58', '--ink-3': '#5f6676', '--line': 'rgba(26,29,36,0.12)', '--line-strong': 'rgba(26,29,36,0.28)',
|
|
25
|
+
'--accent': '#2c55a8', '--accent-ink': '#ffffff', '--focus': '#a05a00', '--warn': '#8a4a00', '--danger': '#a32121', '--ok': '#1f6b3c', '--shadow': '0 12px 36px rgba(20,22,30,0.14)',
|
|
26
|
+
},
|
|
27
|
+
graph: { canvas: '#f5f3ee', edge: '#9aa1ae', edgeStructural: '#4a5162', dim: '#dcd8cf', label: '#1a1d24', labelHalo: '#f5f3ee', selection: '#a05a00',
|
|
28
|
+
zones: ['#2f62c9', '#0f7259', '#9a4f0c', '#7a45a8', '#b83255', '#6f5d00', '#0f6a85', '#3f6c14', '#a3337f', '#5d6577'] },
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export const DEFAULT_THEME = Object.freeze({
|
|
32
|
+
schema: 'atelier.graph-workspace-theme@v1', id: 'atelier-night', version: '1', defaultScheme: 'dark',
|
|
33
|
+
schemes: Object.freeze({ dark: night, light: paper }),
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
// Every token has one type. Only colours, lengths, durations, an easing and font
|
|
37
|
+
// family names are accepted, so no theme value can load a resource or inject CSS.
|
|
38
|
+
const HEX = /^#[0-9a-f]{6}$/i
|
|
39
|
+
const RGBA = /^rgba\(\s*\d{1,3}\s*,\s*\d{1,3}\s*,\s*\d{1,3}\s*,\s*(?:0|1|0?\.\d{1,3})\s*\)$/i
|
|
40
|
+
const COLOR = v => HEX.test(v)
|
|
41
|
+
const SOFT_COLOR = v => HEX.test(v) || RGBA.test(v)
|
|
42
|
+
const FONT = v => /^[A-Za-z0-9 "',.-]{1,160}$/.test(v) && !/["']{2}/.test(v)
|
|
43
|
+
const LENGTH = v => /^\d{1,2}(?:\.\d{1,2})?px$/.test(v)
|
|
44
|
+
const SHADOW = v => v === 'none' || v.split(/,(?![^()]*\))/).every(part => /^\s*(?:(?:0|-?\d{1,3}(?:\.\d{1,2})?px)\s+){2,4}(?:rgba\(\s*\d{1,3}\s*,\s*\d{1,3}\s*,\s*\d{1,3}\s*,\s*(?:0|1|0?\.\d{1,3})\s*\)|#[0-9a-f]{6})\s*$/i.test(part))
|
|
45
|
+
const DURATION = v => /^\d{1,4}ms$/.test(v)
|
|
46
|
+
const EASE = v => /^(?:ease|linear|ease-in|ease-out|ease-in-out|cubic-bezier\(\s*-?\d(?:\.\d{1,3})?\s*,\s*-?\d(?:\.\d{1,3})?\s*,\s*-?\d(?:\.\d{1,3})?\s*,\s*-?\d(?:\.\d{1,3})?\s*\))$/.test(v)
|
|
47
|
+
export const TOKEN_TYPES = Object.freeze({
|
|
48
|
+
'--canvas': COLOR, '--surface': COLOR, '--surface-2': COLOR, '--surface-glass': SOFT_COLOR,
|
|
49
|
+
'--ink': COLOR, '--ink-2': COLOR, '--ink-3': COLOR, '--line': SOFT_COLOR, '--line-strong': SOFT_COLOR,
|
|
50
|
+
'--accent': COLOR, '--accent-ink': COLOR, '--focus': COLOR, '--warn': COLOR, '--danger': COLOR, '--ok': COLOR,
|
|
51
|
+
'--font-sans': FONT, '--font-display': FONT, '--radius': LENGTH, '--radius-lg': LENGTH, '--shadow': SHADOW, '--motion': DURATION, '--ease': EASE,
|
|
52
|
+
})
|
|
53
|
+
const LAYER_FIELDS = ['id', 'version', 'defaultScheme', 'schemes']
|
|
54
|
+
const SCHEME_FIELDS = ['vars', 'graph']
|
|
55
|
+
// Text and the surfaces it is drawn on in the rooms, with the minimum ratio for its size.
|
|
56
|
+
const TEXT_PAIRS = [
|
|
57
|
+
['--ink', '--canvas', 7], ['--ink', '--surface', 7], ['--ink', '--surface-2', 7],
|
|
58
|
+
['--ink-2', '--canvas', 4.5], ['--ink-2', '--surface', 4.5], ['--ink-2', '--surface-2', 4.5],
|
|
59
|
+
['--ink-3', '--canvas', 4.5], ['--ink-3', '--surface', 4.5], ['--ink-3', '--surface-2', 4.5],
|
|
60
|
+
['--accent-ink', '--accent', 4.5], ['--accent', '--canvas', 4.5], ['--accent', '--surface', 4.5],
|
|
61
|
+
['--warn', '--surface', 4.5], ['--danger', '--surface', 4.5], ['--ok', '--surface', 4.5],
|
|
62
|
+
]
|
|
63
|
+
// Text on the translucent chrome (pills, command bar, selection card), composited
|
|
64
|
+
// over each backdrop it floats on.
|
|
65
|
+
const GLASS_TEXT = [['--ink', 4.5], ['--ink-2', 4.5], ['--ink-3', 4.5]]
|
|
66
|
+
const GLASS_BACKDROPS = ['--canvas', '--surface-2']
|
|
67
|
+
function composite(over, under) {
|
|
68
|
+
const rgb = h => { const n = parseInt(h.slice(1, 7), 16); return [n >> 16, (n >> 8) & 255, n & 255] }
|
|
69
|
+
const m = RGBA.test(over) ? over.match(/[\d.]+/g).map(Number) : [...rgb(over), 1]
|
|
70
|
+
const [r, g, b, a] = [Math.min(255, m[0]), Math.min(255, m[1]), Math.min(255, m[2]), Math.min(1, m[3])]
|
|
71
|
+
const base = rgb(under)
|
|
72
|
+
return '#' + [r, g, b].map((v, i) => Math.round(v * a + base[i] * (1 - a)).toString(16).padStart(2, '0')).join('')
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function layerProblems(layer, { partial }) {
|
|
76
|
+
const problems = []
|
|
77
|
+
if (!layer || typeof layer !== 'object' || Array.isArray(layer)) return ['theme layer must be an object']
|
|
78
|
+
for (const key of Object.keys(layer)) if (!LAYER_FIELDS.includes(key) && !(key === 'schema' && !partial)) problems.push(`unknown theme field ${key}`)
|
|
79
|
+
if ('id' in layer && !/^[a-z][a-z0-9-]{0,47}$/.test(layer.id ?? '')) problems.push('id must be a short lowercase slug')
|
|
80
|
+
if ('version' in layer && !/^[\w.-]{1,32}$/.test(layer.version ?? '')) problems.push('version must be a short label')
|
|
81
|
+
if ('defaultScheme' in layer && !['dark', 'light'].includes(layer.defaultScheme)) problems.push('defaultScheme must be dark or light')
|
|
82
|
+
if ('schemes' in layer) {
|
|
83
|
+
if (!layer.schemes || typeof layer.schemes !== 'object' || Array.isArray(layer.schemes)) problems.push('schemes must be an object')
|
|
84
|
+
else for (const [scheme, value] of Object.entries(layer.schemes)) {
|
|
85
|
+
if (!['dark', 'light'].includes(scheme)) { problems.push(`unknown scheme ${scheme}`); continue }
|
|
86
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) { problems.push(`${scheme}: must be an object`); continue }
|
|
87
|
+
for (const key of Object.keys(value)) if (!SCHEME_FIELDS.includes(key)) problems.push(`${scheme}: unknown field ${key}`)
|
|
88
|
+
for (const [k, v] of Object.entries(value.vars ?? {})) {
|
|
89
|
+
if (!Object.hasOwn(TOKEN_TYPES, k)) problems.push(`${scheme}: unknown token ${k}`)
|
|
90
|
+
else if (typeof v !== 'string' || !TOKEN_TYPES[k](v)) problems.push(`${scheme}: rejected ${k}`)
|
|
91
|
+
}
|
|
92
|
+
for (const [k, v] of Object.entries(value.graph ?? {})) {
|
|
93
|
+
if (k === 'zones') { if (!Array.isArray(v) || v.length < 4 || v.length > 16 || !v.every(z => typeof z === 'string' && HEX.test(z))) problems.push(`${scheme}: graph.zones needs 4–16 #rrggbb colours`) }
|
|
94
|
+
else if (!GRAPH.includes(k)) problems.push(`${scheme}: unknown graph key ${k}`)
|
|
95
|
+
else if (typeof v !== 'string' || !HEX.test(v)) problems.push(`${scheme}: graph.${k} must be #rrggbb`)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return problems
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function validateTheme(theme) {
|
|
103
|
+
const problems = []
|
|
104
|
+
if (theme?.schema !== 'atelier.graph-workspace-theme@v1') problems.push('schema must be atelier.graph-workspace-theme@v1')
|
|
105
|
+
problems.push(...layerProblems(theme, { partial: false }))
|
|
106
|
+
if (!['dark', 'light'].includes(theme?.defaultScheme)) problems.push('defaultScheme must be dark or light')
|
|
107
|
+
for (const scheme of ['dark', 'light']) {
|
|
108
|
+
const s = theme?.schemes?.[scheme]
|
|
109
|
+
if (!s) { problems.push(`missing ${scheme} scheme`); continue }
|
|
110
|
+
for (const k of Object.keys(TOKEN_TYPES)) if (!(k in (s.vars ?? {}))) problems.push(`${scheme}: missing ${k}`)
|
|
111
|
+
for (const k of GRAPH) if (!(k in (s.graph ?? {}))) problems.push(`${scheme}: missing graph.${k}`)
|
|
112
|
+
if (!Array.isArray(s.graph?.zones)) problems.push(`${scheme}: missing graph.zones`)
|
|
113
|
+
}
|
|
114
|
+
if (problems.length) return [...new Set(problems)]
|
|
115
|
+
for (const scheme of ['dark', 'light']) {
|
|
116
|
+
const { vars, graph } = theme.schemes[scheme]
|
|
117
|
+
if (vars['--canvas'].toLowerCase() !== graph.canvas.toLowerCase()) problems.push(`${scheme}: --canvas and graph.canvas must be the same colour`)
|
|
118
|
+
for (const [fg, bg, min] of TEXT_PAIRS) if (contrastRatio(vars[fg], vars[bg]) < min) problems.push(`${scheme}: ${fg} on ${bg} is below ${min}:1`)
|
|
119
|
+
for (const backdrop of GLASS_BACKDROPS) {
|
|
120
|
+
const glass = composite(vars['--surface-glass'], vars[backdrop])
|
|
121
|
+
for (const [fg, min] of GLASS_TEXT) if (contrastRatio(vars[fg], glass) < min) problems.push(`${scheme}: ${fg} on --surface-glass over ${backdrop} is below ${min}:1`)
|
|
122
|
+
}
|
|
123
|
+
if (contrastRatio(graph.label, graph.canvas) < 7) problems.push(`${scheme}: label contrast on the canvas is below 7:1`)
|
|
124
|
+
if (Math.min(...graph.zones.map(z => contrastRatio(z, graph.canvas))) < 3) problems.push(`${scheme}: a zone colour is below 3:1 on the canvas (zone names are large text)`)
|
|
125
|
+
}
|
|
126
|
+
return problems
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Override a base theme by layer (coach, then owner). Each layer is validated before it is applied. */
|
|
130
|
+
export function composeTheme(base = DEFAULT_THEME, ...layers) {
|
|
131
|
+
const baseProblems = validateTheme(base)
|
|
132
|
+
if (baseProblems.length) throw new TypeError('Base theme refused: ' + baseProblems.join('; '))
|
|
133
|
+
const out = structuredClone(base)
|
|
134
|
+
layers.filter(Boolean).forEach((layer, index) => {
|
|
135
|
+
const problems = layerProblems(layer, { partial: true })
|
|
136
|
+
if (problems.length) throw new TypeError(`Theme layer ${index + 1} refused: ${problems.join('; ')}`)
|
|
137
|
+
if (layer.id) out.id = layer.id
|
|
138
|
+
if (layer.version) out.version = layer.version
|
|
139
|
+
if (layer.defaultScheme) out.defaultScheme = layer.defaultScheme
|
|
140
|
+
for (const scheme of ['dark', 'light']) {
|
|
141
|
+
const s = layer.schemes?.[scheme]
|
|
142
|
+
if (!s) continue
|
|
143
|
+
Object.assign(out.schemes[scheme].vars, s.vars ?? {})
|
|
144
|
+
Object.assign(out.schemes[scheme].graph, s.graph ?? {})
|
|
145
|
+
}
|
|
146
|
+
})
|
|
147
|
+
const problems = validateTheme(out)
|
|
148
|
+
if (problems.length) throw new TypeError('Theme refused: ' + problems.join('; '))
|
|
149
|
+
return out
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function contrastRatio(a, b) {
|
|
153
|
+
const lum = h => { const n = parseInt(h.slice(1, 7), 16); return [n >> 16, (n >> 8) & 255, n & 255].map(v => { v /= 255; return v <= 0.03928 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4 }).reduce((s, v, i) => s + v * [0.2126, 0.7152, 0.0722][i], 0) }
|
|
154
|
+
const [x, y] = [lum(a), lum(b)].sort((p, q) => q - p)
|
|
155
|
+
return (x + 0.05) / (y + 0.05)
|
|
156
|
+
}
|
|
@@ -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/
|
|
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){
|