diff --git a/build/model.mjs b/build/model.mjs index 18f974e..370e427 100644 --- a/build/model.mjs +++ b/build/model.mjs @@ -21,6 +21,7 @@ import { fileURLToPath } from "node:url"; import { parseInstance, imagesOf } from "companygraph-meta-model/instance"; import { syncImages } from "@robertblust/design/images"; import { readInstance } from "./read.mjs"; +import { readSchemas } from "./schemas.mjs"; const ROOT = path.join(path.dirname(fileURLToPath(import.meta.url)), ".."); const { repo, commit } = JSON.parse(fs.readFileSync(path.join(ROOT, "source.json"), "utf8")); @@ -32,13 +33,14 @@ const SUB = "model/"; // The vendored core sits beside the container, and the parser reads the model against it: // R16 makes the declared type the only thing that decides which fields are edges, so the -// schemas travel with the pages they declare. -const CORE = "meta/core/"; - -const [files, schemas] = await Promise.all([ - readInstance({ repo, commit, sub: SUB }), - readInstance({ repo, commit, sub: CORE }), -]); +// schemas travel with the pages they declare. The packs the model's `.companygraph/manifest.json` +// lists are vendored beside core and read with it (`build/schemas.mjs`), or a pack's folders +// fail R13 as declared by no schema. A file read is the one file at that path, or undefined. +const read = async (sub, o) => { + const got = await readInstance({ repo, commit, sub }); + return o?.file ? got.get("") : got; +}; +const [files, schemas] = await Promise.all([readInstance({ repo, commit, sub: SUB }), readSchemas(read)]); // `repo` travels with the data because the stage draws more than one repository's model and the // file link has to point at the right one. `sub` is where these files sit in the model's own // repository, and the parser needs it: an entity's `path` is what a page turns into a link to diff --git a/build/read.mjs b/build/read.mjs index 56ed5ef..13976c6 100644 --- a/build/read.mjs +++ b/build/read.mjs @@ -18,6 +18,10 @@ function readLocal(dir, commit, sub) { } const root = path.join(dir, sub); const files = new Map(); + // An absent folder is no files, as it is on GitHub, and a path to one file is that file + // under the empty key, as the remote listing gives it. + if (!fs.existsSync(root)) return files; + if (fs.statSync(root).isFile()) return files.set("", fs.readFileSync(root, "utf8")); const walk = (d) => { for (const ent of fs.readdirSync(d, { withFileTypes: true })) { const p = path.join(d, ent.name); diff --git a/build/renderers.test.mjs b/build/renderers.test.mjs index ae526c5..eec6be4 100644 --- a/build/renderers.test.mjs +++ b/build/renderers.test.mjs @@ -322,3 +322,41 @@ test("check mode reports a home page behind the blog index", () => { test("a blog index with no entry is an error, not an empty section", () => { assert.throws(() => writeLatest({}, { root: latestSite("") }), /no entry/); }); + +import { readSchemas } from "./schemas.mjs"; + +// The model's repository as `read(sub, { file })` sees it: a folder read returns the files +// under the prefix with it stripped, a file read returns the text or undefined. +function repoReader(tree) { + return async (sub, { file } = {}) => { + if (file) return tree[sub]; + return new Map(Object.entries(tree).filter(([k]) => k.startsWith(sub)).map(([k, v]) => [k.slice(sub.length), v])); + }; +} + +test("readSchemas carries each pack the manifest lists beside core, under /", async () => { + const read = repoReader({ + ".companygraph/manifest.json": JSON.stringify({ units: "meta", packs: ["landscape"] }), + "meta/core/skill-schema.md": "core skill", + "meta/landscape/system-schema.md": "pack system", + "meta/other/stray-schema.md": "not listed", + "model/skill/x.md": "a page", + }); + assert.deepEqual([...(await readSchemas(read)).keys()].sort(), ["landscape/system-schema.md", "skill-schema.md"]); +}); + +test("readSchemas on a model without packs is core alone, as before", async () => { + for (const manifest of [JSON.stringify({ units: "meta" }), undefined]) { + const read = repoReader({ + ...(manifest ? { ".companygraph/manifest.json": manifest } : {}), + "meta/core/a-schema.md": "a", + "meta/landscape/b-schema.md": "left on disk, not taken", + }); + assert.deepEqual([...(await readSchemas(read)).keys()], ["a-schema.md"]); + } +}); + +test("readSchemas names the manifest and the folder when a listed pack has no files", async () => { + const read = repoReader({ ".companygraph/manifest.json": JSON.stringify({ packs: ["landscape"] }), "meta/core/a-schema.md": "a" }); + await assert.rejects(readSchemas(read), /manifest\.json lists the pack "landscape", but meta\/landscape\/ has no files/); +}); diff --git a/build/schemas.mjs b/build/schemas.mjs new file mode 100644 index 0000000..a3e7f3a --- /dev/null +++ b/build/schemas.mjs @@ -0,0 +1,29 @@ +// The schemas an instance is written against: its core, vendored at `/core/`, and the +// pack folders its `.companygraph/manifest.json` lists, vendored beside it at `//`. +// Core's files travel under bare keys and a pack's under `/`, which is how the +// parser tells them apart (R20) and how meta-model's own `readInstance` hands them over. +// `read(sub)` returns the files under one folder of the instance's repository with that +// prefix stripped, whether from a checkout or from GitHub. +export async function readSchemas(read) { + const where = ".companygraph/manifest.json"; + const text = await read(where, { file: true }); + let manifest = {}; + if (text !== undefined) { + try { manifest = JSON.parse(text); } + catch (e) { throw new Error(`${where} is not valid JSON: ${e.message}`); } + } + const packs = manifest.packs ?? []; + if (!Array.isArray(packs) || packs.some((p) => typeof p !== "string")) + throw new Error(`${where}: "packs" must be an array of pack names, got ${JSON.stringify(packs)}`); + const units = manifest.units ?? "meta"; + const schemas = new Map(await read(`${units}/core/`)); + for (const pack of packs) { + let files; + try { files = await read(`${units}/${pack}/`); } + catch (e) { if (e?.code !== "ENOENT") throw e; files = new Map(); } + if (files.size === 0) + throw new Error(`${where} lists the pack "${pack}", but ${units}/${pack}/ has no files in the instance`); + for (const [file, text] of files) schemas.set(`${pack}/${file}`, text); + } + return schemas; +} diff --git a/id/01a1713f-850d-7445-bc3b-c9601a117304/index.html b/id/01a1713f-850d-7445-bc3b-c9601a117304/index.html new file mode 100644 index 0000000..0d5d2e5 --- /dev/null +++ b/id/01a1713f-850d-7445-bc3b-c9601a117304/index.html @@ -0,0 +1,13 @@ + + + + + +Deployment + + + + +

Go on to Deployment.

+ + diff --git a/id/01a1713f-93a4-77e2-ae7c-46e9dfe5e912/index.html b/id/01a1713f-93a4-77e2-ae7c-46e9dfe5e912/index.html new file mode 100644 index 0000000..8312b83 --- /dev/null +++ b/id/01a1713f-93a4-77e2-ae7c-46e9dfe5e912/index.html @@ -0,0 +1,13 @@ + + + + + +id.blust.ch + + + + +

Go on to id.blust.ch.

+ + diff --git a/model.json b/model.json index 0afdb96..a7cf3da 100644 --- a/model.json +++ b/model.json @@ -1,5 +1,5 @@ { - "commit": "c259512efa40734c3fa9102ef718d0641bfcb9e7", + "commit": "7fd8db9bc43194bf8c01a6cae0498349de9340f5", "core": "0.69.0", "root": "Robert Blust", "rootId": "01a04c85-bc20-7849-9ec1-d877215d70b4", @@ -202,6 +202,18 @@ "owner": null, "singular": false }, + { + "type": "system", + "folder": "systems", + "owner": null, + "singular": false + }, + { + "type": "system-kind", + "folder": "system-kinds", + "owner": null, + "singular": false + }, { "type": "track", "folder": "tracks", @@ -21766,6 +21778,81 @@ "owner": null, "path": "model/surfaces/mcp-registry-listing.md" }, + { + "id": "01a1713f-850d-7445-bc3b-c9601a117304", + "address": "system-kinds/deployment", + "type": "system-kind", + "name": "Deployment", + "tagline": "Software the company deploys from its own repositories to a platform it rents, and runs there.", + "fields": { + "id": "01a1713f-850d-7445-bc3b-c9601a117304", + "source": "Local", + "rank": "10", + "element": "application-component" + }, + "sections": [ + { + "heading": "What it means", + "text": "A deployment is software that one of the company's repositories deploys, by that repository's workflows on a merge, to a platform the company rents, whether the company wrote the software or runs it as its maker publishes it. The company holds what the deployment needs to run: its configuration, its secrets and the project it runs in. It is not software a vendor runs that the company only signs in to, and not the platform a deployment runs on.", + "tables": [] + } + ], + "owner": null, + "path": "model/system-kinds/deployment.md" + }, + { + "id": "01a1713f-93a4-77e2-ae7c-46e9dfe5e912", + "address": "systems/id-blust-ch", + "type": "system", + "name": "id.blust.ch", + "tagline": "Signs readers in to the chat on blust.ch, and to the chats on companygraph.io and guestgraph.io, with ZITADEL, the identity service the company runs at https://id.blust.ch in its Google Cloud project blust-ch-mcp in the Zurich region, where accounts are by invitation only, and issues the token from which each chat reads the models a reader may choose.", + "fields": { + "id": "01a1713f-93a4-77e2-ae7c-46e9dfe5e912", + "source": "Local", + "kind": "Deployment", + "owner": "Owner", + "processor": "Google Cloud", + "serves": [ + "Answering" + ] + }, + "sections": [ + { + "heading": "References", + "text": "", + "tables": [ + { + "caption": null, + "columns": [ + "What", + "URL" + ], + "rows": [ + [ + "The service's source, infrastructure and the owner's steps", + "https://github.com/robertblust/id-blust-ch" + ] + ] + } + ], + "table": { + "caption": null, + "columns": [ + "What", + "URL" + ], + "rows": [ + [ + "The service's source, infrastructure and the owner's steps", + "https://github.com/robertblust/id-blust-ch" + ] + ] + } + } + ], + "owner": null, + "path": "model/systems/id-blust-ch.md" + }, { "id": "01a04d2d-0070-7801-8563-bbc8d6eed1d0", "address": "values/build-the-alternative-before-making-the-point", @@ -33571,6 +33658,42 @@ "to": "01a0c9f6-44b8-757d-a3e5-7401751adee7", "via": "surfaces", "attrs": {} + }, + { + "from": "01a1713f-850d-7445-bc3b-c9601a117304", + "to": "01a03d8e-dd00-7372-ac62-ba79f12bf81a", + "via": "source", + "attrs": {} + }, + { + "from": "01a1713f-93a4-77e2-ae7c-46e9dfe5e912", + "to": "01a1713f-850d-7445-bc3b-c9601a117304", + "via": "kind", + "attrs": {} + }, + { + "from": "01a1713f-93a4-77e2-ae7c-46e9dfe5e912", + "to": "01a0a6f1-f188-70b0-9317-126308ad1e41", + "via": "owner", + "attrs": {} + }, + { + "from": "01a1713f-93a4-77e2-ae7c-46e9dfe5e912", + "to": "01a108dc-de29-78aa-9804-a6150ebfabad", + "via": "processor", + "attrs": {} + }, + { + "from": "01a1713f-93a4-77e2-ae7c-46e9dfe5e912", + "to": "01a0ccd4-b070-7ae5-9228-5189a9306e5a", + "via": "serves", + "attrs": {} + }, + { + "from": "01a1713f-93a4-77e2-ae7c-46e9dfe5e912", + "to": "01a03d8e-dd00-7372-ac62-ba79f12bf81a", + "via": "source", + "attrs": {} } ], "repo": "robertblust/mental-model" diff --git a/model/og.png b/model/og.png index 47fce31..0d2c77d 100644 Binary files a/model/og.png and b/model/og.png differ diff --git a/model/og.sha b/model/og.sha index c603d42..e47476e 100644 --- a/model/og.sha +++ b/model/og.sha @@ -1 +1 @@ -ebc452621cd2a289a901344ba89207fed58c964eab21e17fe2b85984a25e5537 +b2c1fd3af566aa9eb5c4ab3efcc754783bbce38eb585f48a5cca511759411b68 diff --git a/principles/index.html b/principles/index.html index 8f60521..98148a2 100644 --- a/principles/index.html +++ b/principles/index.html @@ -337,7 +337,7 @@

Production is the finish line

-

Generated from robertblust/mental-model@c259512 — model/vision.md and model/values/. That repository is an instance of CompanyGraph, a meta-model for describing a company as a graph of Markdown.

+

Generated from robertblust/mental-model@7fd8db9 — model/vision.md and model/values/. That repository is an instance of CompanyGraph, a meta-model for describing a company as a graph of Markdown.

diff --git a/principles/og.sha b/principles/og.sha index d763f29..c6b639a 100644 --- a/principles/og.sha +++ b/principles/og.sha @@ -1 +1 @@ -e3b6dce5253245bf0f5378fc7a0df039a7c9680e51410d0b7ba5a55588a254d6 +99f3376a98c9aeff81473d3b57abbdca25d6f40b6f6d9e70f224cdb19f5a28a0 diff --git a/privacy/index.html b/privacy/index.html index 6b4e585..a2bcbb1 100644 --- a/privacy/index.html +++ b/privacy/index.html @@ -335,7 +335,7 @@

Where your data goes

-
On blust.ch
Your visit
@c259512
+
On blust.ch
Your visit
@7fd8db9
  • Kept in your browseruntil you clear it · never sent on its own
    diff --git a/privacy/og.sha b/privacy/og.sha index 661c7a8..48d6e65 100644 --- a/privacy/og.sha +++ b/privacy/og.sha @@ -1 +1 @@ -a97c64b659f599c39c237d41abcbbf99dcf5fee8b55430c30328914aa1fb06b6 +60f5ce748f40a0bfed6492e636b43d053cee8ad739ca81849da5c693973c5041 diff --git a/processes/og.sha b/processes/og.sha index 1dd339d..331cc7a 100644 --- a/processes/og.sha +++ b/processes/og.sha @@ -1 +1 @@ -7e878999753343b538b1c1b25450aa72e7f5fbfa67af349c44c93f649e1dd301 +0e179f115321f31c3c8d006f3092240f569cd9e06acc2b73c889e6134deb674c diff --git a/source.json b/source.json index bb44f36..09e8ac4 100644 --- a/source.json +++ b/source.json @@ -1 +1 @@ -{"repo":"robertblust/mental-model","commit":"c259512efa40734c3fa9102ef718d0641bfcb9e7"} \ No newline at end of file +{"repo":"robertblust/mental-model","commit":"7fd8db9bc43194bf8c01a6cae0498349de9340f5"} \ No newline at end of file diff --git a/surfaces/index.html b/surfaces/index.html index 8d23f8e..413a748 100644 --- a/surfaces/index.html +++ b/surfaces/index.html @@ -351,7 +351,7 @@

    -
    The model
    mental-model
    @c259512
    +
    The model
    mental-model
    @7fd8db9
    • The ownerby hand
      diff --git a/surfaces/og.sha b/surfaces/og.sha index 0d8cda2..88f7f89 100644 --- a/surfaces/og.sha +++ b/surfaces/og.sha @@ -1 +1 @@ -7d20294be3f2fc9249ff51498c79df180c6ed5170902b22687aa471a7652a8a6 +a4f9d12c866c4bb50fc2b768c5e18e7d2b3185c69a93a6487a9c527028a31402 diff --git a/timeline/og.sha b/timeline/og.sha index f786908..ab4aef6 100644 --- a/timeline/og.sha +++ b/timeline/og.sha @@ -1 +1 @@ -8caff229862ccce71e15f81bfda399075b5c6ea4f39ad8c36804f65a9a992553 +5907b8dc81e82f99485669f983c520f7f0dbc546e8585ee32af04a794cae4892