naive-framework 0.1.0 → 0.3.0
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/AGENTS.md +2 -1
- package/docs/decisions/decisions.json +42 -0
- package/docs/reference/ROUTES.md +36 -37
- package/evidence/skills/fidelity-ladder-audit/ladder-census.mjs +2 -1
- package/governance/layer.json +2 -1
- package/governance/venture-template/roles/venture-roles.json +62 -0
- package/interface/acting.mjs +83 -0
- package/interface/adapters/claude-code.json +16 -0
- package/interface/adapters/codex.json +14 -0
- package/interface/adapters/cursor.json +16 -0
- package/interface/adapters.mjs +183 -0
- package/interface/api/server.mjs +4 -2
- package/interface/boundaries.mjs +219 -0
- package/interface/brand.mjs +215 -0
- package/interface/cli/freshness.mjs +59 -0
- package/interface/cli/guard.mjs +6 -0
- package/interface/cli/mentura.mjs +82 -11
- package/interface/cli/naive.mjs +44 -14
- package/interface/context.mjs +525 -0
- package/interface/demo/board.mjs +9 -8
- package/interface/demo/decision-harness.mjs +4 -3
- package/interface/dev/setup.mjs +84 -0
- package/interface/gate-hooks.mjs +37 -0
- package/interface/harness/evals.mjs +10 -9
- package/interface/harness/run.mjs +16 -15
- package/interface/instrumentation.mjs +87 -0
- package/interface/kit-boundary.mjs +73 -0
- package/interface/layer.json +8 -2
- package/interface/mcp/server.mjs +47 -7
- package/interface/package-routes.mjs +197 -0
- package/interface/provenance.mjs +28 -4
- package/interface/reads.mjs +174 -0
- package/interface/release-card.mjs +43 -0
- package/interface/router.mjs +110 -36
- package/interface/skills/naive-setup/SKILL.md +71 -0
- package/interface/skills/naive-setup/artifact.html +83 -0
- package/interface/vitals.mjs +116 -0
- package/interface/writes.mjs +190 -0
- package/knowledge/connectors/catalog.mjs +3 -2
- package/knowledge/connectors/filesystem.mjs +10 -5
- package/knowledge/connectors/index.mjs +3 -2
- package/knowledge/freshness.json +85 -0
- package/knowledge/layer.json +11 -3
- package/knowledge/templates/glossary.md +23 -0
- package/operations/layer.json +10 -3
- package/operations/marketing/check-public.mjs +7 -6
- package/operations/marketing/site/plan.mjs +4 -3
- package/operations/marketing/site/presence.mjs +4 -3
- package/operations/templates/marketing-brand.md +46 -0
- package/operations/templates/public-assets.md +39 -0
- package/package.json +3 -1
- package/packages/catalog.json +8 -0
- package/packages/customers/package.json +21 -1
- package/packages/deals/package.json +27 -0
- package/packages/decisions/package.json +12 -1
- package/packages/finance/frames/fund-without-ceding-control.frame.json +177 -0
- package/packages/foundation/package.json +8 -1
- package/packages/fund/package.json +48 -0
- package/packages/marketing/package.json +10 -4
- package/packages/money/package.json +24 -3
- package/packages/validation/package.json +13 -0
- package/platform/README.md +6 -0
- package/platform/adapter.json +15 -0
- package/platform/integrations/remote-image.mjs +79 -0
- package/platform/integrations/rho.mjs +9 -7
- package/platform/kits/README.md +8 -1
- package/platform/kits/agent-upgrade/kit.json +15 -0
- package/platform/kits/agent-upgrade/run.mjs +82 -0
- package/platform/kits/agent-upgrade/stand-in-agent.mjs +32 -0
- package/platform/kits/validation-kickoff/kit.json +29 -0
- package/platform/webhooks/server.mjs +70 -0
- package/purpose/frameworks/idea-map/ideamap.mjs +4 -2
- package/purpose/frameworks/idea-map/route.mjs +5 -4
- package/purpose/frameworks/product-anatomy/anatomy.mjs +2 -1
- package/schema/README.md +1 -0
- package/schema/access.mjs +58 -0
- package/schema/adapter.schema.json +33 -0
- package/schema/add-department.mjs +3 -2
- package/schema/assess-problem.mjs +3 -2
- package/schema/assets.mjs +213 -0
- package/schema/brand.mjs +374 -0
- package/schema/build-release.mjs +61 -0
- package/schema/caching.mjs +506 -0
- package/schema/check-assets.mjs +24 -0
- package/schema/check-brand.mjs +24 -0
- package/schema/check-config.mjs +111 -0
- package/schema/check-data-model.mjs +2 -1
- package/schema/check-data-packages.mjs +5 -4
- package/schema/check-execution.mjs +4 -3
- package/schema/check-nextjs-status.mjs +3 -1
- package/schema/check-people.mjs +84 -0
- package/schema/check-private.mjs +2 -1
- package/schema/check-ready.mjs +6 -5
- package/schema/check-sources.mjs +11 -2
- package/schema/check-where-used.mjs +4 -3
- package/schema/data-model.json +334 -8
- package/schema/deal.schema.json +80 -0
- package/schema/decision-analytics.mjs +2 -1
- package/schema/decisions.mjs +21 -12
- package/schema/examples/frames/judge.mjs +10 -0
- package/schema/examples/venture-type/jev.mjs +12 -0
- package/schema/examples/workspace/ventures/acme-labs/AGENTS.md +17 -0
- package/schema/examples/workspace/ventures/acme-labs/accounts/.gitkeep +0 -0
- package/schema/examples/workspace/ventures/acme-labs/decisions/discovery-goal.decision.json +23 -0
- package/schema/examples/workspace/ventures/acme-labs/decisions/statement-icp.decision.json +22 -0
- package/schema/examples/workspace/ventures/acme-labs/decisions/statement-problem.decision.json +22 -0
- package/schema/examples/workspace/ventures/acme-labs/decisions/venture.team.decision.json +37 -0
- package/schema/examples/workspace/ventures/acme-labs/knowledge/README.md +3 -0
- package/schema/examples/workspace/ventures/acme-labs/knowledge/sources.json +46 -0
- package/schema/examples/workspace/ventures/acme-labs/money/30000000-0000-4000-8000-000000000001.json +12 -0
- package/schema/examples/workspace/ventures/acme-labs/money/30000000-0000-4000-8000-000000000002.json +12 -0
- package/schema/examples/workspace/ventures/acme-labs/money/30000000-0000-4000-8000-000000000003.json +12 -0
- package/schema/examples/workspace/ventures/acme-labs/money/30000000-0000-4000-8000-000000000004.json +12 -0
- package/schema/examples/workspace/ventures/acme-labs/money/30000000-0000-4000-8000-000000000005.json +12 -0
- package/schema/examples/workspace/ventures/acme-labs/money/30000000-0000-4000-8000-000000000006.json +12 -0
- package/schema/examples/workspace/ventures/acme-labs/money/30000000-0000-4000-8000-000000000007.json +12 -0
- package/schema/examples/workspace/ventures/acme-labs/money/30000000-0000-4000-8000-000000000008.json +12 -0
- package/schema/examples/workspace/ventures/acme-labs/money/30000000-0000-4000-8000-000000000009.json +12 -0
- package/schema/examples/workspace/ventures/acme-labs/money/30000000-0000-4000-8000-000000000010.json +12 -0
- package/schema/examples/workspace/ventures/acme-labs/money/30000000-0000-4000-8000-000000000011.json +12 -0
- package/schema/examples/workspace/ventures/acme-labs/money/30000000-0000-4000-8000-000000000012.json +12 -0
- package/schema/examples/workspace/ventures/acme-labs/money/30000000-0000-4000-8000-000000000013.json +12 -0
- package/schema/examples/workspace/ventures/acme-labs/money/30000000-0000-4000-8000-000000000014.json +12 -0
- package/schema/examples/workspace/ventures/acme-labs/money/_streams/transactions/stream.json +5 -0
- package/schema/examples/workspace/ventures/acme-labs/money/_summary.json +30 -0
- package/schema/examples/workspace/ventures/acme-labs/operations/departments/customer-service/README.md +5 -0
- package/schema/examples/workspace/ventures/acme-labs/operations/departments/customer-service/department.json +5 -0
- package/schema/examples/workspace/ventures/acme-labs/operations/departments/finance/README.md +5 -0
- package/schema/examples/workspace/ventures/acme-labs/operations/departments/finance/department.json +5 -0
- package/schema/examples/workspace/ventures/acme-labs/operations/departments/human-capital/README.md +5 -0
- package/schema/examples/workspace/ventures/acme-labs/operations/departments/human-capital/department.json +5 -0
- package/schema/examples/workspace/ventures/acme-labs/operations/departments/it/README.md +5 -0
- package/schema/examples/workspace/ventures/acme-labs/operations/departments/it/department.json +5 -0
- package/schema/examples/workspace/ventures/acme-labs/operations/departments/marketing-and-sales/README.md +5 -0
- package/schema/examples/workspace/ventures/acme-labs/operations/departments/marketing-and-sales/department.json +5 -0
- package/schema/examples/workspace/ventures/acme-labs/operations/departments/products/README.md +5 -0
- package/schema/examples/workspace/ventures/acme-labs/operations/departments/products/department.json +5 -0
- package/schema/examples/workspace/ventures/acme-labs/operations/departments/strategy/README.md +5 -0
- package/schema/examples/workspace/ventures/acme-labs/operations/departments/strategy/department.json +5 -0
- package/schema/examples/workspace/ventures/acme-labs/people/founders/maya-chen.json +15 -0
- package/schema/examples/workspace/ventures/acme-labs/people/people.json +22 -0
- package/schema/examples/workspace/ventures/acme-labs/pillars/brand.md +5 -0
- package/schema/examples/workspace/ventures/acme-labs/pillars/business.md +5 -0
- package/schema/examples/workspace/ventures/acme-labs/pillars/product.md +5 -0
- package/schema/examples/workspace/ventures/acme-labs/products/.gitkeep +0 -0
- package/schema/examples/workspace/ventures/acme-labs/products/decisions/product.core-product.decision.json +28 -0
- package/schema/examples/workspace/ventures/acme-labs/products/decisions/product.exists.decision.json +38 -0
- package/schema/examples/workspace/ventures/acme-labs/products/decisions/product.flagship.decision.json +27 -0
- package/schema/examples/workspace/ventures/acme-labs/profile.md +12 -0
- package/schema/examples/workspace/ventures/acme-labs/provenance.jsonl +12 -0
- package/schema/examples/workspace/ventures/acme-labs/raw/dump.md +6 -0
- package/schema/examples/workspace/ventures/acme-labs/venture.json +27 -0
- package/schema/examples/workspace/ventures/northstar/AGENTS.md +17 -0
- package/schema/examples/workspace/ventures/northstar/accounts/.gitkeep +0 -0
- package/schema/examples/workspace/ventures/northstar/deals/ductline.json +8 -0
- package/schema/examples/workspace/ventures/northstar/deals/kilnworks.json +14 -0
- package/schema/examples/workspace/ventures/northstar/decisions/venture.team.decision.json +31 -0
- package/schema/examples/workspace/ventures/northstar/evidence/README.md +3 -0
- package/schema/examples/workspace/ventures/northstar/fund/decisions/terms.decision.json +27 -0
- package/schema/examples/workspace/ventures/northstar/fund/decisions/terms.first-close.decision.json +27 -0
- package/schema/examples/workspace/ventures/northstar/fund/decisions/thesis.decision.json +26 -0
- package/schema/examples/workspace/ventures/northstar/fund/terms.json +22 -0
- package/schema/examples/workspace/ventures/northstar/fund/thesis.json +22 -0
- package/schema/examples/workspace/ventures/northstar/governance/README.md +3 -0
- package/schema/examples/workspace/ventures/northstar/governance/departments/external-relationships/charter.md +5 -0
- package/schema/examples/workspace/ventures/northstar/governance/departments/external-relationships/department.json +5 -0
- package/schema/examples/workspace/ventures/northstar/governance/departments/finance/charter.md +5 -0
- package/schema/examples/workspace/ventures/northstar/governance/departments/finance/department.json +5 -0
- package/schema/examples/workspace/ventures/northstar/governance/departments/human-capital/charter.md +5 -0
- package/schema/examples/workspace/ventures/northstar/governance/departments/human-capital/department.json +5 -0
- package/schema/examples/workspace/ventures/northstar/governance/departments/risk-and-compliance/charter.md +5 -0
- package/schema/examples/workspace/ventures/northstar/governance/departments/risk-and-compliance/department.json +5 -0
- package/schema/examples/workspace/ventures/northstar/governance/departments/strategy/charter.md +5 -0
- package/schema/examples/workspace/ventures/northstar/governance/departments/strategy/department.json +5 -0
- package/schema/examples/workspace/ventures/northstar/knowledge/README.md +3 -0
- package/schema/examples/workspace/ventures/northstar/operations/README.md +3 -0
- package/schema/examples/workspace/ventures/northstar/operations/departments/external-relationships/README.md +5 -0
- package/schema/examples/workspace/ventures/northstar/operations/departments/external-relationships/department.json +5 -0
- package/schema/examples/workspace/ventures/northstar/operations/departments/finance/README.md +5 -0
- package/schema/examples/workspace/ventures/northstar/operations/departments/finance/department.json +5 -0
- package/schema/examples/workspace/ventures/northstar/operations/departments/human-capital/README.md +5 -0
- package/schema/examples/workspace/ventures/northstar/operations/departments/human-capital/department.json +5 -0
- package/schema/examples/workspace/ventures/northstar/operations/departments/risk-and-compliance/README.md +5 -0
- package/schema/examples/workspace/ventures/northstar/operations/departments/risk-and-compliance/department.json +5 -0
- package/schema/examples/workspace/ventures/northstar/operations/departments/strategy/README.md +5 -0
- package/schema/examples/workspace/ventures/northstar/operations/departments/strategy/department.json +5 -0
- package/schema/examples/workspace/ventures/northstar/people/founders/ana-ruiz.json +13 -0
- package/schema/examples/workspace/ventures/northstar/people/founders/ben-okafor.json +7 -0
- package/schema/examples/workspace/ventures/northstar/people/people.json +27 -0
- package/schema/examples/workspace/ventures/northstar/pillars/brand.md +5 -0
- package/schema/examples/workspace/ventures/northstar/pillars/business.md +5 -0
- package/schema/examples/workspace/ventures/northstar/pillars/product.md +5 -0
- package/schema/examples/workspace/ventures/northstar/portfolio/kilnworks/AGENTS.md +17 -0
- package/schema/examples/workspace/ventures/northstar/portfolio/kilnworks/accounts/.gitkeep +0 -0
- package/schema/examples/workspace/ventures/northstar/portfolio/kilnworks/decisions/venture.team.decision.json +31 -0
- package/schema/examples/workspace/ventures/northstar/portfolio/kilnworks/knowledge/README.md +3 -0
- package/schema/examples/workspace/ventures/northstar/portfolio/kilnworks/operations/departments/customer-service/README.md +5 -0
- package/schema/examples/workspace/ventures/northstar/portfolio/kilnworks/operations/departments/customer-service/department.json +5 -0
- package/schema/examples/workspace/ventures/northstar/portfolio/kilnworks/operations/departments/finance/README.md +5 -0
- package/schema/examples/workspace/ventures/northstar/portfolio/kilnworks/operations/departments/finance/department.json +5 -0
- package/schema/examples/workspace/ventures/northstar/portfolio/kilnworks/operations/departments/human-capital/README.md +5 -0
- package/schema/examples/workspace/ventures/northstar/portfolio/kilnworks/operations/departments/human-capital/department.json +5 -0
- package/schema/examples/workspace/ventures/northstar/portfolio/kilnworks/operations/departments/it/README.md +5 -0
- package/schema/examples/workspace/ventures/northstar/portfolio/kilnworks/operations/departments/it/department.json +5 -0
- package/schema/examples/workspace/ventures/northstar/portfolio/kilnworks/operations/departments/marketing-and-sales/README.md +5 -0
- package/schema/examples/workspace/ventures/northstar/portfolio/kilnworks/operations/departments/marketing-and-sales/department.json +5 -0
- package/schema/examples/workspace/ventures/northstar/portfolio/kilnworks/operations/departments/products/README.md +5 -0
- package/schema/examples/workspace/ventures/northstar/portfolio/kilnworks/operations/departments/products/department.json +5 -0
- package/schema/examples/workspace/ventures/northstar/portfolio/kilnworks/operations/departments/strategy/README.md +5 -0
- package/schema/examples/workspace/ventures/northstar/portfolio/kilnworks/operations/departments/strategy/department.json +5 -0
- package/schema/examples/workspace/ventures/northstar/portfolio/kilnworks/pillars/brand.md +5 -0
- package/schema/examples/workspace/ventures/northstar/portfolio/kilnworks/pillars/business.md +5 -0
- package/schema/examples/workspace/ventures/northstar/portfolio/kilnworks/pillars/product.md +5 -0
- package/schema/examples/workspace/ventures/northstar/portfolio/kilnworks/products/.gitkeep +0 -0
- package/schema/examples/workspace/ventures/northstar/portfolio/kilnworks/products/decisions/product.core-product.decision.json +28 -0
- package/schema/examples/workspace/ventures/northstar/portfolio/kilnworks/products/decisions/product.exists.decision.json +32 -0
- package/schema/examples/workspace/ventures/northstar/portfolio/kilnworks/products/decisions/product.flagship.decision.json +27 -0
- package/schema/examples/workspace/ventures/northstar/portfolio/kilnworks/profile.md +5 -0
- package/schema/examples/workspace/ventures/northstar/portfolio/kilnworks/venture.json +27 -0
- package/schema/examples/workspace/ventures/northstar/products/.gitkeep +0 -0
- package/schema/examples/workspace/ventures/northstar/profile.md +5 -0
- package/schema/examples/workspace/ventures/northstar/provenance.jsonl +3 -0
- package/schema/examples/workspace/ventures/northstar/purpose/README.md +3 -0
- package/schema/examples/workspace/ventures/northstar/venture.json +49 -0
- package/schema/foundation.mjs +8 -7
- package/schema/founder.schema.json +72 -0
- package/schema/frame.schema.json +240 -0
- package/schema/frames.mjs +229 -0
- package/schema/freshness-log.schema.json +40 -0
- package/schema/freshness-profiles.schema.json +44 -0
- package/schema/freshness.mjs +474 -0
- package/schema/fund-terms.schema.json +107 -0
- package/schema/fund-thesis.schema.json +87 -0
- package/schema/gate-drafts.mjs +131 -0
- package/schema/gate.mjs +105 -11
- package/schema/glossary.schema.json +25 -0
- package/schema/init-venture.mjs +69 -21
- package/schema/judgment.schema.json +38 -0
- package/schema/kit.schema.json +47 -0
- package/schema/kits.mjs +24 -0
- package/schema/knowledge-sources.schema.json +145 -15
- package/schema/layer.json +16 -3
- package/schema/marketing-brand.schema.json +116 -0
- package/schema/migrate-venture.mjs +8 -7
- package/schema/migrations/kit-to-package.mjs +95 -0
- package/schema/migrations/package-layout.mjs +90 -0
- package/schema/model-steps.mjs +718 -0
- package/schema/money-summary.schema.json +138 -0
- package/schema/naive-config.mjs +166 -0
- package/schema/naive-config.schema.json +268 -0
- package/schema/need-now.json +171 -0
- package/schema/need-now.mjs +8 -6
- package/schema/packages.mjs +21 -15
- package/schema/payouts.mjs +3 -2
- package/schema/people.schema.json +85 -0
- package/schema/port-venture.mjs +206 -132
- package/schema/public-assets.schema.json +76 -0
- package/schema/release-read.mjs +112 -0
- package/schema/releases.mjs +498 -0
- package/schema/run-handoff.mjs +5 -4
- package/schema/setup.mjs +165 -0
- package/schema/stale.mjs +112 -0
- package/schema/start-release.mjs +41 -0
- package/schema/structure.mjs +187 -0
- package/schema/systems-catalog.json +126 -0
- package/schema/systems-catalog.schema.json +78 -0
- package/schema/time-to-ready.mjs +7 -6
- package/schema/trigger.schema.json +44 -0
- package/schema/triggers.mjs +238 -0
- package/schema/upgrade-registry.schema.json +60 -0
- package/schema/upgrade-venture.mjs +96 -118
- package/schema/upgrades.json +69 -0
- package/schema/upgrades.mjs +386 -0
- package/schema/validate.py +19 -10
- package/schema/venture-paths.mjs +85 -0
- package/schema/venture-role.schema.json +64 -0
- package/schema/venture-type.mjs +62 -0
- package/schema/venture-types.json +346 -0
- package/schema/venture-types.schema.json +129 -0
- package/schema/venture.schema.json +61 -0
- package/schema/workspace.schema.json +5 -1
package/interface/mcp/server.mjs
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
import { createInterface } from "node:readline";
|
|
12
12
|
import { call, routes, routesMd, ventureDir, ventures } from "../router.mjs";
|
|
13
|
+
import { runStreaming } from "../context.mjs";
|
|
13
14
|
import { propose } from "../../schema/decisions.mjs";
|
|
14
15
|
|
|
15
16
|
const PROTOCOL = "2025-06-18";
|
|
@@ -20,17 +21,42 @@ export const TOOLS = [
|
|
|
20
21
|
{ name: "mentura_layout", description: "The context for a route before you act on it: the system, layer and kit purpose, skills and references, the schema and template, the gate, and what need-now says about the record.", inputSchema: { type: "object", properties: { venture, route }, required: ["route"] } },
|
|
21
22
|
{ name: "mentura_view", description: "A route as a Markdown page for a person. system/overview shows the venture at a glance.", inputSchema: { type: "object", properties: { venture, route }, required: ["venture", "route"] } },
|
|
22
23
|
{ name: "mentura_ventures", description: "The ventures in the workspace, with their phase.", inputSchema: { type: "object", properties: {} } },
|
|
23
|
-
{ name: "mentura_read", description: "Read one record of a venture
|
|
24
|
+
{ name: "mentura_read", description: "Read one record of a venture, header first: the header (what it is, its state, owner and size in tokens), the budget, the layout (in a session, only the parts you have not seen), the template (need-now and open decisions, fresh), the record as result (null when the venture has no such record yet), and prefetch: short summaries of the routes you are likely to need next. Pass header_only to stop after the header. A list returns only the items your role may read.", inputSchema: { type: "object", properties: { venture, route,
|
|
25
|
+
session: { type: "string", description: "Your session id: the layout is sent once per session, then only the parts that changed" },
|
|
26
|
+
task: { type: "string", description: "What you are doing; a route it names is prefetched in full" },
|
|
27
|
+
prefetch: { description: "false turns prefetch off; a list of routes prefetches those in full", oneOf: [{ type: "boolean" }, { type: "array", items: { type: "string" } }] },
|
|
28
|
+
header_only: { type: "boolean", description: "Return only the header, to decide whether to read on" },
|
|
29
|
+
where: { type: "object", description: "For a list: keep only records whose fields match these values" },
|
|
30
|
+
agent_id: { type: "string", description: "Your role in the team plan, so its read routes apply" } }, required: ["venture", "route"] } },
|
|
24
31
|
{ name: "mentura_write", description: "Write one record of a venture. The record must match its schema. A gated record is written as a draft under _drafts/ for a person to accept. Every write is logged in the venture's provenance log: say what the content came from and the decision it acts under.", inputSchema: { type: "object", properties: { venture, route, record: { type: "object", description: "The whole record" },
|
|
25
32
|
derived_from: { type: "array", items: { type: "string" }, description: "What the content came from: venture-relative paths, URLs or decision ids" },
|
|
26
33
|
decision: { type: "string", description: "The decision the write acts under, such as EE-026" },
|
|
27
34
|
model: { type: "string", description: "Your model, for the provenance log" }, note: { type: "string" },
|
|
28
|
-
agent_id: { type: "string", description: "Your role in the team plan, so its routes and unlocks apply" }
|
|
35
|
+
agent_id: { type: "string", description: "Your role in the team plan, so its routes and unlocks apply" },
|
|
36
|
+
// Gate group hook (X-4): a score computed before the write; the gate's confidence rules read it and never compute one.
|
|
37
|
+
confidence: { type: "number", description: "Your confidence in this record, 0 to 1, if you computed one. Below the venture's threshold, a gate rule may send the write to its function owner" } }, required: ["venture", "route", "record"] } },
|
|
29
38
|
{ name: "mentura_decisions", description: "The decisions a venture owes (EE-054): each with its parent, boundary, phase, due time and whether it renders now, is overdue, waits, comes later, is stale or deferred. Work on what renders now or overdue; a decision that waits is held by an open fail-closed decision above it. A route a pending decision unlocks is refused until a person decides it.", inputSchema: { type: "object", properties: { venture,
|
|
30
39
|
render: { type: "array", items: { type: "string" }, description: "Only these states, such as [\"overdue\", \"now\"]" } }, required: ["venture"] } },
|
|
31
40
|
{ name: "mentura_propose_decision", description: "Propose a decision, or new wording for an open one. It stays open until a person approves it in the CLI (mentura decide); you can never mark a decision decided. Put it beside what it decides with at (a venture folder such as operations) and name (<record>[.<topic>]).", inputSchema: { type: "object", properties: { venture,
|
|
32
41
|
decision: { type: "object", description: "id, question, and optionally answer (your proposed wording), decides, layer, parent, needed_in, due, outcomes, unlocks, note, plus at and name for where it goes" },
|
|
33
42
|
agent_id: { type: "string", description: "Your role in the team plan" }, model: { type: "string" } }, required: ["venture", "decision"] } },
|
|
43
|
+
// Setup (EE-083, interface/skills/naive-setup): the questions that establish a new venture, in order, and an artifact of the progress.
|
|
44
|
+
{ name: "mentura_setup", description: "Where a new venture's setup stands: its people and hats, the source of truth for each function, the foundation and first decisions (the docket), the goal for discovery, and which steps are done. artifact is that state as one self-contained HTML page: render it as an artifact where your harness has artifacts, and again after every answer. Ask the founder the next open question, one at a time, in order.", inputSchema: { type: "object", properties: { venture }, required: ["venture"] } },
|
|
45
|
+
{ name: "mentura_setup_answer", description: "Write down the founder's answer to one setup question (a docket id from mentura_setup, or discovery-goal), exactly in their words. It is proposed with their words as its wording and stays open until the founder confirms it at the keyboard (naive setup confirm): you never decide for them.", inputSchema: { type: "object", properties: { venture,
|
|
46
|
+
id: { type: "string", description: "The question's id from mentura_setup's docket, or discovery-goal" },
|
|
47
|
+
answer: { type: "string", description: "The founder's answer, verbatim" },
|
|
48
|
+
agent_id: { type: "string", description: "Your role in the team plan" }, model: { type: "string" } }, required: ["venture", "id", "answer"] } },
|
|
49
|
+
{ name: "mentura_setup_point", description: "Point one function of the venture at the system that holds its truth (from mentura_setup's catalog), as the founder said, such as finance at rho or products at github with address acme/app. Naive keeps a pointer, never a copy; Rho is synced at once.", inputSchema: { type: "object", properties: { venture,
|
|
50
|
+
function: { type: "string", description: "A function, such as products, finance or legal" },
|
|
51
|
+
system: { type: "string", description: "A system the catalog offers for that function" },
|
|
52
|
+
address: { type: "string", description: "Where in that system, such as acme/app" },
|
|
53
|
+
agent_id: { type: "string", description: "Your role in the team plan" } }, required: ["venture", "function", "system"] } },
|
|
54
|
+
// The decision library (schema/frames.mjs): a founder's problem matched to a frame, its premises judged, its decisions proposed.
|
|
55
|
+
{ name: "mentura_frame", description: "When the founder states a problem in their own words (\"I need investors to scale but I don't want to give up control\"), match it to a frame in the decision library, judge the premises their sentence assumes against the venture's records, and propose the decisions hidden in it, open, with options. The result says which premises hold, whether the trade is false, real or a sequence, and the better question to ask. Never pick an option for the founder, and never name an instrument as the answer: options for how to do something come from a dated, cited lookup.", inputSchema: { type: "object", properties: { venture,
|
|
56
|
+
text: { type: "string", description: "The founder's words, verbatim" },
|
|
57
|
+
frame: { type: "string", description: "A frame id, when you already know it; otherwise the founder's words pick one" },
|
|
58
|
+
lookup: { type: "object", description: "Options a lookup found: { options: [{ option, source, as_of }] }; only dated, cited options within the frame's freshness are kept" },
|
|
59
|
+
agent_id: { type: "string", description: "Your role in the team plan" }, model: { type: "string" } } } },
|
|
34
60
|
{ name: "mentura_check", description: "Run the checks for one record of a venture.", inputSchema: { type: "object", properties: { venture, route }, required: ["venture", "route"] } },
|
|
35
61
|
{ name: "mentura_template", description: "The fill-in template for a record, where its kit has one.", inputSchema: { type: "object", properties: { route }, required: ["route"] } },
|
|
36
62
|
{ name: "mentura_run", description: "Run a command route for a venture: system/need-now (what to work on now), system/time-to-ready, purpose/validation/assess (what the problem's evidence is strong enough for), governance/payouts, knowledge/catalog (index every source the venture keeps records in), knowledge/search (find items in any of them). A result with needs_input lists what is missing: ask the person whether to fill it in or run as is, and only then call again with as_is.", inputSchema: { type: "object", properties: { venture, route, value: { type: ["number", "string"], description: "governance/payouts: a sale value; knowledge/search: the query" },
|
|
@@ -40,7 +66,8 @@ export const TOOLS = [
|
|
|
40
66
|
|
|
41
67
|
let client = null; // the harness, as it named itself at initialize
|
|
42
68
|
|
|
43
|
-
|
|
69
|
+
// notify sends a notification to the client; progress for a call that carries _meta.progressToken (N-5).
|
|
70
|
+
export async function handle(msg, notify = () => {}) {
|
|
44
71
|
const { id, method, params = {} } = msg;
|
|
45
72
|
const reply = (result) => ({ jsonrpc: "2.0", id, result });
|
|
46
73
|
const fail = (code, message) => ({ jsonrpc: "2.0", id, error: { code, message } });
|
|
@@ -54,15 +81,26 @@ export async function handle(msg) {
|
|
|
54
81
|
case "tools/list": return reply({ tools: TOOLS });
|
|
55
82
|
case "tools/call": {
|
|
56
83
|
const a = params.arguments ?? {};
|
|
84
|
+
const token = params._meta?.progressToken;
|
|
85
|
+
let step = 0;
|
|
86
|
+
const progress = token === undefined ? null : ({ progress: p, total, message }) => notify({ jsonrpc: "2.0", method: "notifications/progress", params: { progressToken: token, progress: Math.max(p ?? 0, ++step), ...(total ? { total } : {}), message } });
|
|
57
87
|
let r;
|
|
58
88
|
switch (params.name) {
|
|
59
89
|
case "mentura_routes": r = { ok: true, result: a.group ? routes().filter((x) => x.groups.includes(`(${a.group})`)) : routes() }; break;
|
|
60
90
|
case "mentura_ventures": r = { ok: true, result: ventures().map(({ id, name, phase, path }) => ({ id, name, phase, path })) }; break;
|
|
61
91
|
case "mentura_layout": r = await call({ venture: a.venture, route: a.route, action: "layout" }); break;
|
|
62
92
|
case "mentura_view": r = await call({ venture: a.venture, route: a.route, action: "view" }); break;
|
|
63
|
-
|
|
93
|
+
// Context hook (interface/context.mjs): a read is a navigation, header first, its parts streamed as progress.
|
|
94
|
+
// A filtered list (where) is a plain read (interface/writes.mjs); anything else is a navigation.
|
|
95
|
+
case "mentura_read": r = a.where ? await call({ venture: a.venture, route: a.route, action: "read", actor_id: a.agent_id, via: "mcp", input: { where: a.where } }) : await call({ venture: a.venture, route: a.route, action: "navigate", actor_id: a.agent_id, via: "mcp",
|
|
96
|
+
input: { session: a.session, task: a.task, prefetch: a.prefetch, header_only: a.header_only, ...(progress ? { onPart: ({ part, data }) => progress({ message: `${part}: ${JSON.stringify(data)}` }) } : {}) } }); break;
|
|
64
97
|
case "mentura_write": r = await call({ venture: a.venture, route: a.route, action: "write", input: a.record, actor: "agent", actor_id: a.agent_id, via: "mcp",
|
|
65
|
-
provenance: { derived_from: a.derived_from, decision: a.decision, note: a.note, agent: Object.fromEntries(Object.entries({ harness: client, model: a.model }).filter(([, v]) => v)) } }); break;
|
|
98
|
+
provenance: { derived_from: a.derived_from, decision: a.decision, note: a.note, confidence: a.confidence, agent: Object.fromEntries(Object.entries({ harness: client, model: a.model }).filter(([, v]) => v)) } }); break;
|
|
99
|
+
// Setup (EE-083): schema/setup.mjs holds the questions; answers are proposals the founder confirms.
|
|
100
|
+
case "mentura_setup": { const m = await import("../../schema/setup.mjs"); const dir = ventureDir(a.venture); r = { ok: true, result: { state: await m.state(dir), artifact: await m.artifact(dir) } }; break; }
|
|
101
|
+
case "mentura_setup_answer": r = await (await import("../../schema/setup.mjs")).relay(ventureDir(a.venture), { id: a.id, answer: a.answer, agentId: a.agent_id, harness: client, model: a.model }); break;
|
|
102
|
+
case "mentura_setup_point": r = await (await import("../../schema/setup.mjs")).point(ventureDir(a.venture), { fn: a.function, system: a.system, address: a.address, via: "mcp", actor: "agent", agentId: a.agent_id }); break;
|
|
103
|
+
case "mentura_frame": r = await (await import("../../schema/frames.mjs")).run(ventureDir(a.venture), { id: a.frame, text: a.text, lookup: a.lookup, agentId: a.agent_id ?? "frames", harness: client, model: a.model }); break;
|
|
66
104
|
case "mentura_decisions": {
|
|
67
105
|
r = await call({ venture: a.venture, route: "governance/decisions", action: "run", actor_id: a.agent_id });
|
|
68
106
|
if (r.ok && a.render?.length) r.result = { ...r.result, decisions: r.result.decisions.filter((d) => a.render.includes(d.render)) };
|
|
@@ -75,7 +113,9 @@ export async function handle(msg) {
|
|
|
75
113
|
}
|
|
76
114
|
case "mentura_check": r = await call({ venture: a.venture, route: a.route, action: "check" }); break;
|
|
77
115
|
case "mentura_template": r = await call({ route: a.route, action: "template" }); break;
|
|
78
|
-
case "mentura_run": r =
|
|
116
|
+
case "mentura_run": r = progress && a.route !== "system/overview" && a.route !== "system/robots"
|
|
117
|
+
? await runStreaming({ venture: a.venture, route: a.route, input: { value: a.value, as_is: a.as_is }, actor_id: a.agent_id }, progress)
|
|
118
|
+
: await call({ venture: a.venture, route: a.route, action: "run", actor_id: a.agent_id, input: a.value !== undefined || a.as_is ? { ...(a.value !== undefined ? { value: a.value } : {}), ...(a.as_is ? { as_is: true } : {}) } : undefined }); break;
|
|
79
119
|
default: return fail(-32602, `unknown tool ${params.name}`);
|
|
80
120
|
}
|
|
81
121
|
if (r.ok && r.needs_input) return reply({ content: [{ type: "text", text: `Ask the person before running. ${r.needs_input.ask}\n${JSON.stringify(r.needs_input, null, 2)}` }], isError: false });
|
|
@@ -115,7 +155,7 @@ if (import.meta.url === `file://${process.argv[1]}`) {
|
|
|
115
155
|
let msg;
|
|
116
156
|
try { msg = JSON.parse(line); } catch { process.stdout.write(JSON.stringify({ jsonrpc: "2.0", id: null, error: { code: -32700, message: "parse error" } }) + "\n"); return; }
|
|
117
157
|
let res;
|
|
118
|
-
try { res = await handle(msg); } catch (e) { res = { jsonrpc: "2.0", id: msg.id ?? null, error: { code: -32603, message: String(e.message ?? e) } }; }
|
|
158
|
+
try { res = await handle(msg, (n) => process.stdout.write(JSON.stringify(n) + "\n")); } catch (e) { res = { jsonrpc: "2.0", id: msg.id ?? null, error: { code: -32603, message: String(e.message ?? e) } }; }
|
|
119
159
|
if (res) process.stdout.write(JSON.stringify(res) + "\n");
|
|
120
160
|
});
|
|
121
161
|
});
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
// Package routes, route groups and slot defaults (Next.js translation L-6, P-17, P-36, P-37, and the
|
|
2
|
+
// venture route for P-3). The router reaches these when a route is not in the route table, so the
|
|
3
|
+
// table, generated from the data model, stays as it is.
|
|
4
|
+
//
|
|
5
|
+
// <package> a package's own route (validation, foundation, marketing; packages/<name> works
|
|
6
|
+
// for every package, even one whose name is also a record route such as product).
|
|
7
|
+
// read returns its records and their state: progress, what moves now, what waits,
|
|
8
|
+
// and the drafts pending in it (its loading state, P-37). check runs its boundary
|
|
9
|
+
// (E-5). view is the same as a page. layout is its group's and its own context.
|
|
10
|
+
// (<group>) a route group (P-36): packages that share a layout, so they share context. The
|
|
11
|
+
// framework defines (go-to-market) for marketing, relationships and customers; a
|
|
12
|
+
// venture adds or changes groups in naive.config.json "groups", and may keep the
|
|
13
|
+
// group's own AGENTS.md in a (<group>)/ folder at its root. Only the packages in a
|
|
14
|
+
// group get its layout: the layout is opt-in.
|
|
15
|
+
// venture the venture's own route: read is the manifest venture.json declares (P-3), check
|
|
16
|
+
// runs every boundary (E-5).
|
|
17
|
+
// @packages the system/overview slot: each package in use, and for a package with nothing in
|
|
18
|
+
// it, its default: what to write first and where to start (P-17).
|
|
19
|
+
|
|
20
|
+
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
21
|
+
import { join } from "node:path";
|
|
22
|
+
import { ventureFile } from "../schema/venture-paths.mjs";
|
|
23
|
+
|
|
24
|
+
const repo = new URL("..", import.meta.url).pathname;
|
|
25
|
+
const load = (f) => JSON.parse(readFileSync(f, "utf8"));
|
|
26
|
+
|
|
27
|
+
/** The framework's route groups (P-36). A venture's naive.config.json "groups" adds to or replaces these. */
|
|
28
|
+
export const GROUPS = {
|
|
29
|
+
"go-to-market": { packages: ["marketing", "relationships", "customers"], context: "Who the venture sells to and how it reaches them. Marketing and sales share the ICP, the offer and the accounts, so they share one layout." },
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export const packageNames = () => existsSync(join(repo, "packages")) ? readdirSync(join(repo, "packages")).filter((n) => existsSync(join(repo, "packages", n, "package.json"))) : [];
|
|
33
|
+
export const packageDef = (n) => load(join(repo, "packages", n, "package.json"));
|
|
34
|
+
|
|
35
|
+
/** The venture's route groups: the framework's, then its own from naive.config.json. */
|
|
36
|
+
export function groupsOf(dir) {
|
|
37
|
+
const f = dir && join(ventureFile(dir, "naive.config.json"));
|
|
38
|
+
const own = f && existsSync(f) ? load(f).groups ?? {} : {};
|
|
39
|
+
return { ...GROUPS, ...own };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** The package a record type lives in: its core home first, else the data or folder package that declares it. */
|
|
43
|
+
export function packageOfRecord(type) {
|
|
44
|
+
if (!type) return null;
|
|
45
|
+
const names = packageNames();
|
|
46
|
+
const core = names.find((n) => { const p = packageDef(n); return p.kind === "core" && [...(p.required ?? []), ...(p.optional ?? [])].includes(type); });
|
|
47
|
+
if (core) return core;
|
|
48
|
+
return names.find((n) => { const p = packageDef(n); return p.kind !== "handoff" && (p.records ?? []).includes(type); }) ?? null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** The group layout a package gets (P-36), or null when it is in no group. */
|
|
52
|
+
export function groupLayout(pkg, dir) {
|
|
53
|
+
const gs = groupsOf(dir);
|
|
54
|
+
const id = Object.keys(gs).find((g) => (gs[g].packages ?? []).includes(pkg));
|
|
55
|
+
if (!id) return null;
|
|
56
|
+
const g = gs[id];
|
|
57
|
+
const agents = dir && existsSync(join(dir, `(${id})`, "AGENTS.md")) ? `(${id})/AGENTS.md` : null;
|
|
58
|
+
const shares = [...new Set(g.packages.filter((n) => packageNames().includes(n)).flatMap((n) => packageDef(n).refs ?? []))];
|
|
59
|
+
return { group: `(${id})`, packages: g.packages, context: g.context ?? null, shares, agents };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Which special route a name is: a package, a group or the venture. */
|
|
63
|
+
export function special(route) {
|
|
64
|
+
if (route === "venture") return { kind: "venture" };
|
|
65
|
+
const g = route.match(/^\(([a-z0-9-]+)\)$/);
|
|
66
|
+
if (g) return { kind: "group", id: g[1] };
|
|
67
|
+
const name = route.startsWith("packages/") ? route.slice("packages/".length) : route;
|
|
68
|
+
return packageNames().includes(name) ? { kind: "package", name } : null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** The route of a record type: its item or single route, else its list route. */
|
|
72
|
+
export const routeOfType = async (type) => {
|
|
73
|
+
const { routes } = await import("./router.mjs");
|
|
74
|
+
const rs = routes().filter((r) => r.record === type);
|
|
75
|
+
return (rs.find((r) => r.collection !== "list") ?? rs[0])?.route ?? null;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
// The drafts waiting in a package's records (P-37): its pending state.
|
|
79
|
+
async function pendingIn(dir, types, venture) {
|
|
80
|
+
const { recordOf } = await import("../schema/decisions.mjs");
|
|
81
|
+
const { entries } = await import("./provenance.mjs");
|
|
82
|
+
const d = join(ventureFile(dir, "_drafts"));
|
|
83
|
+
if (!existsSync(d)) return [];
|
|
84
|
+
const log = entries(dir);
|
|
85
|
+
const out = [];
|
|
86
|
+
for (const f of readdirSync(d).filter((x) => x.endsWith(".draft"))) {
|
|
87
|
+
const file = f.slice(0, -".draft".length).replace(/__/g, "/");
|
|
88
|
+
const type = recordOf(file);
|
|
89
|
+
if (!types.includes(type)) continue;
|
|
90
|
+
const route = await routeOfType(type);
|
|
91
|
+
const id = route?.endsWith("[id]") ? file.split("/").at(-1).replace(/\.json$/, "") : null;
|
|
92
|
+
const concrete = id ? route.replace("[id]", id) : route;
|
|
93
|
+
out.push({ record: type, route: concrete, draft: `_drafts/${f}`, since: log.filter((e) => e.entity === `_drafts/${f}`).at(-1)?.at ?? null, accept: `mentura accept ${concrete} --venture ${venture}` });
|
|
94
|
+
}
|
|
95
|
+
return out;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** A package's own route read (L-6, P-37): its records and their state, what moves now, what waits, what is pending. */
|
|
99
|
+
export async function packageState(dir, name, venture) {
|
|
100
|
+
const def = packageDef(name);
|
|
101
|
+
const { packages } = await import("../schema/packages.mjs");
|
|
102
|
+
const all = packages(dir);
|
|
103
|
+
const p = [...all.core, ...all.handoff].find((x) => x.id === name);
|
|
104
|
+
if (p?.kind === "core") {
|
|
105
|
+
const records = await Promise.all(p.records.map(async (r) => ({ record: r.record, title: r.title, role: r.role, route: await routeOfType(r.record), status: r.status, moves: r.moves, ...(r.waits_on ? { waits_on: r.waits_on } : {}), files: r.files })));
|
|
106
|
+
const pending = await pendingIn(dir, p.records.filter((r) => r.role !== "reads").map((r) => r.record), venture);
|
|
107
|
+
return { package: name, kind: "core", title: p.title, purpose: p.purpose, function: def.function ?? def.department ?? null, fit: p.fit, in: p.in, state: p.state, progress: p.progress,
|
|
108
|
+
records, moves_now: records.filter((r) => r.moves === "now" && r.role !== "reads").map((r) => r.route), waits: records.filter((r) => r.moves === "waits").map((r) => ({ route: r.route, on: r.waits_on })),
|
|
109
|
+
loading: pending.length ? { drafts: pending, note: `${pending.length} draft${pending.length > 1 ? "s" : ""} waiting for a person to accept` } : null, unlocks: p.unlocks, missing: p.missing, to_review: p.to_review };
|
|
110
|
+
}
|
|
111
|
+
if (p?.kind === "handoff") return { package: name, kind: "handoff", title: p.title, for: p.for, from: p.from, fit: p.fit, in: p.in, state: p.state, progress: p.progress, waits_on: p.waits_on, unlocks: p.unlocks, loading: null };
|
|
112
|
+
// Data, folder and raw packages: never complete; their state is what they hold.
|
|
113
|
+
const { routes, glob } = await import("./router.mjs");
|
|
114
|
+
const { files } = await import("./router.mjs");
|
|
115
|
+
const here = files(dir);
|
|
116
|
+
const records = await Promise.all((def.records ?? []).map(async (t) => {
|
|
117
|
+
const r = routes().find((x) => x.record === t);
|
|
118
|
+
const held = r ? here.filter((f) => r.files.some((g) => glob(g).test(f))) : [];
|
|
119
|
+
return { record: t, route: await routeOfType(t), items: held.length, files: held };
|
|
120
|
+
}));
|
|
121
|
+
const pending = await pendingIn(dir, def.records ?? [], venture);
|
|
122
|
+
return { package: name, kind: def.kind, title: def.title, purpose: def.purpose, state: records.some((r) => r.items) ? "holding" : "empty", records, gates: def.gates ?? [],
|
|
123
|
+
loading: pending.length ? { drafts: pending, note: `${pending.length} draft${pending.length > 1 ? "s" : ""} waiting for a person to accept` } : null };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** The overview's @packages slot (P-17): each package in use, and the default a package with nothing in it shows. */
|
|
127
|
+
export async function packageSlots(dir, venture) {
|
|
128
|
+
const { manifest } = await import("../schema/structure.mjs");
|
|
129
|
+
const m = await manifest(dir);
|
|
130
|
+
const out = [];
|
|
131
|
+
for (const { name, known } of m.packages.list) {
|
|
132
|
+
if (!known) { out.push({ package: name, shows: "not a Naive package: check venture.json packages" }); continue; }
|
|
133
|
+
const s = await packageState(dir, name, venture);
|
|
134
|
+
const empty = s.kind === "core" ? s.records.filter((r) => r.role !== "reads").every((r) => r.status === "missing") : s.kind === "handoff" ? false : s.state === "empty";
|
|
135
|
+
if (!empty) { out.push({ package: name, title: s.title, state: s.state, progress: s.progress ?? null, moving: (s.moves_now ?? []).join(", ") || "none" }); continue; }
|
|
136
|
+
// The default: nothing written yet, and where to start.
|
|
137
|
+
const first = s.kind === "core" ? s.records.find((r) => r.role === "required" && r.moves === "now") ?? s.records.find((r) => r.role !== "reads") : s.records[0];
|
|
138
|
+
const route = first?.route ?? null;
|
|
139
|
+
const concrete = route?.endsWith("[id]") ? route.replace("[id]", "<id>") : route;
|
|
140
|
+
const { routes } = await import("./router.mjs");
|
|
141
|
+
const acts = routes().find((x) => x.route === route)?.actions ?? [];
|
|
142
|
+
const write = acts.includes("write") ? `mentura ${concrete} write --venture ${venture} --input <file>` : null;
|
|
143
|
+
const start = !concrete ? null : acts.includes("template") ? `mentura ${concrete} template${write ? `, then ${write}` : ""}` : write ?? `mentura ${concrete} layout --venture ${venture}`;
|
|
144
|
+
out.push({ package: name, title: s.title, default: true, shows: `${s.title}: nothing written yet`, start });
|
|
145
|
+
}
|
|
146
|
+
return out;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const pageOf = (s) => {
|
|
150
|
+
const lines = [`# ${s.title ?? s.package}`, "", s.purpose ?? "", ""];
|
|
151
|
+
if (s.state) lines.push(`State: ${s.state}${s.progress != null ? ` (${Math.round(s.progress * 100)}%)` : ""}.`, "");
|
|
152
|
+
if (s.loading) lines.push(`> ${s.loading.note}: ${s.loading.drafts.map((d) => `\`${d.accept}\``).join(", ")}`, "");
|
|
153
|
+
if (s.records?.length) {
|
|
154
|
+
const cols = s.kind === "core" ? ["route", "role", "status", "moves"] : ["route", "items"];
|
|
155
|
+
lines.push(`| ${cols.join(" | ")} |`, `| ${cols.map(() => "---").join(" | ")} |`, ...s.records.map((r) => `| ${cols.map((c) => String(r[c] ?? "")).join(" | ")} |`), "");
|
|
156
|
+
}
|
|
157
|
+
return lines.join("\n").replace(/\n{3,}/g, "\n\n").trim() + "\n";
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
/** Answers a call to a package, group or venture route; null when the route is none of them. */
|
|
161
|
+
export async function specialCall({ venture, route, action, workspace }) {
|
|
162
|
+
const sp = special(route);
|
|
163
|
+
if (!sp) return null;
|
|
164
|
+
const { ventureDir } = await import("./router.mjs");
|
|
165
|
+
const actions = sp.kind === "group" ? ["read", "view", "layout"] : ["read", "view", "check", "layout"];
|
|
166
|
+
if (!actions.includes(action)) return { ok: false, error: `${route} has no action ${action} (it has ${actions.join(", ")})` };
|
|
167
|
+
const dir = venture ? ventureDir(venture, workspace) : null;
|
|
168
|
+
if (!dir && !(action === "layout" && sp.kind !== "venture")) return { ok: false, error: `no venture ${venture}` };
|
|
169
|
+
if (sp.kind === "venture") {
|
|
170
|
+
const { manifest } = await import("../schema/structure.mjs");
|
|
171
|
+
const { boundaries } = await import("./boundaries.mjs");
|
|
172
|
+
if (action === "check") { const b = await boundaries(dir); return { ok: b.ok, result: b }; }
|
|
173
|
+
const m = await manifest(dir);
|
|
174
|
+
if (action === "view") return { ok: true, result: `# ${m.identity.name}\n\nPhase: ${m.phase}. Packages: ${m.packages.list.map((p) => p.name).join(", ")}.\n\n| Function | Owner | Packages |\n| --- | --- | --- |\n${m.functions.map((f) => `| ${f.function} | ${f.owner ?? "nobody yet"} | ${f.packages.join(", ")} |`).join("\n")}\n` };
|
|
175
|
+
if (action === "layout") return { ok: true, result: { route: "venture", manifest: m } };
|
|
176
|
+
return { ok: true, result: m };
|
|
177
|
+
}
|
|
178
|
+
if (sp.kind === "group") {
|
|
179
|
+
const gs = groupsOf(dir);
|
|
180
|
+
const g = gs[sp.id];
|
|
181
|
+
if (!g) return { ok: false, error: `no route group (${sp.id}): the groups are ${Object.keys(gs).map((x) => `(${x})`).join(", ")}` };
|
|
182
|
+
const lay = groupLayout(g.packages[0], dir);
|
|
183
|
+
if (action === "layout" || !dir) return { ok: true, result: { ...lay, chain: lay.agents && dir ? [{ path: lay.agents, text: readFileSync(ventureFile(dir, lay.agents), "utf8") }] : [] } };
|
|
184
|
+
const states = [];
|
|
185
|
+
for (const n of g.packages.filter((x) => packageNames().includes(x))) states.push(await packageState(dir, n, venture));
|
|
186
|
+
const res = { group: `(${sp.id})`, context: g.context ?? null, packages: states.map((s) => ({ package: s.package, state: s.state, progress: s.progress ?? null, pending: s.loading?.drafts.length ?? 0 })) };
|
|
187
|
+
return { ok: true, result: action === "view" ? `# (${sp.id})\n\n${res.context ?? ""}\n\n${res.packages.map((p) => `- ${p.package}: ${p.state}${p.progress != null ? ` (${Math.round(p.progress * 100)}%)` : ""}${p.pending ? `, ${p.pending} pending` : ""}`).join("\n")}\n` : res };
|
|
188
|
+
}
|
|
189
|
+
const name = sp.name;
|
|
190
|
+
if (action === "layout") {
|
|
191
|
+
const def = packageDef(name);
|
|
192
|
+
return { ok: true, result: { route: name, package: { name, title: def.title, purpose: def.purpose, function: def.function ?? def.department ?? null, skill: def.skill?.path ?? null, gates: def.gates ?? [] }, group: groupLayout(name, dir) } };
|
|
193
|
+
}
|
|
194
|
+
if (action === "check") { const { boundaries } = await import("./boundaries.mjs"); const b = await boundaries(dir, { only: [name] }); return { ok: b.packages.every((p) => p.ok), result: b.packages[0] ?? { package: name, ok: true, note: "not in use by this venture" } }; }
|
|
195
|
+
const s = await packageState(dir, name, venture);
|
|
196
|
+
return action === "view" ? { ok: true, result: pageOf(s) } : { ok: true, result: s, ...(s.loading ? { pending_drafts: s.loading.drafts } : {}) };
|
|
197
|
+
}
|
package/interface/provenance.mjs
CHANGED
|
@@ -5,8 +5,11 @@
|
|
|
5
5
|
// written outside the router.
|
|
6
6
|
|
|
7
7
|
import { createHash } from "node:crypto";
|
|
8
|
-
import {
|
|
8
|
+
import { spawnSync } from "node:child_process";
|
|
9
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
9
10
|
import { join } from "node:path";
|
|
11
|
+
import { emit } from "./instrumentation.mjs";
|
|
12
|
+
import { ventureFile } from "../schema/venture-paths.mjs";
|
|
10
13
|
|
|
11
14
|
export const LOG = "provenance.jsonl";
|
|
12
15
|
const sha = (s) => createHash("sha256").update(s).digest("hex");
|
|
@@ -15,7 +18,7 @@ export const lineHash = (prev, entry) => { const { hash, ...rest } = entry; retu
|
|
|
15
18
|
export const contentHash = (file) => sha(readFileSync(file));
|
|
16
19
|
|
|
17
20
|
export function entries(dir) {
|
|
18
|
-
const f =
|
|
21
|
+
const f = ventureFile(dir, LOG);
|
|
19
22
|
return existsSync(f) ? readFileSync(f, "utf8").split("\n").filter((l) => l.trim()).map((l) => JSON.parse(l)) : [];
|
|
20
23
|
}
|
|
21
24
|
|
|
@@ -35,8 +38,29 @@ export function record(dir, { entity, route, activity, agent, derived_from, deci
|
|
|
35
38
|
const prior = entries(dir);
|
|
36
39
|
const prev = prior.length ? prior.at(-1).hash : "genesis";
|
|
37
40
|
const entry = Object.fromEntries(Object.entries({ at: new Date().toISOString(), entity, route, activity, agent,
|
|
38
|
-
derived_from: derived_from?.length ? derived_from : undefined, decision, note, source, statement, sha256: sha256 ?? contentHash(
|
|
41
|
+
derived_from: derived_from?.length ? derived_from : undefined, decision, note, source, statement, sha256: sha256 ?? contentHash(ventureFile(dir, entity)), prev }).filter(([, v]) => v !== undefined));
|
|
39
42
|
entry.hash = lineHash(prev, entry);
|
|
40
|
-
appendFileSync(
|
|
43
|
+
appendFileSync(ventureFile(dir, LOG), JSON.stringify(entry) + "\n");
|
|
44
|
+
// Context hook: every provenance entry (write, draft, accept, confirm, decision) is an instrumentation event (P-4).
|
|
45
|
+
emit(dir, { kind: "provenance", activity: entry.activity, entity: entry.entity, route: entry.route, decision: entry.decision, by: entry.agent?.kind, via: entry.agent?.via });
|
|
46
|
+
fireTriggers(dir);
|
|
41
47
|
return entry;
|
|
42
48
|
}
|
|
49
|
+
|
|
50
|
+
// Triggers hook (M-12, E-7): every entry is an event. A venture whose naive.config.json has triggers or
|
|
51
|
+
// kits runs them (schema/triggers.mjs) in a separate process after the write; the runs themselves do
|
|
52
|
+
// not fire this again. A crash of that process is kept for the next run to raise, never lost.
|
|
53
|
+
function fireTriggers(dir) {
|
|
54
|
+
if (process.env.NAIVE_TRIGGERS_ACTIVE) return;
|
|
55
|
+
let cfg;
|
|
56
|
+
try { cfg = JSON.parse(readFileSync(join(ventureFile(dir, "naive.config.json")), "utf8")); } catch { return; }
|
|
57
|
+
if (!cfg?.triggers?.length && !cfg?.kits?.length) return;
|
|
58
|
+
// The write has landed; nothing here may undo it or throw past it.
|
|
59
|
+
try {
|
|
60
|
+
const p = spawnSync(process.execPath, [new URL("../schema/triggers.mjs", import.meta.url).pathname, dir, "--fire", "--quiet"], { encoding: "utf8", env: { ...process.env, NAIVE_TRIGGERS_ACTIVE: "hook" } });
|
|
61
|
+
if (p.status !== 0) {
|
|
62
|
+
mkdirSync(join(dir, ".naive"), { recursive: true });
|
|
63
|
+
appendFileSync(join(dir, ".naive/trigger-errors.jsonl"), JSON.stringify({ at: new Date().toISOString(), status: p.status, error: (p.stderr || p.stdout || String(p.error ?? "")).trim().slice(-2000) }) + "\n");
|
|
64
|
+
}
|
|
65
|
+
} catch (e) { console.error(`triggers did not run after this write: ${e.message}`); }
|
|
66
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
// What a read answers besides the record (Next.js translation L-9, L-10, O-1, O-2, O-3, P-28). The
|
|
2
|
+
// router's wrapper (interface/writes.mjs) adds these to every read and layout. Code computes all of
|
|
3
|
+
// it from the files and the provenance log; no model writes any of it.
|
|
4
|
+
//
|
|
5
|
+
// params L-9 the dynamic segment's value ({ id: "kettle" } for product/kettle), given to the
|
|
6
|
+
// read and to every layout below it: the item's own folder AGENTS.md joins the
|
|
7
|
+
// chain, and "[id]" in an instruction becomes the id, so a product's layout knows
|
|
8
|
+
// which product
|
|
9
|
+
// where L-10 route inputs that change what a read returns: input.where { status, phase,
|
|
10
|
+
// function, state, or any field by dot path }. A list keeps the items that match;
|
|
11
|
+
// a single record that does not match comes back null with filtered_out
|
|
12
|
+
// metadata O-1 the static metadata: the venture's identity from venture.json and the record's
|
|
13
|
+
// own frontmatter (Markdown) or title
|
|
14
|
+
// O-2 derived fields, computed by code from the record: counts, the phase, the
|
|
15
|
+
// function and its owner, and its state (confirmed, agent, draft, unlogged)
|
|
16
|
+
// O-3 the fields the framework adds to every record without being asked: id,
|
|
17
|
+
// created, updated, and who wrote it last (person or agent), from provenance
|
|
18
|
+
// hierarchy P-28 the order the router resolves a read: layout (context), template (fresh
|
|
19
|
+
// context), error (failed checks), loading (pending drafts), not-found (missing
|
|
20
|
+
// record), then page (the record). resolved_by names the level that answered;
|
|
21
|
+
// input.hierarchy returns every level
|
|
22
|
+
|
|
23
|
+
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
24
|
+
import { dirname, join } from "node:path";
|
|
25
|
+
import { entries } from "./provenance.mjs";
|
|
26
|
+
import { ventureFile } from "../schema/venture-paths.mjs";
|
|
27
|
+
|
|
28
|
+
const load = (f) => JSON.parse(readFileSync(f, "utf8"));
|
|
29
|
+
const get = (o, p) => p.split(".").reduce((x, k) => (x == null ? undefined : x[k]), o);
|
|
30
|
+
|
|
31
|
+
/** Simple YAML frontmatter: key: value lines between --- fences. Lists in [a, b] form become arrays. */
|
|
32
|
+
export function frontmatter(text) {
|
|
33
|
+
const m = typeof text === "string" ? text.match(/^---\n([\s\S]*?)\n---\n?/) : null;
|
|
34
|
+
if (!m) return null;
|
|
35
|
+
const out = {};
|
|
36
|
+
for (const line of m[1].split("\n")) {
|
|
37
|
+
const kv = line.match(/^([A-Za-z_][\w-]*)\s*:\s*(.*)$/);
|
|
38
|
+
if (!kv) continue;
|
|
39
|
+
let v = kv[2].trim().replace(/^"(.*)"$/, "$1").replace(/^'(.*)'$/, "$1");
|
|
40
|
+
if (/^\[.*\]$/.test(v)) v = v.slice(1, -1).split(",").map((x) => x.trim()).filter(Boolean);
|
|
41
|
+
out[kv[1]] = v;
|
|
42
|
+
}
|
|
43
|
+
return out;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** A record's state from the log and the drafts folder (O-2). */
|
|
47
|
+
export async function stateOf(dir, file) {
|
|
48
|
+
const { confirmedIn } = await import("../schema/foundation.mjs");
|
|
49
|
+
if (existsSync(join(ventureFile(dir, "_drafts"), `${file.replace(/\//g, "__")}.draft`))) return "draft";
|
|
50
|
+
const es = entries(dir).filter((e) => e.entity === file);
|
|
51
|
+
if (!es.length) return "unlogged";
|
|
52
|
+
return confirmedIn(dir, file, null) ? "confirmed" : "agent";
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** The fields every record gets (O-3): id, created, updated and who wrote it last, from the provenance log, else the file. */
|
|
56
|
+
export function defaults(dir, file, id) {
|
|
57
|
+
const es = entries(dir).filter((e) => e.entity === file);
|
|
58
|
+
if (es.length) {
|
|
59
|
+
const last = es.at(-1);
|
|
60
|
+
return { id, created: es[0].at, updated: last.at, by: { kind: last.agent?.kind ?? null, ...(last.agent?.id ? { id: last.agent.id } : {}), via: last.agent?.via ?? null }, activity: last.activity, writes: es.length, from: "provenance" };
|
|
61
|
+
}
|
|
62
|
+
const st = existsSync(ventureFile(dir, file)) ? statSync(ventureFile(dir, file)) : null;
|
|
63
|
+
return { id, created: st ? new Date(st.birthtimeMs || st.mtimeMs).toISOString() : null, updated: st ? st.mtime.toISOString() : null, by: null, writes: 0, from: "file", note: "written outside the router: mentura baseline adopts it into the log" };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Counts derived from a record (O-2): the length of each top-level list. */
|
|
67
|
+
const countsOf = (v) => v && typeof v === "object" && !Array.isArray(v) ? Object.fromEntries(Object.entries(v).filter(([, x]) => Array.isArray(x)).map(([k, x]) => [k, x.length])) : {};
|
|
68
|
+
|
|
69
|
+
/** The function owning a record and its owner (O-2), from the package that holds it and venture.json's functions. */
|
|
70
|
+
export async function ownerOf(dir, type) {
|
|
71
|
+
const { packageOfRecord, packageDef } = await import("./package-routes.mjs");
|
|
72
|
+
const pkg = packageOfRecord(type);
|
|
73
|
+
const def = pkg ? packageDef(pkg) : null;
|
|
74
|
+
const fn = def?.function ?? def?.department ?? null;
|
|
75
|
+
const v = load(join(dir, "venture.json"));
|
|
76
|
+
const owner = fn ? v.functions?.[fn]?.owner ?? (!(v.members?.length) ? "the founder" : null) : null;
|
|
77
|
+
return { package: pkg, function: fn, owner };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** The metadata a read answers with (O-1, O-2, O-3). items maps each id to its venture-relative file. */
|
|
81
|
+
export async function metadataFor({ dir, r, route, params, result, items }) {
|
|
82
|
+
const v = load(join(dir, "venture.json"));
|
|
83
|
+
const fn = await ownerOf(dir, r.record);
|
|
84
|
+
const one = async (id, file, value) => ({
|
|
85
|
+
...defaults(dir, file, id),
|
|
86
|
+
...(file.endsWith(".md") || file.endsWith(".mdx") ? { frontmatter: frontmatter(value) } : value && typeof value === "object" && (value.title || value.name) ? { title: value.title ?? value.name } : {}),
|
|
87
|
+
derived: { state: await stateOf(dir, file), counts: countsOf(value) },
|
|
88
|
+
});
|
|
89
|
+
const base = {
|
|
90
|
+
venture: { id: v.id, name: v.name, phase: v.phase, status: v.status },
|
|
91
|
+
record: { type: r.record, route, schema: r.schema ?? null, form: r.form },
|
|
92
|
+
derived: { phase: v.phase, function: fn.function, owner: fn.owner, package: fn.package, by: "code" },
|
|
93
|
+
};
|
|
94
|
+
if (r.collection === "list") {
|
|
95
|
+
const per = {};
|
|
96
|
+
for (const [id, file] of Object.entries(items)) per[id] = await one(id, file, result?.[id]);
|
|
97
|
+
return { ...base, derived: { ...base.derived, count: Object.keys(per).length }, items: per };
|
|
98
|
+
}
|
|
99
|
+
const [id, file] = Object.entries(items)[0] ?? [];
|
|
100
|
+
if (!file) return base;
|
|
101
|
+
const own = await one(params.id ?? r.record, file, result);
|
|
102
|
+
return { ...base, ...own, derived: { ...base.derived, ...own.derived } };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// One filter test: a field by dot path on the record, else a derived field.
|
|
106
|
+
function matches(value, meta, where) {
|
|
107
|
+
return Object.entries(where).every(([k, want]) => {
|
|
108
|
+
let have = get(value ?? {}, k);
|
|
109
|
+
if (have === undefined) have = { state: meta?.derived?.state, function: meta?.function, owner: meta?.owner, phase: meta?.phase }[k];
|
|
110
|
+
const wants = Array.isArray(want) ? want.map(String) : [String(want)];
|
|
111
|
+
return Array.isArray(have) ? have.some((x) => wants.includes(String(typeof x === "object" ? x?.id ?? x?.name : x))) : have !== undefined && have !== null && wants.includes(String(have));
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Applies input.where to a read answer (L-10): a list keeps matching items; a record that does not match comes back null. */
|
|
116
|
+
export function filterRead(answer, where, r) {
|
|
117
|
+
if (!where || typeof where !== "object" || !Object.keys(where).length || answer.result == null) return answer;
|
|
118
|
+
const md = answer.metadata ?? {};
|
|
119
|
+
const shared = { function: md.derived?.function, owner: md.derived?.owner, phase: md.venture?.phase };
|
|
120
|
+
if (r.collection === "list") {
|
|
121
|
+
const keep = Object.keys(answer.result).filter((id) => matches(answer.result[id], { ...shared, derived: md.items?.[id]?.derived }, where));
|
|
122
|
+
const pick = (o) => o ? Object.fromEntries(Object.entries(o).filter(([id]) => keep.includes(id))) : o;
|
|
123
|
+
return { ...answer, result: pick(answer.result), sources: pick(answer.sources), ...(md.items ? { metadata: { ...md, items: pick(md.items), derived: { ...md.derived, count: keep.length } } } : {}), filtered: { where, matched: keep.length, of: Object.keys(answer.result).length } };
|
|
124
|
+
}
|
|
125
|
+
if (matches(answer.result, { ...shared, derived: md.derived }, where)) return { ...answer, filtered: { where, matched: 1, of: 1 } };
|
|
126
|
+
return { ...answer, result: null, filtered_out: { where, note: "the record exists but does not match the filter" } };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const parseInstructions = (chain) => {
|
|
130
|
+
const out = {};
|
|
131
|
+
for (const f of chain) for (const line of f.text.split("\n")) { const m = line.match(/^([a-z][a-z0-9_-]*)\s*:\s*(.+)$/); if (m) out[m[1]] = m[2].trim(); }
|
|
132
|
+
return out;
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* The layout with params (L-9) and its group (P-36): the group's AGENTS.md after the venture root's,
|
|
137
|
+
* then the item's own folders' AGENTS.md, nearest last; "[id]" in an instruction becomes the id.
|
|
138
|
+
*/
|
|
139
|
+
export function withParams(layout, { dir, params, file, group }) {
|
|
140
|
+
const chain = [...(layout.chain ?? [])];
|
|
141
|
+
const have = new Set(chain.map((c) => c.path));
|
|
142
|
+
const add = (rel, at = chain.length) => { if (dir && !have.has(rel) && existsSync(ventureFile(dir, rel))) { chain.splice(at, 0, { path: rel, text: readFileSync(ventureFile(dir, rel), "utf8") }); have.add(rel); } };
|
|
143
|
+
if (group?.agents) add(group.agents, chain.findIndex((c) => c.path === "AGENTS.md") + 1);
|
|
144
|
+
if (file) {
|
|
145
|
+
const parts = dirname(file).split("/").filter((p) => p && p !== ".");
|
|
146
|
+
let acc = "";
|
|
147
|
+
for (const p of parts) { acc = acc ? `${acc}/${p}` : p; add(`${acc}/AGENTS.md`); }
|
|
148
|
+
}
|
|
149
|
+
const instructions = parseInstructions(chain);
|
|
150
|
+
if (params?.id) for (const k of Object.keys(instructions)) instructions[k] = instructions[k].replace(/\[id\]/g, params.id);
|
|
151
|
+
return { ...layout, chain, instructions, params: params ?? {}, ...(group ? { group } : {}) };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** The read's hierarchy (P-28), outermost first, and the level that answered. */
|
|
155
|
+
export function hierarchy({ answer, layout }) {
|
|
156
|
+
const levels = [
|
|
157
|
+
{ level: "layout", holds: layout ? { chain: layout.chain.map((c) => c.path), stack: layout.stack?.map((s) => s.name) } : null },
|
|
158
|
+
{ level: "template", holds: layout ? { need_now: layout.need_now ?? null, gate: layout.gate ?? null, note: "recomputed on every action" } : null },
|
|
159
|
+
{ level: "error", caught: !answer.ok || !!answer.errors, holds: !answer.ok ? { error: answer.error, fix: answer.fix ?? null } : answer.errors ?? null },
|
|
160
|
+
{ level: "loading", caught: !!(answer.pending_draft || answer.pending_pull_requests), holds: answer.pending_draft ?? answer.pending_pull_requests ?? null },
|
|
161
|
+
{ level: "not-found", caught: answer.ok && answer.result === null && !!answer.not_found, holds: answer.not_found ?? null },
|
|
162
|
+
{ level: "page", caught: answer.ok && answer.result !== null && answer.result !== undefined, holds: answer.ok && answer.result != null ? "the record" : null },
|
|
163
|
+
];
|
|
164
|
+
return { levels, resolved_by: resolvedBy(answer) };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** The level that answered a read (P-28): error, then loading, then not-found, then page. */
|
|
168
|
+
export function resolvedBy(answer) {
|
|
169
|
+
if (!answer.ok) return "error";
|
|
170
|
+
if (answer.pending_draft || answer.pending_pull_requests) return "loading";
|
|
171
|
+
if (answer.result === null && answer.not_found) return "not-found";
|
|
172
|
+
if (answer.result === null && answer.filtered_out) return "page";
|
|
173
|
+
return "page";
|
|
174
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Draws a release card (P-25, O-7). The card's data is assembled by code from the release's records
|
|
3
|
+
// (schema/releases.mjs cardOf, written to <venture>/.release/card.json); drawing it is the interface's
|
|
4
|
+
// job, so the picture lives here and never in the release. It is a 1200 by 630 SVG, the size link
|
|
5
|
+
// previews use, with the venture's name, phase, headline numbers with their dates, and who confirmed them.
|
|
6
|
+
//
|
|
7
|
+
// node interface/release-card.mjs <venture dir> prints the SVG
|
|
8
|
+
|
|
9
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
import { ventureFile } from "../schema/venture-paths.mjs";
|
|
12
|
+
|
|
13
|
+
const esc = (s) => String(s ?? "").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
14
|
+
const day = (t) => (t ? String(t).slice(0, 10) : "");
|
|
15
|
+
|
|
16
|
+
/** The card as SVG text. */
|
|
17
|
+
export function svg(card) {
|
|
18
|
+
const nums = card.headline.filter((h) => h.label !== "Live holes").slice(0, 4);
|
|
19
|
+
const cell = (h, i) => {
|
|
20
|
+
const x = 60 + i * 275;
|
|
21
|
+
return `<text x="${x}" y="330" font-size="64" font-weight="700" fill="#111">${esc(h.value)}${h.unit ? `<tspan font-size="24"> ${esc(h.unit)}</tspan>` : ""}</text>
|
|
22
|
+
<text x="${x}" y="370" font-size="22" fill="#333">${esc(h.label)}</text>
|
|
23
|
+
<text x="${x}" y="400" font-size="18" fill="#666">as of ${esc(day(h.as_of))}</text>`;
|
|
24
|
+
};
|
|
25
|
+
const by = card.confirmed_by.length ? `Confirmed by ${card.confirmed_by.join(", ")}` : "Nothing confirmed yet";
|
|
26
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="630" viewBox="0 0 1200 630" font-family="system-ui, sans-serif" role="img" aria-label="${esc(card.name)} release card">
|
|
27
|
+
<rect width="1200" height="630" fill="#fafaf7"/>
|
|
28
|
+
<rect x="0" y="0" width="1200" height="12" fill="#1f6f5c"/>
|
|
29
|
+
<text x="60" y="120" font-size="64" font-weight="700" fill="#111">${esc(card.name)}</text>
|
|
30
|
+
<text x="60" y="175" font-size="28" fill="#333">Phase: ${esc(card.phase)}</text>
|
|
31
|
+
${nums.map(cell).join("\n ")}
|
|
32
|
+
<text x="60" y="520" font-size="22" fill="#333">${esc(by)}</text>
|
|
33
|
+
<text x="60" y="560" font-size="18" fill="#666">Release ${esc(card.release)}, built ${esc(day(card.built_at))}</text>
|
|
34
|
+
</svg>
|
|
35
|
+
`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
39
|
+
const dir = process.argv[2];
|
|
40
|
+
const f = dir && join(ventureFile(dir, ".release/card.json"));
|
|
41
|
+
if (!f || !existsSync(f)) { console.error("usage: node interface/release-card.mjs <venture dir> (after node schema/build-release.mjs)"); process.exit(2); }
|
|
42
|
+
process.stdout.write(svg(JSON.parse(readFileSync(f, "utf8"))));
|
|
43
|
+
}
|