@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
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export const ruleIdentity = Object.freeze({
|
|
2
2
|
"profile": "source-qualified-required-direction/r1",
|
|
3
|
-
"commit": "
|
|
4
|
-
"tree": "
|
|
3
|
+
"commit": "43a6d6a9d4718e735576bdd878cbea1f9b7e454a",
|
|
4
|
+
"tree": "306d92f8f3a36fa8938419bf8df32cb3b30f6704",
|
|
5
5
|
"files": [
|
|
6
6
|
{
|
|
7
7
|
"path": "src/knowledge/plan.mjs",
|
|
@@ -24,8 +24,8 @@ export const ruleIdentity = Object.freeze({
|
|
|
24
24
|
{
|
|
25
25
|
"path": "src/graph/knowledge-graph.mjs",
|
|
26
26
|
"bytes": 66377,
|
|
27
|
-
"sha256": "
|
|
28
|
-
"blob": "
|
|
27
|
+
"sha256": "ee62867895c7a8ab16295365120c2b4a69754066bf2258fc9156708d9b61bc96",
|
|
28
|
+
"blob": "fc6c543d08bef5de449709674122994cb1c51b75"
|
|
29
29
|
},
|
|
30
30
|
{
|
|
31
31
|
"path": "templates/knowledge-workspace/knowledge-plan.schema.json",
|
|
@@ -42,8 +42,8 @@ export const ruleIdentity = Object.freeze({
|
|
|
42
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
43
|
},
|
|
44
44
|
"previousSourceAuthority": {
|
|
45
|
-
"commit": "
|
|
46
|
-
"tree": "
|
|
47
|
-
"note": "Regenerated by the source owner for a changed candidate:
|
|
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."
|
|
48
48
|
}
|
|
49
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
|
-
|
|
36
|
-
|
|
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',
|
|
@@ -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$/
|
|
@@ -136,7 +137,7 @@ function readHistory(personalHome) {
|
|
|
136
137
|
let record
|
|
137
138
|
try { record = JSON.parse(bytes) } catch { refuse('selection-history-corrupt') }
|
|
138
139
|
if (record?.schema !== SELECTION_SCHEMA || record.sequence !== index + 1 || record.previous !== head || !ID.test(record.generationId ?? '') ||
|
|
139
|
-
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')
|
|
140
141
|
records.push(record)
|
|
141
142
|
head = hash(bytes)
|
|
142
143
|
files.push({ name, bytes: bytes.length, digest: head })
|
|
@@ -163,7 +164,7 @@ export function selectPersonalGeneration(options) {
|
|
|
163
164
|
const { eligible, reason } = eligibility(personalHome, generationId)
|
|
164
165
|
if (!eligible) refuse(reason)
|
|
165
166
|
const dir = privateDir(personalHome, 'selections', true)
|
|
166
|
-
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() }
|
|
167
168
|
const content = canonical(record)
|
|
168
169
|
try { publishExclusive(dir, `${String(record.sequence).padStart(6, '0')}.json`, content, 'selection') }
|
|
169
170
|
catch (error) { if (error.code === 'EEXIST') refuse('selection-concurrent'); throw error }
|
|
@@ -198,6 +199,20 @@ export function readPersonalSelectionHead(options) {
|
|
|
198
199
|
}, 'personal-home-unavailable')
|
|
199
200
|
}
|
|
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
|
+
|
|
201
216
|
// Lists entries without failing the whole inventory: a concurrently removed entry
|
|
202
217
|
// is skipped; an unreadable, foreign or oversized one is reported without a digest.
|
|
203
218
|
function listFiles(dir, keep) {
|
package/src/project/config.mjs
CHANGED
|
@@ -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]))) {
|
|
@@ -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
|
|
4
|
+
export { githubLookup } from './providers/github.mjs'
|
|
5
5
|
|
|
6
|
-
const
|
|
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
|
|
@@ -75,7 +75,7 @@ import { isUserOwnedSettingsPath, prepareSettings } from './settings.mjs'
|
|
|
75
75
|
//
|
|
76
76
|
// A view is laid out in vault layout 2: folders mirror the repository, the
|
|
77
77
|
// file name is the note's title and each note names its identity in generated
|
|
78
|
-
// front-matter properties (see "Vault layout" in docs/obsidian-contract.md).
|
|
78
|
+
// front-matter properties (see "Vault layout" in docs/reference/obsidian-contract.md).
|
|
79
79
|
// Layout 1, the earlier release's, is kept for a view whose prior generation
|
|
80
80
|
// is in layout 1 and holds a note with an open edit, and for recovering a
|
|
81
81
|
// layout 1 generation as it was published.
|
|
@@ -5,7 +5,7 @@ import { refuse } from './byte-lens.mjs'
|
|
|
5
5
|
// judges the assembled result, every note included, whether the note was
|
|
6
6
|
// emitted on this call or reused from the preparation cache, and refuses the
|
|
7
7
|
// view with `redaction-failure`, naming the in-view note (or file) and the
|
|
8
|
-
// rule, never the value. See "The redaction guard" in docs/obsidian-contract.md.
|
|
8
|
+
// rule, never the value. See "The redaction guard" in docs/reference/obsidian-contract.md.
|
|
9
9
|
//
|
|
10
10
|
// allow-list every path the emitter wrote (note and attachment paths,
|
|
11
11
|
// rewritten link and embed targets, link targets in generated
|
|
@@ -2,11 +2,12 @@ import { createHash } from 'node:crypto'
|
|
|
2
2
|
import fs from 'node:fs'
|
|
3
3
|
import path from 'node:path'
|
|
4
4
|
import { Worker, isMainThread, parentPort, workerData } from 'node:worker_threads'
|
|
5
|
-
import { PersonalWorkspaceRefusal, composePersonalWorkspace
|
|
5
|
+
import { PersonalWorkspaceRefusal, composePersonalWorkspace } from '@mnstry/atelier/personal-workspace'
|
|
6
6
|
import { ensureLocalState, resolveProjectConfig, validateProjectConfigDoc } from '@mnstry/atelier/project'
|
|
7
7
|
import { buildCanonicalGraph } from '../../graph/graph.mjs'
|
|
8
8
|
import { ObsidianMaintenanceRefusal, refuse } from '../../runtime/obsidian/errors.mjs'
|
|
9
9
|
import { OBSIDIAN_EXT_KEY } from './contracts.mjs'
|
|
10
|
+
import { readPersonalSelectionSnapshot } from '../../personal-workspace/selection.mjs'
|
|
10
11
|
|
|
11
12
|
// The one personal-workspace route of the Obsidian projection.
|
|
12
13
|
//
|
|
@@ -411,22 +412,17 @@ export async function validatePersonalWorkspace(project) {
|
|
|
411
412
|
|
|
412
413
|
const selectionRecord = ({ personalHome }, sequence) => path.join(personalHome, 'selections', `${String(sequence).padStart(6, '0')}.json`)
|
|
413
414
|
|
|
414
|
-
// The
|
|
415
|
-
//
|
|
416
|
-
// it stops a view (notePersonalSelectionNow). Kept in the main thread: no composition worker reads the history's end,
|
|
417
|
-
// and one that did would have a map of its own and escape the rule.
|
|
415
|
+
// The most recently accepted head observed in this process. Durable continuity is checked by the loader's note
|
|
416
|
+
// callback before this index changes. The index only tells an engine when another reader requires it to reload.
|
|
418
417
|
const READ_UP_TO = new Map()
|
|
419
418
|
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
const before = READ_UP_TO.get(personalHome) ?? 0
|
|
428
|
-
if (head.sequence > before) READ_UP_TO.set(personalHome, head.sequence)
|
|
429
|
-
else if (refusesShortened && head.sequence < before) refuse('selection-history-shortened', 'the selection history of the personal workspace ends at an earlier record than this process has read it ending at; nothing is used from it while it is shorter', { source: 'personal-workspace' })
|
|
419
|
+
export function notePersonalSelectionHead(personalHome, head, { refusesShortened = true, note = null } = {}) {
|
|
420
|
+
if (note !== null) note(head)
|
|
421
|
+
else {
|
|
422
|
+
const before = READ_UP_TO.get(personalHome)?.sequence ?? 0
|
|
423
|
+
if (refusesShortened && head.sequence < before) refuse('selection-history-shortened', 'the selection history of the personal workspace ends at an earlier record than this process has read; nothing is used from it', { source: 'personal-workspace' })
|
|
424
|
+
}
|
|
425
|
+
if (refusesShortened) READ_UP_TO.set(personalHome, { sequence: head.sequence, head: head.head })
|
|
430
426
|
return head
|
|
431
427
|
}
|
|
432
428
|
|
|
@@ -435,8 +431,8 @@ export function notePersonalSelectionHead(personalHome, head, { refusesShortened
|
|
|
435
431
|
// appended to the history, or a history cut short, is a change. And every time the composition is asked again
|
|
436
432
|
// (validatePersonalWorkspace) the history is read again and must still end with that record. `observe: false`,
|
|
437
433
|
// `recheck: false` and `refusesShortened: false` are test seams only, the mutation controls of each.
|
|
438
|
-
export function pinPersonalSelection(project, { sequence, head }, { observe = true, recheck = true, refusesShortened = true } = {}) {
|
|
439
|
-
SELECTED.set(personalWorkspaceBindingOf(project), Object.freeze({ sequence, head, observe, recheck, refusesShortened }))
|
|
434
|
+
export function pinPersonalSelection(project, { sequence, head }, { observe = true, recheck = true, refusesShortened = true, note = null } = {}) {
|
|
435
|
+
SELECTED.set(personalWorkspaceBindingOf(project), Object.freeze({ sequence, head, observe, recheck, refusesShortened, note }))
|
|
440
436
|
return project
|
|
441
437
|
}
|
|
442
438
|
|
|
@@ -449,25 +445,23 @@ export function personalSelectionOf(project) {
|
|
|
449
445
|
|
|
450
446
|
// Refuses `personal-selection-changed` unless the history still ends with the record this project is pinned to; a
|
|
451
447
|
// history that cannot be read refuses with the module's own code. What is read is noted first
|
|
452
|
-
// (notePersonalSelectionHead):
|
|
453
|
-
//
|
|
454
|
-
// is not pinned.
|
|
448
|
+
// (notePersonalSelectionHead): the same durable continuity rule applies to this read, before its head is compared
|
|
449
|
+
// with the pin. Nothing for a project that is not pinned.
|
|
455
450
|
export function assertPersonalSelection(project) {
|
|
456
451
|
const pinned = personalSelectionOf(project)
|
|
457
452
|
if (pinned === null) return
|
|
458
453
|
let now
|
|
459
|
-
try { now =
|
|
454
|
+
try { now = readPersonalSelectionSnapshot({ personalHome: pinned.personalHome }) } catch (error) { asMaintenanceRefusal(error, 'the selection history of the personal workspace could not be read; nothing is used from it') }
|
|
460
455
|
notePersonalSelectionHead(pinned.personalHome, now, SELECTED.get(personalWorkspaceBindingOf(project)))
|
|
461
456
|
if (now.sequence !== pinned.sequence || now.head !== pinned.head) refuse('personal-selection-changed', 'the confirmed selection of the personal workspace is no longer the one this project was loaded under; nothing is used from it', { source: 'personal-workspace' })
|
|
462
457
|
}
|
|
463
458
|
|
|
464
|
-
// Whether this process
|
|
465
|
-
//
|
|
466
|
-
// is not the one to publish under any more, and is loaded again: the load follows the confirmation, or refuses the
|
|
467
|
-
// shortened history. In memory, with no read. False for a project that is not pinned.
|
|
459
|
+
// Whether another reader in this process accepted a head different from the pin: a later confirmation or a newly
|
|
460
|
+
// confirmed shorter history. In memory, with no read. False for a project that is not pinned.
|
|
468
461
|
export function personalSelectionBehind(project) {
|
|
469
462
|
const pinned = personalSelectionOf(project)
|
|
470
|
-
|
|
463
|
+
const latest = pinned === null ? null : READ_UP_TO.get(pinned.personalHome)
|
|
464
|
+
return latest !== undefined && latest !== null && (latest.sequence !== pinned.sequence || latest.head !== pinned.head)
|
|
471
465
|
}
|
|
472
466
|
|
|
473
467
|
// Reads where the history of a pinned project's home ends now, for the note alone: what the read refuses is not this
|
|
@@ -498,10 +492,12 @@ export function personalOverlayRepoOf(project) {
|
|
|
498
492
|
}
|
|
499
493
|
|
|
500
494
|
// Settings that a personal binding takes from the overlay or does not have yet. A saved view is authored in the
|
|
501
|
-
// overlay, so `view add` would write a module-owned generation file
|
|
502
|
-
//
|
|
495
|
+
// overlay, so `view add` would write a module-owned generation file. The machine records an owner-only audience or
|
|
496
|
+
// an explicit clear. Custom audiences refuse; vault location remains unavailable for this binding.
|
|
503
497
|
export function refuseOnPersonalBinding(project, operation) {
|
|
504
498
|
if (personalWorkspaceBindingOf(project) === null) return
|
|
499
|
+
if (operation === 'audience set me' || operation === 'audience clear') return
|
|
500
|
+
if (operation.startsWith('audience ')) refuse('personal-binding-owner-only', 'a personal workspace vault is owner only; use `audience set me` or `audience clear`', { operation })
|
|
505
501
|
if (operation === 'view add') refuse('views-from-personal-overlay', 'the views of a personal workspace come from its overlay; nothing was written', { operation })
|
|
506
502
|
refuse('personal-binding-decision-unavailable', `${operation} is not available for a personal workspace yet; nothing was written`, { operation })
|
|
507
503
|
}
|
|
@@ -38,7 +38,7 @@ import { createHmac } from 'node:crypto'
|
|
|
38
38
|
// and the exact bytes of the answer. A handshake is used once and lapses after
|
|
39
39
|
// five seconds, a session after fifteen minutes. Nothing here takes a path to
|
|
40
40
|
// act on, a command or code, and every command maps to state the service
|
|
41
|
-
// already keeps (see docs/obsidian-plugin.md).
|
|
41
|
+
// already keeps (see docs/reference/obsidian-plugin.md).
|
|
42
42
|
//
|
|
43
43
|
// The plugin cannot import this module: it runs inside the app, from the
|
|
44
44
|
// vault. It carries the same constants and the same computations, and the test
|
|
@@ -16,6 +16,10 @@ import { probeExchange } from './exchange.mjs'
|
|
|
16
16
|
import { CRASH_INJECTION_TEST_SEAM } from './test-seam.mjs'
|
|
17
17
|
import { createDirectAdapter } from './transport.mjs'
|
|
18
18
|
|
|
19
|
+
// The caller's private-state location refusals: once the state root may lie in a declared repository, no further
|
|
20
|
+
// entry is written there.
|
|
21
|
+
const isLocationRefusal = (error) => /^managed-root-|^repository-inside-managed-root$/.test(String(error?.code ?? ''))
|
|
22
|
+
|
|
19
23
|
// publishView: conditional publication of one prepared view into an editable
|
|
20
24
|
// vault. The order is the contract's:
|
|
21
25
|
//
|
|
@@ -380,6 +384,9 @@ export async function publishViewForOracleTests(options = {}, primitives = PUBLI
|
|
|
380
384
|
|
|
381
385
|
const budget = createCooperativeBudget(options.scheduling)
|
|
382
386
|
budget.check()
|
|
387
|
+
// The caller's guard alone, not the cancellation signal: a cancelled publication still records its failure, and so
|
|
388
|
+
// does one the guard stops for another reason (a changed selection or settings). Only a location refusal suppresses it.
|
|
389
|
+
const locationStillProven = () => { try { options.scheduling?.guard?.(); return true } catch (error) { return !isLocationRefusal(error) } }
|
|
383
390
|
const releases = []
|
|
384
391
|
let journal = null
|
|
385
392
|
try {
|
|
@@ -426,9 +433,11 @@ export async function publishViewForOracleTests(options = {}, primitives = PUBLI
|
|
|
426
433
|
// Path selection. An app with this vault open in several windows is refused as such: publishing through one of
|
|
427
434
|
// them would leave the others uncoordinated.
|
|
428
435
|
const probe = await adapter.probe({ vaultRoot: store.vaultRoot })
|
|
436
|
+
budget.check()
|
|
429
437
|
store.checkAllocatedVault?.()
|
|
430
438
|
const unheld = probe.state !== 'coordinated' && probe.state !== 'absent' && await unheldPathApplies({ probe, pointer, store, unheldEvidence, platform })
|
|
431
|
-
// The evidence took time to read: the folder
|
|
439
|
+
// The evidence took time to read: the location and the folder are checked again.
|
|
440
|
+
budget.check()
|
|
432
441
|
if (unheld) store.checkAllocatedVault?.()
|
|
433
442
|
if (probe.state !== 'coordinated' && probe.state !== 'absent' && !unheld) refuse(probe.code === 'vault-open-in-several-windows' ? probe.code : 'editor-uncoordinated', `an Obsidian process may have this vault open and cannot be coordinated with: ${probe.reason}`)
|
|
434
443
|
const mode = probe.state === 'coordinated' ? 'in-app' : unheld ? 'direct-unheld' : 'direct'
|
|
@@ -517,10 +526,14 @@ export async function publishViewForOracleTests(options = {}, primitives = PUBLI
|
|
|
517
526
|
store.checkAllocatedVault?.()
|
|
518
527
|
crash('after-staging')
|
|
519
528
|
|
|
520
|
-
|
|
529
|
+
// `check` runs the caller's guard: every wait for the app resumes through it before the next recovery, journal or
|
|
530
|
+
// vault write.
|
|
531
|
+
const context = { store, journal, journalId, channel, clock, crash, mode, rules, check: () => budget.check() }
|
|
521
532
|
const results = []
|
|
522
533
|
// Kept notes are compared first, several at a time; one that holds exactly its candidate bytes is unchanged.
|
|
523
534
|
const unchanged = await unchangedKeepUnits(units, store)
|
|
535
|
+
// The comparison read the disk: the location is proven again before any unit's recovery, journal or vault write.
|
|
536
|
+
budget.check()
|
|
524
537
|
// The direct path has no editor coordination, so it is only right while no
|
|
525
538
|
// Obsidian runs. Staging takes time and an app may have started since path
|
|
526
539
|
// selection: the process table is read again immediately before the first
|
|
@@ -537,11 +550,14 @@ export async function publishViewForOracleTests(options = {}, primitives = PUBLI
|
|
|
537
550
|
if (mode === 'direct' && (lastProbe === null || Date.now() - lastProbe > 2000)) {
|
|
538
551
|
lastProbe = Date.now()
|
|
539
552
|
const again = await adapter.probe({ vaultRoot: store.vaultRoot })
|
|
553
|
+
budget.check()
|
|
540
554
|
if (again.state !== 'absent') context.uncoordinated = again.reason
|
|
541
555
|
}
|
|
542
556
|
if (mode === 'direct-unheld' && !context.uncoordinated && (lastProbe === null || Date.now() - lastProbe > 2000)) {
|
|
543
557
|
lastProbe = Date.now()
|
|
544
|
-
|
|
558
|
+
const listed = await readsUnlisted(unheldEvidence, store)
|
|
559
|
+
budget.check()
|
|
560
|
+
if (!listed) context.uncoordinated = 'the vault list of the app no longer shows, positively, that no Obsidian lists this vault'
|
|
545
561
|
}
|
|
546
562
|
store.checkAllocatedVault?.()
|
|
547
563
|
if (unit.op === 'keep' && unchanged.has(unit.path)) {
|
|
@@ -569,6 +585,7 @@ export async function publishViewForOracleTests(options = {}, primitives = PUBLI
|
|
|
569
585
|
|
|
570
586
|
// Quiet period, then look at the displaced files again.
|
|
571
587
|
if (quietPeriodMs > 0 && results.some((result) => result.recoveryRef)) await sleep(quietPeriodMs)
|
|
588
|
+
budget.check()
|
|
572
589
|
const lateWriters = recheckDisplacedFiles({ store, journalIds: [journalId], clock })
|
|
573
590
|
for (const finding of lateWriters.filter((item) => item.code === 'late-writer-captured')) {
|
|
574
591
|
journal.append({ step: 'verify', outcome: 'conflict', state: 'verifying', notePath: finding.notePath, beforeDigest: finding.digestAtMove, afterDigest: finding.observedDigest, recoveryRef: finding.displacedRef,
|
|
@@ -595,7 +612,9 @@ export async function publishViewForOracleTests(options = {}, primitives = PUBLI
|
|
|
595
612
|
return { ...base, state: 'committed', lateWriters }
|
|
596
613
|
} catch (error) {
|
|
597
614
|
if (!(error instanceof PublicationRefusal)) {
|
|
598
|
-
|
|
615
|
+
// A private-state location refusal records nothing more there, a failure entry included, and neither does a
|
|
616
|
+
// failure that leaves the location unproven now (a rejected wait for the app). Locks are still released.
|
|
617
|
+
if (journal && !isLocationRefusal(error) && locationStillProven()) try { journal.append({ step: 'verify', outcome: 'failed', state: 'failed', detail: { code: rules.journalFailureCode(error), message: String(error.code ?? error.message).slice(0, 200) } }) } catch { /* the journal itself cannot be written */ }
|
|
599
618
|
throw error
|
|
600
619
|
}
|
|
601
620
|
return { state: 'refused', refusal: { code: error.code, message: error.message, detail: error.detail }, notes: [], retainedEdits: [], lateWriters: [] }
|
|
@@ -772,6 +791,7 @@ async function publishOneUnit(unit, context) {
|
|
|
772
791
|
}
|
|
773
792
|
const baseObject = observe(current)
|
|
774
793
|
const inspected = await context.channel.inspect({ vaultRoot: store.vaultRoot, path: unit.path })
|
|
794
|
+
context.check?.()
|
|
775
795
|
if (inspected.status !== 'inspected') {
|
|
776
796
|
capture('failed', inspected.status, { beforeDigest: currentDigest })
|
|
777
797
|
if (plan.stagedPath) retire(plan, context)
|
|
@@ -790,6 +810,7 @@ async function publishOneUnit(unit, context) {
|
|
|
790
810
|
? { op: 'publish', mode: 'remove', vaultRoot: store.vaultRoot, path: unit.path, operationId, baseSha256: hex(plan.baseDigest), recoveryPath: displaced }
|
|
791
811
|
: { op: 'publish', mode: 'replace', vaultRoot: store.vaultRoot, path: unit.path, operationId, baseSha256: hex(plan.baseDigest), candidateSha256: hex(plan.candidateDigest), stagedPath: plan.stagedPath, recoveryPath: displaced })
|
|
792
812
|
context.crash('after-publish')
|
|
813
|
+
context.check?.()
|
|
793
814
|
|
|
794
815
|
if (reply.status === 'published' || reply.status === 'published-external-captured' || reply.status === 'removed' || reply.status === 'removed-external-captured') {
|
|
795
816
|
syncPrivateDirectory(path.dirname(displaced))
|
|
@@ -3,6 +3,7 @@ import { isDeepStrictEqual } from 'node:util'
|
|
|
3
3
|
import path from 'node:path'
|
|
4
4
|
import { commandProject, readJson, writeJson } from '../project/config.mjs'
|
|
5
5
|
import { summarizeReadinessJourney } from '../readiness-protocols/runtime.mjs'
|
|
6
|
+
import { bundledReadinessEnabled } from '../readiness-protocols/bundled-selection.mjs'
|
|
6
7
|
import { atelierControlStyles } from '../ui/control-system.mjs'
|
|
7
8
|
|
|
8
9
|
const esc = (value) => String(value ?? '').replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>').replaceAll('"', '"')
|
|
@@ -45,11 +46,12 @@ function resolveDistributionBranding(project) {
|
|
|
45
46
|
}
|
|
46
47
|
|
|
47
48
|
export function buildProjectProjection(project) {
|
|
49
|
+
const includeBundled = bundledReadinessEnabled(project)
|
|
48
50
|
const graph = readJson(project.graphPath)
|
|
49
51
|
const branding = resolveDistributionBranding(project)
|
|
50
|
-
const readinessJourney = summarizeReadinessJourney(project)
|
|
52
|
+
const readinessJourney = includeBundled ? summarizeReadinessJourney(project) : null
|
|
51
53
|
const output = path.join(project.outputRoot, 'index.html')
|
|
52
|
-
const dimensionCards = readinessJourney
|
|
54
|
+
const dimensionCards = (readinessJourney?.dimensions ?? []).map((item) => `<article class="readiness-card" data-protocol="${esc(item.protocolId)}">
|
|
53
55
|
<p class="eyebrow">${esc(item.label)}</p>
|
|
54
56
|
<h3>${esc(item.title)}</h3>
|
|
55
57
|
<p><strong>${esc(item.status)}</strong> · ${esc(item.score)} / 100 answers complete</p>
|
|
@@ -90,7 +92,7 @@ ${branding.themeCss}</style>
|
|
|
90
92
|
<p class="eyebrow">${esc(branding.name)} · ${esc(branding.eyebrow)}</p>
|
|
91
93
|
<h1>${esc(project.config.name || 'Project Workspace')}</h1>
|
|
92
94
|
<p>${graph.counts.nodes} graph nodes · ${graph.counts.edges} graph edges · ${graph.counts.diagnostics} diagnostics</p>
|
|
93
|
-
|
|
95
|
+
${readinessJourney ? `<section class="readiness" aria-labelledby="tenant-readiness-title">
|
|
94
96
|
<div class="readiness-head">
|
|
95
97
|
<div>
|
|
96
98
|
<p class="eyebrow">MNSTRY Tenant Readiness</p>
|
|
@@ -101,7 +103,7 @@ ${branding.themeCss}</style>
|
|
|
101
103
|
<div class="readiness-score">${esc(readinessJourney.score)}%</div>
|
|
102
104
|
</div>
|
|
103
105
|
<div class="readiness-grid">${dimensionCards}</div>
|
|
104
|
-
</section
|
|
106
|
+
</section>` : ''}
|
|
105
107
|
<section class="grid">${cards}</section>
|
|
106
108
|
<script>
|
|
107
109
|
document.addEventListener('click', async (event) => {
|
|
@@ -126,16 +128,18 @@ export function buildProjectManifest(project, projection) {
|
|
|
126
128
|
generatedAt: 'deterministic',
|
|
127
129
|
graphPath: path.relative(project.outputRoot, project.graphPath).split(path.sep).join('/'),
|
|
128
130
|
entry: 'index.html',
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
131
|
+
...(projection.readinessJourney ? {
|
|
132
|
+
tenantReadiness: {
|
|
133
|
+
score: projection.readinessJourney.score,
|
|
134
|
+
ready: projection.readinessJourney.ready,
|
|
135
|
+
dimensions: projection.readinessJourney.dimensions.map((item) => ({
|
|
136
|
+
key: item.key,
|
|
137
|
+
protocolId: item.protocolId,
|
|
138
|
+
status: item.status,
|
|
139
|
+
score: item.score,
|
|
140
|
+
})),
|
|
141
|
+
},
|
|
142
|
+
} : {}),
|
|
139
143
|
nodes: projection.graph.nodes.map((node) => ({ id: node.id, title: node.title, audience: node.audience, path: node.path })),
|
|
140
144
|
}
|
|
141
145
|
}
|