naive-framework 0.1.0 → 0.2.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 +27 -0
- package/docs/reference/ROUTES.md +36 -37
- 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 +182 -0
- package/interface/api/server.mjs +4 -2
- package/interface/boundaries.mjs +218 -0
- package/interface/brand.mjs +214 -0
- package/interface/cli/freshness.mjs +59 -0
- package/interface/cli/guard.mjs +6 -0
- package/interface/cli/mentura.mjs +78 -8
- package/interface/cli/naive.mjs +33 -8
- package/interface/context.mjs +524 -0
- package/interface/demo/board.mjs +1 -1
- package/interface/dev/setup.mjs +84 -0
- package/interface/gate-hooks.mjs +36 -0
- package/interface/harness/evals.mjs +3 -3
- package/interface/harness/run.mjs +4 -4
- package/interface/instrumentation.mjs +86 -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 +196 -0
- package/interface/provenance.mjs +24 -1
- package/interface/reads.mjs +173 -0
- package/interface/release-card.mjs +42 -0
- package/interface/router.mjs +101 -28
- package/interface/skills/naive-setup/SKILL.md +71 -0
- package/interface/skills/naive-setup/artifact.html +83 -0
- package/interface/vitals.mjs +115 -0
- package/interface/writes.mjs +190 -0
- 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/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 +7 -6
- 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/schema/README.md +1 -0
- package/schema/access.mjs +57 -0
- package/schema/adapter.schema.json +33 -0
- package/schema/assets.mjs +212 -0
- package/schema/brand.mjs +373 -0
- package/schema/build-release.mjs +61 -0
- package/schema/caching.mjs +505 -0
- package/schema/check-assets.mjs +24 -0
- package/schema/check-brand.mjs +24 -0
- package/schema/check-config.mjs +110 -0
- package/schema/check-nextjs-status.mjs +3 -1
- package/schema/check-people.mjs +70 -0
- package/schema/check-sources.mjs +9 -1
- package/schema/data-model.json +334 -8
- package/schema/deal.schema.json +80 -0
- package/schema/decisions.mjs +7 -1
- 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/founder.schema.json +72 -0
- package/schema/frame.schema.json +240 -0
- package/schema/frames.mjs +228 -0
- package/schema/freshness-log.schema.json +40 -0
- package/schema/freshness-profiles.schema.json +44 -0
- package/schema/freshness.mjs +473 -0
- package/schema/fund-terms.schema.json +107 -0
- package/schema/fund-thesis.schema.json +87 -0
- package/schema/gate-drafts.mjs +130 -0
- package/schema/gate.mjs +105 -11
- package/schema/glossary.schema.json +25 -0
- package/schema/init-venture.mjs +38 -8
- 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/migrations/kit-to-package.mjs +94 -0
- package/schema/migrations/package-layout.mjs +86 -0
- package/schema/model-steps.mjs +717 -0
- package/schema/money-summary.schema.json +138 -0
- package/schema/naive-config.mjs +165 -0
- package/schema/naive-config.schema.json +268 -0
- package/schema/need-now.json +171 -0
- package/schema/need-now.mjs +3 -2
- package/schema/packages.mjs +10 -5
- package/schema/people.schema.json +86 -0
- package/schema/public-assets.schema.json +76 -0
- package/schema/release-read.mjs +111 -0
- package/schema/releases.mjs +497 -0
- package/schema/setup.mjs +164 -0
- package/schema/stale.mjs +111 -0
- package/schema/start-release.mjs +41 -0
- package/schema/structure.mjs +186 -0
- package/schema/systems-catalog.json +126 -0
- package/schema/systems-catalog.schema.json +78 -0
- package/schema/trigger.schema.json +44 -0
- package/schema/triggers.mjs +237 -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 +385 -0
- package/schema/validate.py +19 -10
- 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 +53 -0
package/interface/provenance.mjs
CHANGED
|
@@ -5,8 +5,10 @@
|
|
|
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";
|
|
10
12
|
|
|
11
13
|
export const LOG = "provenance.jsonl";
|
|
12
14
|
const sha = (s) => createHash("sha256").update(s).digest("hex");
|
|
@@ -38,5 +40,26 @@ export function record(dir, { entity, route, activity, agent, derived_from, deci
|
|
|
38
40
|
derived_from: derived_from?.length ? derived_from : undefined, decision, note, source, statement, sha256: sha256 ?? contentHash(join(dir, entity)), prev }).filter(([, v]) => v !== undefined));
|
|
39
41
|
entry.hash = lineHash(prev, entry);
|
|
40
42
|
appendFileSync(join(dir, LOG), JSON.stringify(entry) + "\n");
|
|
43
|
+
// Context hook: every provenance entry (write, draft, accept, confirm, decision) is an instrumentation event (P-4).
|
|
44
|
+
emit(dir, { kind: "provenance", activity: entry.activity, entity: entry.entity, route: entry.route, decision: entry.decision, by: entry.agent?.kind, via: entry.agent?.via });
|
|
45
|
+
fireTriggers(dir);
|
|
41
46
|
return entry;
|
|
42
47
|
}
|
|
48
|
+
|
|
49
|
+
// Triggers hook (M-12, E-7): every entry is an event. A venture whose naive.config.json has triggers or
|
|
50
|
+
// kits runs them (schema/triggers.mjs) in a separate process after the write; the runs themselves do
|
|
51
|
+
// not fire this again. A crash of that process is kept for the next run to raise, never lost.
|
|
52
|
+
function fireTriggers(dir) {
|
|
53
|
+
if (process.env.NAIVE_TRIGGERS_ACTIVE) return;
|
|
54
|
+
let cfg;
|
|
55
|
+
try { cfg = JSON.parse(readFileSync(join(dir, "naive.config.json"), "utf8")); } catch { return; }
|
|
56
|
+
if (!cfg?.triggers?.length && !cfg?.kits?.length) return;
|
|
57
|
+
// The write has landed; nothing here may undo it or throw past it.
|
|
58
|
+
try {
|
|
59
|
+
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" } });
|
|
60
|
+
if (p.status !== 0) {
|
|
61
|
+
mkdirSync(join(dir, ".naive"), { recursive: true });
|
|
62
|
+
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");
|
|
63
|
+
}
|
|
64
|
+
} catch (e) { console.error(`triggers did not run after this write: ${e.message}`); }
|
|
65
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
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
|
+
|
|
27
|
+
const load = (f) => JSON.parse(readFileSync(f, "utf8"));
|
|
28
|
+
const get = (o, p) => p.split(".").reduce((x, k) => (x == null ? undefined : x[k]), o);
|
|
29
|
+
|
|
30
|
+
/** Simple YAML frontmatter: key: value lines between --- fences. Lists in [a, b] form become arrays. */
|
|
31
|
+
export function frontmatter(text) {
|
|
32
|
+
const m = typeof text === "string" ? text.match(/^---\n([\s\S]*?)\n---\n?/) : null;
|
|
33
|
+
if (!m) return null;
|
|
34
|
+
const out = {};
|
|
35
|
+
for (const line of m[1].split("\n")) {
|
|
36
|
+
const kv = line.match(/^([A-Za-z_][\w-]*)\s*:\s*(.*)$/);
|
|
37
|
+
if (!kv) continue;
|
|
38
|
+
let v = kv[2].trim().replace(/^"(.*)"$/, "$1").replace(/^'(.*)'$/, "$1");
|
|
39
|
+
if (/^\[.*\]$/.test(v)) v = v.slice(1, -1).split(",").map((x) => x.trim()).filter(Boolean);
|
|
40
|
+
out[kv[1]] = v;
|
|
41
|
+
}
|
|
42
|
+
return out;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** A record's state from the log and the drafts folder (O-2). */
|
|
46
|
+
export async function stateOf(dir, file) {
|
|
47
|
+
const { confirmedIn } = await import("../schema/foundation.mjs");
|
|
48
|
+
if (existsSync(join(dir, "_drafts", `${file.replace(/\//g, "__")}.draft`))) return "draft";
|
|
49
|
+
const es = entries(dir).filter((e) => e.entity === file);
|
|
50
|
+
if (!es.length) return "unlogged";
|
|
51
|
+
return confirmedIn(dir, file, null) ? "confirmed" : "agent";
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** The fields every record gets (O-3): id, created, updated and who wrote it last, from the provenance log, else the file. */
|
|
55
|
+
export function defaults(dir, file, id) {
|
|
56
|
+
const es = entries(dir).filter((e) => e.entity === file);
|
|
57
|
+
if (es.length) {
|
|
58
|
+
const last = es.at(-1);
|
|
59
|
+
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" };
|
|
60
|
+
}
|
|
61
|
+
const st = existsSync(join(dir, file)) ? statSync(join(dir, file)) : null;
|
|
62
|
+
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" };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Counts derived from a record (O-2): the length of each top-level list. */
|
|
66
|
+
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])) : {};
|
|
67
|
+
|
|
68
|
+
/** The function owning a record and its owner (O-2), from the package that holds it and venture.json's functions. */
|
|
69
|
+
export async function ownerOf(dir, type) {
|
|
70
|
+
const { packageOfRecord, packageDef } = await import("./package-routes.mjs");
|
|
71
|
+
const pkg = packageOfRecord(type);
|
|
72
|
+
const def = pkg ? packageDef(pkg) : null;
|
|
73
|
+
const fn = def?.function ?? def?.department ?? null;
|
|
74
|
+
const v = load(join(dir, "venture.json"));
|
|
75
|
+
const owner = fn ? v.functions?.[fn]?.owner ?? (!(v.members?.length) ? "the founder" : null) : null;
|
|
76
|
+
return { package: pkg, function: fn, owner };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** The metadata a read answers with (O-1, O-2, O-3). items maps each id to its venture-relative file. */
|
|
80
|
+
export async function metadataFor({ dir, r, route, params, result, items }) {
|
|
81
|
+
const v = load(join(dir, "venture.json"));
|
|
82
|
+
const fn = await ownerOf(dir, r.record);
|
|
83
|
+
const one = async (id, file, value) => ({
|
|
84
|
+
...defaults(dir, file, id),
|
|
85
|
+
...(file.endsWith(".md") || file.endsWith(".mdx") ? { frontmatter: frontmatter(value) } : value && typeof value === "object" && (value.title || value.name) ? { title: value.title ?? value.name } : {}),
|
|
86
|
+
derived: { state: await stateOf(dir, file), counts: countsOf(value) },
|
|
87
|
+
});
|
|
88
|
+
const base = {
|
|
89
|
+
venture: { id: v.id, name: v.name, phase: v.phase, status: v.status },
|
|
90
|
+
record: { type: r.record, route, schema: r.schema ?? null, form: r.form },
|
|
91
|
+
derived: { phase: v.phase, function: fn.function, owner: fn.owner, package: fn.package, by: "code" },
|
|
92
|
+
};
|
|
93
|
+
if (r.collection === "list") {
|
|
94
|
+
const per = {};
|
|
95
|
+
for (const [id, file] of Object.entries(items)) per[id] = await one(id, file, result?.[id]);
|
|
96
|
+
return { ...base, derived: { ...base.derived, count: Object.keys(per).length }, items: per };
|
|
97
|
+
}
|
|
98
|
+
const [id, file] = Object.entries(items)[0] ?? [];
|
|
99
|
+
if (!file) return base;
|
|
100
|
+
const own = await one(params.id ?? r.record, file, result);
|
|
101
|
+
return { ...base, ...own, derived: { ...base.derived, ...own.derived } };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// One filter test: a field by dot path on the record, else a derived field.
|
|
105
|
+
function matches(value, meta, where) {
|
|
106
|
+
return Object.entries(where).every(([k, want]) => {
|
|
107
|
+
let have = get(value ?? {}, k);
|
|
108
|
+
if (have === undefined) have = { state: meta?.derived?.state, function: meta?.function, owner: meta?.owner, phase: meta?.phase }[k];
|
|
109
|
+
const wants = Array.isArray(want) ? want.map(String) : [String(want)];
|
|
110
|
+
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));
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Applies input.where to a read answer (L-10): a list keeps matching items; a record that does not match comes back null. */
|
|
115
|
+
export function filterRead(answer, where, r) {
|
|
116
|
+
if (!where || typeof where !== "object" || !Object.keys(where).length || answer.result == null) return answer;
|
|
117
|
+
const md = answer.metadata ?? {};
|
|
118
|
+
const shared = { function: md.derived?.function, owner: md.derived?.owner, phase: md.venture?.phase };
|
|
119
|
+
if (r.collection === "list") {
|
|
120
|
+
const keep = Object.keys(answer.result).filter((id) => matches(answer.result[id], { ...shared, derived: md.items?.[id]?.derived }, where));
|
|
121
|
+
const pick = (o) => o ? Object.fromEntries(Object.entries(o).filter(([id]) => keep.includes(id))) : o;
|
|
122
|
+
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 } };
|
|
123
|
+
}
|
|
124
|
+
if (matches(answer.result, { ...shared, derived: md.derived }, where)) return { ...answer, filtered: { where, matched: 1, of: 1 } };
|
|
125
|
+
return { ...answer, result: null, filtered_out: { where, note: "the record exists but does not match the filter" } };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const parseInstructions = (chain) => {
|
|
129
|
+
const out = {};
|
|
130
|
+
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(); }
|
|
131
|
+
return out;
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* The layout with params (L-9) and its group (P-36): the group's AGENTS.md after the venture root's,
|
|
136
|
+
* then the item's own folders' AGENTS.md, nearest last; "[id]" in an instruction becomes the id.
|
|
137
|
+
*/
|
|
138
|
+
export function withParams(layout, { dir, params, file, group }) {
|
|
139
|
+
const chain = [...(layout.chain ?? [])];
|
|
140
|
+
const have = new Set(chain.map((c) => c.path));
|
|
141
|
+
const add = (rel, at = chain.length) => { if (dir && !have.has(rel) && existsSync(join(dir, rel))) { chain.splice(at, 0, { path: rel, text: readFileSync(join(dir, rel), "utf8") }); have.add(rel); } };
|
|
142
|
+
if (group?.agents) add(group.agents, chain.findIndex((c) => c.path === "AGENTS.md") + 1);
|
|
143
|
+
if (file) {
|
|
144
|
+
const parts = dirname(file).split("/").filter((p) => p && p !== ".");
|
|
145
|
+
let acc = "";
|
|
146
|
+
for (const p of parts) { acc = acc ? `${acc}/${p}` : p; add(`${acc}/AGENTS.md`); }
|
|
147
|
+
}
|
|
148
|
+
const instructions = parseInstructions(chain);
|
|
149
|
+
if (params?.id) for (const k of Object.keys(instructions)) instructions[k] = instructions[k].replace(/\[id\]/g, params.id);
|
|
150
|
+
return { ...layout, chain, instructions, params: params ?? {}, ...(group ? { group } : {}) };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** The read's hierarchy (P-28), outermost first, and the level that answered. */
|
|
154
|
+
export function hierarchy({ answer, layout }) {
|
|
155
|
+
const levels = [
|
|
156
|
+
{ level: "layout", holds: layout ? { chain: layout.chain.map((c) => c.path), stack: layout.stack?.map((s) => s.name) } : null },
|
|
157
|
+
{ level: "template", holds: layout ? { need_now: layout.need_now ?? null, gate: layout.gate ?? null, note: "recomputed on every action" } : null },
|
|
158
|
+
{ level: "error", caught: !answer.ok || !!answer.errors, holds: !answer.ok ? { error: answer.error, fix: answer.fix ?? null } : answer.errors ?? null },
|
|
159
|
+
{ level: "loading", caught: !!(answer.pending_draft || answer.pending_pull_requests), holds: answer.pending_draft ?? answer.pending_pull_requests ?? null },
|
|
160
|
+
{ level: "not-found", caught: answer.ok && answer.result === null && !!answer.not_found, holds: answer.not_found ?? null },
|
|
161
|
+
{ level: "page", caught: answer.ok && answer.result !== null && answer.result !== undefined, holds: answer.ok && answer.result != null ? "the record" : null },
|
|
162
|
+
];
|
|
163
|
+
return { levels, resolved_by: resolvedBy(answer) };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** The level that answered a read (P-28): error, then loading, then not-found, then page. */
|
|
167
|
+
export function resolvedBy(answer) {
|
|
168
|
+
if (!answer.ok) return "error";
|
|
169
|
+
if (answer.pending_draft || answer.pending_pull_requests) return "loading";
|
|
170
|
+
if (answer.result === null && answer.not_found) return "not-found";
|
|
171
|
+
if (answer.result === null && answer.filtered_out) return "page";
|
|
172
|
+
return "page";
|
|
173
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
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
|
+
|
|
12
|
+
const esc = (s) => String(s ?? "").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
13
|
+
const day = (t) => (t ? String(t).slice(0, 10) : "");
|
|
14
|
+
|
|
15
|
+
/** The card as SVG text. */
|
|
16
|
+
export function svg(card) {
|
|
17
|
+
const nums = card.headline.filter((h) => h.label !== "Live holes").slice(0, 4);
|
|
18
|
+
const cell = (h, i) => {
|
|
19
|
+
const x = 60 + i * 275;
|
|
20
|
+
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>
|
|
21
|
+
<text x="${x}" y="370" font-size="22" fill="#333">${esc(h.label)}</text>
|
|
22
|
+
<text x="${x}" y="400" font-size="18" fill="#666">as of ${esc(day(h.as_of))}</text>`;
|
|
23
|
+
};
|
|
24
|
+
const by = card.confirmed_by.length ? `Confirmed by ${card.confirmed_by.join(", ")}` : "Nothing confirmed yet";
|
|
25
|
+
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">
|
|
26
|
+
<rect width="1200" height="630" fill="#fafaf7"/>
|
|
27
|
+
<rect x="0" y="0" width="1200" height="12" fill="#1f6f5c"/>
|
|
28
|
+
<text x="60" y="120" font-size="64" font-weight="700" fill="#111">${esc(card.name)}</text>
|
|
29
|
+
<text x="60" y="175" font-size="28" fill="#333">Phase: ${esc(card.phase)}</text>
|
|
30
|
+
${nums.map(cell).join("\n ")}
|
|
31
|
+
<text x="60" y="520" font-size="22" fill="#333">${esc(by)}</text>
|
|
32
|
+
<text x="60" y="560" font-size="18" fill="#666">Release ${esc(card.release)}, built ${esc(day(card.built_at))}</text>
|
|
33
|
+
</svg>
|
|
34
|
+
`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
38
|
+
const dir = process.argv[2];
|
|
39
|
+
const f = dir && join(dir, ".release/card.json");
|
|
40
|
+
if (!f || !existsSync(f)) { console.error("usage: node interface/release-card.mjs <venture dir> (after node schema/build-release.mjs)"); process.exit(2); }
|
|
41
|
+
process.stdout.write(svg(JSON.parse(readFileSync(f, "utf8"))));
|
|
42
|
+
}
|
package/interface/router.mjs
CHANGED
|
@@ -24,6 +24,11 @@
|
|
|
24
24
|
// record (escalate -> an agent's write is a draft), and the role's allowed routes
|
|
25
25
|
// robots a team-plan role may list the routes it may read and write; system/robots shows them
|
|
26
26
|
// aliases interface/route-aliases.json maps a renamed route to its new one, with a note
|
|
27
|
+
// hierarchy a read resolves layout, template, error, loading, not-found, then page (P-28);
|
|
28
|
+
// resolved_by names the level that answered (interface/reads.mjs)
|
|
29
|
+
// writes a write answers with what it changed and the next route to act on; a layout lists
|
|
30
|
+
// the actions open to the actor; package, group and venture routes; global errors
|
|
31
|
+
// (interface/writes.mjs wraps call)
|
|
27
32
|
// storage a record is read and written through the connector for the source its record
|
|
28
33
|
// type lives in (knowledge/connectors, EE-037): the venture's own folder unless its
|
|
29
34
|
// knowledge/sources.json names another, such as a GitHub repository, where a write
|
|
@@ -40,7 +45,26 @@ import { agentFromEnv, entries as provEntries, record as prov } from "./provenan
|
|
|
40
45
|
import { address } from "../knowledge/connectors/contract.mjs";
|
|
41
46
|
import { holding, locked } from "../schema/decisions.mjs";
|
|
42
47
|
import { configOf, hold } from "../schema/gate.mjs";
|
|
48
|
+
// Freshness (Revalidating, V-1 to V-8): stale flags on reads, and the freshness and revalidate commands.
|
|
49
|
+
import { readFlag as freshnessFlag } from "../schema/freshness.mjs";
|
|
50
|
+
// Gate group hooks (M-18, F-5, X-2, X-4 to X-6, P-2, P-7, U-4): who is acting, access at the record,
|
|
51
|
+
// the gate's verdict, where held drafts wait, and check settings. The logic lives in these modules.
|
|
52
|
+
import { actingCall, itemId, mayReadItem, mayReadSome } from "./acting.mjs";
|
|
53
|
+
import { checkPlan, gateWrite, heldDraft } from "./gate-hooks.mjs";
|
|
43
54
|
import { LOCAL, attributeSources, connect, sourceFor } from "../knowledge/connectors/index.mjs";
|
|
55
|
+
// Caching (schema/caching.mjs): per-action memo, the record-level cache for command routes, and system/cache, system/judge, system/answer.
|
|
56
|
+
import { actionScope, cacheRoutes, command as cachedCommand, forget, internal as cacheInternal, memo } from "../schema/caching.mjs";
|
|
57
|
+
// Context hook: instrumentation of every call and judgment (interface/instrumentation.mjs, P-4, F-4).
|
|
58
|
+
import { instrumented, judgment } from "./instrumentation.mjs";
|
|
59
|
+
// Hooks for writes, reads and boundaries (wf/writes): the wrapper around call, per-record containment and private folders.
|
|
60
|
+
import { around } from "./writes.mjs";
|
|
61
|
+
import { contain } from "./boundaries.mjs";
|
|
62
|
+
import { packageSlots } from "./package-routes.mjs";
|
|
63
|
+
import { routable } from "../schema/structure.mjs";
|
|
64
|
+
// Brand hook (Y-2..Y-4, J-2..J-4, T-1, T-2, P-24, P-26): routes, commands and layout context from schema/brand.mjs.
|
|
65
|
+
import { BRAND_ROUTES, brandCommands, brandLayout } from "../schema/brand.mjs";
|
|
66
|
+
// Upgrades hook (V-9): a read says when what the record reads changed in its system of record.
|
|
67
|
+
import { staleOf } from "../schema/stale.mjs";
|
|
44
68
|
|
|
45
69
|
export const repo = new URL("..", import.meta.url).pathname;
|
|
46
70
|
const load = (p) => JSON.parse(readFileSync(p, "utf8"));
|
|
@@ -70,7 +94,9 @@ export function kits() {
|
|
|
70
94
|
const needNowPriors = () => load(join(repo, "schema/need-now.json"));
|
|
71
95
|
|
|
72
96
|
/** The route table, generated from schema/data-model.json, the kit manifests and schema/need-now.json. */
|
|
73
|
-
|
|
97
|
+
// Caching hook (F-12): built once per action and shared.
|
|
98
|
+
export function routes() { return memo("routes", buildRoutes); }
|
|
99
|
+
function buildRoutes() {
|
|
74
100
|
const model = load(join(repo, "schema/data-model.json"));
|
|
75
101
|
const ks = kits();
|
|
76
102
|
const owner = new Map(ks.flatMap((k) => k.records.map((r) => [r, k])));
|
|
@@ -110,6 +136,8 @@ export function routes() {
|
|
|
110
136
|
cmd("system/package", "schema/packages.mjs", "The venture package (EE-067, EE-069): the core packages open now and the handoffs, each record confirmed, to review or to write, what moves on its own and what waits, and what completion unlocks; --write renders the view and records a version. Assumes a solo founder or small founding team in Discovery and Validation; agents are a package enhancement"),
|
|
111
137
|
cmd("system/need-now", "schema/need-now.mjs", "What the venture needs now, soon, or should hold back (EE-028)"),
|
|
112
138
|
cmd("system/time-to-ready", "schema/time-to-ready.mjs", "Time to running, and to ready per layer and MAD tier"),
|
|
139
|
+
// Model steps hook (use model, EE-073): the logic is schema/model-steps.mjs; this route only shows the payload.
|
|
140
|
+
cmd("system/steps", "schema/model-steps.mjs", "The model steps' payload (use model, EE-073): what code computed, a placeholder for each judgment a model has yet to give, and the only fields each one receives; asks no model. mentura steps run asks one", { args: ["payload"] }),
|
|
113
141
|
cmd("purpose/validation/assess", "schema/assess-problem.mjs", "What the problem's evidence is strong enough for: each assumption labelled demonstrated, inferred, assumed, unknown or contradicted, the commitment it supports and the next test (EE-050). Asks first when the record is incomplete; input as_is runs it anyway", { as_is: true }),
|
|
114
142
|
cmd("governance/analytics", "schema/decision-analytics.mjs", "Decision analytics (EE-072): how many decisions of each type each function has made, open against decided, in whose words, in which phase, and their outcomes. Data only; a view is the platform's, or a visualization skill's", { actions: ["run", "layout"] }),
|
|
115
143
|
cmd("governance/decisions", "schema/decisions.mjs", "The decision matrix: every decision the venture owes by layer and phase, with its parent and boundary, and whether it renders now, waits, comes later, is stale or deferred (EE-054, EE-057)"),
|
|
@@ -124,9 +152,17 @@ export function routes() {
|
|
|
124
152
|
cmd("governance/payouts", "schema/payouts.mjs", "Who gets what at a sale value, from the cap table (EE-022); input value, else the scenarios in exit.json", { needs: ["governance/ownership.json"], venture_arg: "id" }),
|
|
125
153
|
cmd("system/overview", null, "The venture at a glance: telos, validation, need-now, time to ready, team and recent provenance, rendered as parallel slots", { internal: "overview" }),
|
|
126
154
|
cmd("system/robots", null, "Which roles may read and write which routes, from the team plan", { internal: "robots" }),
|
|
155
|
+
// Context hook (interface/vitals.mjs, interface/context.mjs): agent vitals and the context budget (N-6, C-15, N-13).
|
|
156
|
+
cmd("system/vitals", "interface/vitals.mjs", "Agent vitals (N-6): calls per task, tokens of context per call, time to a useful answer and time from a decision opening to a person deciding it, at the 75th percentile, each checked against its threshold (C-15). Reads the events log that instrumentation writes"),
|
|
157
|
+
cmd("system/context", "interface/context.mjs", "The context budget (N-13): what every route sends an agent, in tokens, against its budget, with the largest part and where to move it (S-14, F-6)", { args: ["budget"] }),
|
|
127
158
|
cmd("knowledge/catalog", "knowledge/connectors/catalog.mjs", "Build or refresh the catalog: every item in every source the venture keeps records in, with its address and version, no content (EE-037)"),
|
|
128
159
|
cmd("knowledge/search", "knowledge/connectors/catalog.mjs", "Find items across every source through the catalog; input value is the query (EE-037)", { value_flag: "--search" }),
|
|
160
|
+
// Freshness (Revalidating, V-1 to V-8): schema/freshness.mjs.
|
|
161
|
+
cmd("system/freshness", "schema/freshness.mjs", "What is fresh, stale or expired, and which records are live holes: by time (freshness profiles, per phase too) and on demand (a dependency changed, a where-used parent drifted, a tag, path or package was revalidated)"),
|
|
162
|
+
cmd("system/revalidate", "schema/freshness.mjs", "Mark records stale on demand: input value tag:<route> (the record and what depends on it), path:<folder>, package:<name>, or pivot (the validation package); a plain folder is refused in favour of exact tags", { value_flag: "--revalidate" }),
|
|
129
163
|
);
|
|
164
|
+
out.push(...cacheRoutes(cmd)); // Caching hook: system/cache, system/judge, system/answer (schema/caching.mjs).
|
|
165
|
+
out.push(...brandCommands(cmd)); // Brand hook: brand tokens, voice, glossary terms, sitemap.xml, asset facts (schema/brand.mjs).
|
|
130
166
|
return out.sort((a, b) => a.route.localeCompare(b.route));
|
|
131
167
|
}
|
|
132
168
|
|
|
@@ -172,14 +208,15 @@ export function ventureDir(id, workspace = defaultWorkspace()) {
|
|
|
172
208
|
}
|
|
173
209
|
return null;
|
|
174
210
|
}
|
|
175
|
-
|
|
211
|
+
// A broken venture.json is found by its folder name, so its global error can answer (E-8, interface/boundaries.mjs).
|
|
212
|
+
for (const d of walkVentures(workspace)) { let vid; try { vid = load(join(d, "venture.json")).id; } catch { vid = basename(d); } if (vid === id) return d; }
|
|
176
213
|
return null;
|
|
177
214
|
}
|
|
178
215
|
|
|
179
216
|
/** The ventures under a workspace: id, name, phase, path (the [...venture] segment) and folder. */
|
|
180
217
|
export function ventures(workspace = defaultWorkspace()) {
|
|
181
218
|
const root = existsSync(join(workspace, "ventures")) ? join(workspace, "ventures") : workspace;
|
|
182
|
-
return walkVentures(workspace).map((d) => {
|
|
219
|
+
return walkVentures(workspace).map((d) => { let v; try { v = load(join(d, "venture.json")); } catch { return { id: basename(d), name: null, phase: null, path: relative(root, d), dir: d, broken: true }; } return { id: v.id, name: v.name, phase: v.phase, path: relative(root, d), dir: d }; });
|
|
183
220
|
}
|
|
184
221
|
|
|
185
222
|
// The venture's decision rights: the template, tightened by the venture's own file.
|
|
@@ -200,7 +237,7 @@ export function gateOf(r, dir) {
|
|
|
200
237
|
}
|
|
201
238
|
|
|
202
239
|
// Robots: a team-plan role may list the routes it may read and write (globs over routes).
|
|
203
|
-
function allowed(dir, actorId, action, route) {
|
|
240
|
+
export function allowed(dir, actorId, action, route) {
|
|
204
241
|
if (!actorId || !dir) return true;
|
|
205
242
|
const plan = [join(dir, "people/team-plan.json"), join(dir, "operations/team-plan.json")].find((p) => existsSync(p));
|
|
206
243
|
if (!plan) return true;
|
|
@@ -219,6 +256,8 @@ const PACKAGE_ROUTE = {
|
|
|
219
256
|
"anatomy-answers": "product/anatomy-answers", "anatomy-input": "product/anatomy", "venture-package": ".release/venture-package",
|
|
220
257
|
"decision-record": "decisions",
|
|
221
258
|
customer: "customers", investor: "investors", "money-account": "money", "raw-source": "raw/sources",
|
|
259
|
+
people: "people", founder: "people/founders", "fund-thesis": "fund/thesis", "fund-terms": "fund/terms", deal: "deals",
|
|
260
|
+
...BRAND_ROUTES, // Brand hook: marketing/brand, public/assets, glossary (schema/brand.mjs).
|
|
222
261
|
};
|
|
223
262
|
|
|
224
263
|
// Prefer a file that is already there, so an unmigrated venture keeps its old path. A new write uses the first path, which is the package layout.
|
|
@@ -243,6 +282,7 @@ export function draftOf(route, venture, workspace) {
|
|
|
243
282
|
const dir = ventureDir(venture, workspace);
|
|
244
283
|
if (!r || r.kind !== "record" || r.collection === "list" || !dir) return null;
|
|
245
284
|
const file = fileFor(r, params, dir);
|
|
285
|
+
if (!file) return null; // a route with no concrete file (a pattern-only slot) has no draft
|
|
246
286
|
return { dir, file, draft: draftPath(file), gate: gateOf(r, dir), route: r.collection === "item" ? r.route.replace("[id]", params.id) : r.route };
|
|
247
287
|
}
|
|
248
288
|
|
|
@@ -266,9 +306,11 @@ function instructionsOf(chain) {
|
|
|
266
306
|
}
|
|
267
307
|
|
|
268
308
|
/** A score below its threshold is uncertain: not an error and not an answer. It routes to the function owner. */
|
|
269
|
-
export function judge(score, threshold, owner) {
|
|
270
|
-
|
|
271
|
-
|
|
309
|
+
export function judge(score, threshold, owner, at) {
|
|
310
|
+
const out = typeof score !== "number" || score < threshold ? { outcome: "uncertain", owner, result: null } : { outcome: "answer", result: score };
|
|
311
|
+
// Context hook: a judgment made for a venture (at.dir) is an instrumentation event (F-4).
|
|
312
|
+
if (at?.dir) judgment(at.dir, { question: at.question, route: at.route, model: at.model, score, threshold, outcome: out.outcome, owner });
|
|
313
|
+
return out;
|
|
272
314
|
}
|
|
273
315
|
|
|
274
316
|
/** Runs fn. A throw is the uncaught outcome. */
|
|
@@ -292,9 +334,11 @@ export function layout(r, dir) {
|
|
|
292
334
|
const pkg = owned ? { skill: skillsOf(owned).map((s) => s.path), template: r.template ?? null, gates: owned.gates ?? [] } : null;
|
|
293
335
|
const out = { route: r.route, stack, chain, instructions: instructionsOf(chain), package: pkg, note: r.note, groups: r.groups ?? [] };
|
|
294
336
|
if (r.kind === "record") Object.assign(out, { schema: r.schema, template: r.template, checks: r.checks, gate: gateOf(r, dir) });
|
|
337
|
+
{ const brand = brandLayout(dir, r.route); if (brand) out.brand = brand; } // Brand hook: brand, voice and package terms at this route (Y-2, Y-3, T-2).
|
|
295
338
|
if (dir && r.kind === "record") {
|
|
296
339
|
try {
|
|
297
|
-
|
|
340
|
+
// Caching hook (O-5): shared with the system/need-now command in the same action.
|
|
341
|
+
const nn = JSON.parse(memo(`cmd:schema/need-now.mjs ${dir} --json`, () => spawnSync("node", [join(repo, "schema/need-now.mjs"), dir, "--json"], { encoding: "utf8" }), { dir }).stdout);
|
|
298
342
|
const row = nn.rows.find((x) => x.type === r.record);
|
|
299
343
|
if (row) out.need_now = { phase: nn.phase, status: row.status, now: row.now, next: row.next, why: row.why };
|
|
300
344
|
} catch {}
|
|
@@ -311,10 +355,18 @@ const fix = (r) => ({ skills: r.kit_path ? skillsOf(kits().find((k) => k.path ==
|
|
|
311
355
|
* routes; via: the interface (cli, mcp, api; default router); provenance: { derived_from,
|
|
312
356
|
* decision, note, agent } for the log.
|
|
313
357
|
*/
|
|
314
|
-
|
|
358
|
+
// Caching hook (F-2): one top-level call is one action; the calls it makes join it and share its memo.
|
|
359
|
+
// Context hook: every call is timed and measured for agent vitals (interface/instrumentation.mjs).
|
|
360
|
+
// Gate group hook (M-18): who is acting is checked on every call and carried on the answer.
|
|
361
|
+
// Writes hook (interface/writes.mjs): adds what reads, layouts and writes answer.
|
|
362
|
+
export const call = (args) => actionScope(() => instrumented(args, (a) => actingCall(a, (b) => around(callInAction, b), ventureDir), ventureDir));
|
|
363
|
+
async function callInAction({ venture, route, action, input, actor = "agent", actor_id = process.env.MENTURA_ACTOR_ID, workspace, via = "router", provenance = {} }) {
|
|
315
364
|
const requested = route;
|
|
316
365
|
const { r, params, alias } = resolve(route);
|
|
317
366
|
if (!r) return { ok: false, outcome: "error", error: `no route ${route}` };
|
|
367
|
+
// Context hook (interface/context.mjs): navigate reads a route header first, with its layout once per
|
|
368
|
+
// session, its template and prefetched refs; head stops after the header (N-3, L-2, O-4).
|
|
369
|
+
if (action === "navigate" || action === "head") return (await import("./context.mjs")).navigate({ ...(input ?? {}), venture, route: requested, workspace, actor, actor_id, via, header_only: action === "head" || !!input?.header_only });
|
|
318
370
|
const deprecated = alias ? { deprecated: `${alias.from} is now ${alias.to}${alias.note ? `: ${alias.note}` : ""}` } : {};
|
|
319
371
|
route = r.collection === "item" ? r.route.replace("[id]", params.id) : r.route;
|
|
320
372
|
if (!r.actions.includes(action)) return { ok: false, error: `${route} has no action ${action} (it has ${r.actions.join(", ")})` };
|
|
@@ -323,7 +375,8 @@ export async function call({ venture, route, action, input, actor = "agent", act
|
|
|
323
375
|
if (action === "layout") { if (venture && !dir) return { ok: false, error: `no venture ${venture}` }; return { ok: true, result: layout(r, dir), ...deprecated }; }
|
|
324
376
|
if (!dir) return { ok: false, error: `no venture ${venture}` };
|
|
325
377
|
const act = action === "write" ? "write" : "read";
|
|
326
|
-
|
|
378
|
+
// Gate group hook (F-5): a role that may read some items of a collection may read its list, filtered below.
|
|
379
|
+
if (!allowed(dir, actor_id, act, requested) && !allowed(dir, actor_id, act, route) && !(act === "read" && r.collection === "list" && mayReadSome(dir, actor_id, route))) return { ok: false, error: `${actor_id} may not ${act} ${route} (team plan routes)` };
|
|
327
380
|
// An activity a decision unlocks stays closed to agents until a person makes that decision (EE-058).
|
|
328
381
|
const lock = actor !== "person" && (action === "write" || action === "run") ? (locked(dir, requested, actor_id) || locked(dir, route, actor_id)) : null;
|
|
329
382
|
if (lock) return { ok: false, error: `${route} is locked until decision ${lock.id} is made: ${lock.question}`, locked_by: lock.id,
|
|
@@ -337,15 +390,19 @@ export async function call({ venture, route, action, input, actor = "agent", act
|
|
|
337
390
|
if (r.kind === "command") {
|
|
338
391
|
if (r.internal === "overview") return { ok: true, result: await overview(venture, workspace), ...deprecated };
|
|
339
392
|
if (r.internal === "robots") return { ok: true, result: robots(dir), ...deprecated };
|
|
393
|
+
// Caching hook: system/cache, system/judge and system/answer are handled in schema/caching.mjs.
|
|
394
|
+
if (["cache", "judge", "answer"].includes(r.internal)) return { ...(await cacheInternal(r.internal, { dir, venture, workspace, input, actor, actor_id, via, provenance }, { call, resolve, draftOf })), ...deprecated };
|
|
340
395
|
const lacking = (r.needs ?? []).filter((f) => !existsSync(join(dir, f)));
|
|
341
396
|
if (lacking.length) return { ok: false, error: `${route} needs ${lacking.join(", ")} in ${venture}`, fix: { missing: lacking } };
|
|
342
397
|
if (r.value_flag && !input?.value) return { ok: false, error: `${route} needs a value (${r.note})` };
|
|
343
398
|
const args = r.venture_arg === "id" ? [dir, ...(input?.value ? [String(input.value)] : []), "--json"] : r.value_flag ? [dir, r.value_flag, String(input.value), "--json"] : [dir, ...(r.args ?? []), ...(r.as_is && input?.as_is ? ["--as-is"] : []), "--json"];
|
|
344
|
-
|
|
399
|
+
// Caching hook (C-3, H-3): a declared command's result comes from the record-level cache; the spawn is memoized per action (O-5).
|
|
400
|
+
const p = await cachedCommand({ dir, route, script: r.script, args, input, actor, actor_id }, () => memo(`cmd:${r.script} ${args.join(" ")}`, () => spawnSync("node", [join(repo, r.script), ...args], { encoding: "utf8" }), { dir }));
|
|
401
|
+
const cache = p.cache ? { cache: p.cache } : {};
|
|
345
402
|
if (p.status !== 0) return { ok: false, error: (p.stderr || p.stdout).trim() };
|
|
346
403
|
// A command that needs input first (EE-050) answers needs_input: the caller asks the person
|
|
347
404
|
// whether to fill the record in or run as is, and runs again with input as_is to go on.
|
|
348
|
-
try { const out = JSON.parse(p.stdout); return { ok: true, result: out.needs_input ? null : out, ...(out.needs_input ? { needs_input: out.needs_input } : {}), ...deprecated }; } catch { return { ok: true, result: p.stdout, ...deprecated }; }
|
|
405
|
+
try { const out = JSON.parse(p.stdout); return { ok: true, result: out.needs_input ? null : out, ...(out.needs_input ? { needs_input: out.needs_input } : {}), ...cache, ...deprecated }; } catch { return { ok: true, result: p.stdout, ...cache, ...deprecated }; }
|
|
349
406
|
}
|
|
350
407
|
|
|
351
408
|
// Storage: the connector for the source this record type lives in.
|
|
@@ -365,11 +422,15 @@ export async function call({ venture, route, action, input, actor = "agent", act
|
|
|
365
422
|
const patterns = r.collection === "item" ? [fileFor(r, params, dir)] : r.files;
|
|
366
423
|
let items;
|
|
367
424
|
try {
|
|
368
|
-
|
|
369
|
-
|
|
425
|
+
// a file named _* is never an item; routable: private _folders (P-22)
|
|
426
|
+
const paths = patterns.some((g) => g.includes("*")) ? (await store.list()).map((x) => x.path).filter((f) => !/(^|\/)_[^/]*$/.test(f) && patterns.some((g) => glob(g).test(f) && routable(f, g))) : patterns;
|
|
427
|
+
// Caching hook (F-2): each record is read once per action and shared.
|
|
428
|
+
items = (await Promise.all(paths.map((f) => memo(`read:${src.id}:${f}`, () => store.read(f), { dir })))).filter(Boolean);
|
|
429
|
+
// Gate group hook (F-5): access is checked again at the record; a list holds only the items the role may read.
|
|
430
|
+
if (r.collection === "list" && actor_id) items = items.filter((x) => mayReadItem(dir, actor_id, route, itemId(r.files, x.path)));
|
|
370
431
|
} catch (e) { return { ok: false, error: `${src.id}: ${String(e.message ?? e)}` }; }
|
|
371
432
|
const found = items.map((x) => x.path);
|
|
372
|
-
const draft = !remote && r.collection !== "list" && existsSync(join(dir, draftPath(fileFor(r, params, dir)))) ? draftPath(fileFor(r, params, dir)) : null;
|
|
433
|
+
const draft = !remote && r.collection !== "list" && fileFor(r, params, dir) && existsSync(join(dir, draftPath(fileFor(r, params, dir)))) ? draftPath(fileFor(r, params, dir)) : null;
|
|
373
434
|
const loading = draft ? { pending_draft: { file: draft, since: provEntries(dir).filter((e) => e.entity === draft).at(-1)?.at ?? null, accept: `mentura accept ${route} --venture ${venture}` } } : {};
|
|
374
435
|
if (remote) {
|
|
375
436
|
const prs = provEntries(dir).filter((e) => e.source?.pull_request && found.concat(patterns).some((f) => e.entity === address(src.id, f)));
|
|
@@ -394,6 +455,9 @@ export async function call({ venture, route, action, input, actor = "agent", act
|
|
|
394
455
|
}
|
|
395
456
|
if (attrs.length) x.attributes = cites;
|
|
396
457
|
}
|
|
458
|
+
// Freshness (V-5): a stale record is still read, flagged stale until it is redone (stale-while-revalidate).
|
|
459
|
+
const fresh = remote ? {} : await freshnessFlag(dir, found, r.collection === "list");
|
|
460
|
+
Object.assign(loading, fresh);
|
|
397
461
|
const parseOne = parse;
|
|
398
462
|
const parseWith = (x) => { const v = parseOne(x); return overlays.has(x.path) ? { ...v, ...overlays.get(x.path) } : v; };
|
|
399
463
|
if (r.collection === "list") {
|
|
@@ -406,19 +470,26 @@ export async function call({ venture, route, action, input, actor = "agent", act
|
|
|
406
470
|
}
|
|
407
471
|
return path;
|
|
408
472
|
};
|
|
409
|
-
|
|
473
|
+
// Per-record containment (E-6): an item that does not parse is named in errors; the rest come back.
|
|
474
|
+
const { values, errors } = contain(items, parseWith, idOf);
|
|
475
|
+
return { ok: true, result: Object.fromEntries(values.map(([x, v]) => [idOf(x.path), v])), sources: Object.fromEntries(values.map(([x]) => [idOf(x.path), cite(x)])), ...(Object.keys(errors).length ? { errors } : {}), ...fresh, ...deprecated };
|
|
410
476
|
}
|
|
411
477
|
const data = Object.fromEntries(items.map((x) => [x.path, parseWith(x)]));
|
|
412
|
-
|
|
478
|
+
// V-9 hook: a summary whose source changed since it was written is marked stale (schema/stale.mjs).
|
|
479
|
+
const stale = !remote && items.length === 1 ? staleOf(dir, found[0]) : null;
|
|
480
|
+
return { ok: true, result: items.length === 1 ? data[found[0]] : data, ...(items.length === 1 ? { source: cite(items[0]) } : { sources: Object.fromEntries(items.map((x) => [x.path, cite(x)])) }), ...loading, ...(stale ? { stale } : {}), ...deprecated };
|
|
413
481
|
}
|
|
414
482
|
|
|
415
483
|
if (action === "check") {
|
|
416
|
-
|
|
417
|
-
const
|
|
418
|
-
|
|
484
|
+
// Gate group hook (P-7): the checks settings in naive.config.json turn a check off or make it warn only.
|
|
485
|
+
const plan = checkPlan(dir, r.checks);
|
|
486
|
+
const outs = plan.filter((p) => p.mode !== "off").map((p) => ({ check: p.check, mode: p.mode, ...spawnSync("node", [join(repo, p.check), dir], { encoding: "utf8" }) }));
|
|
487
|
+
const ok = outs.every((o) => o.status === 0 || o.mode === "warn");
|
|
488
|
+
return { ok, result: [...outs.map((o) => ({ check: o.check, passed: o.status === 0, ...(o.mode === "warn" ? { warn_only: true } : {}), output: (o.stdout + o.stderr).trim() })), ...plan.filter((p) => p.mode === "off").map((p) => ({ check: p.check, passed: null, off: true }))], ...(ok ? {} : { fix: fix(r) }), ...deprecated };
|
|
419
489
|
}
|
|
420
490
|
|
|
421
491
|
if (action === "write") {
|
|
492
|
+
forget(dir); // Caching hook (H-3): a write never uses the cache, and what this action memoized for the venture is read again after it.
|
|
422
493
|
if (!input || typeof input !== "object") return { ok: false, error: "write needs the record as input" };
|
|
423
494
|
if (r.collection === "item" && !/^[a-z0-9][a-z0-9-]*$/.test(params.id)) return { ok: false, error: `${params.id} is not a valid id (lowercase letters, digits and hyphens)` };
|
|
424
495
|
const file = fileFor(r, params, dir);
|
|
@@ -444,7 +515,9 @@ export async function call({ venture, route, action, input, actor = "agent", act
|
|
|
444
515
|
// (EE-054): an agent's write becomes a draft, as for a gated record.
|
|
445
516
|
const held = !remote && actor !== "person" ? holding(dir, file) : null;
|
|
446
517
|
// The venture's own fail-closed routes (naive.config.json, the gate, EE-073) hold an agent's write too.
|
|
447
|
-
|
|
518
|
+
// Gate group hook (X-1 to X-6, U-4): the venture's gate decides; a refusal stops the write here.
|
|
519
|
+
const configured = !remote ? gateWrite({ dir, r, route, requested, file, actor, actor_id, provenance, gate }) : null;
|
|
520
|
+
if (configured?.then === "refuse") return { ok: false, error: configured.why, gate_rule: configured.rule, refused_by: configured.source };
|
|
448
521
|
const gatedDraft = !remote && (!!gate?.drafts || !!held || !!configured) && actor !== "person";
|
|
449
522
|
const attributeWrites = [];
|
|
450
523
|
if (split.length && !gatedDraft) {
|
|
@@ -482,13 +555,12 @@ export async function call({ venture, route, action, input, actor = "agent", act
|
|
|
482
555
|
}
|
|
483
556
|
|
|
484
557
|
const draft = gatedDraft;
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
return { ok: true, result: { written: draft ? draftPath(file) : file, draft, gate: gate?.class ?? null, provenance: entry.hash, note: draft ? (gate?.drafts ? `${r.record} is a ${gate.class} decision (${gate.level}, owner ${gate.owner}): written as a draft for a person to accept` : held ? `decision ${held.id} ${held.why}: written as a draft for a person to accept` : `${configured.rule} fails closed in naive.config.json: written as a draft for a person to accept`) : "written", ...(held ? { held_by: held.id } : {}), ...(configured ? { gate_rule: configured.rule } : {}), ...attributesOut }, ...deprecated };
|
|
558
|
+
// Gate group hook (X-2): the held draft, the owner it was sent to and any variants kept side by side.
|
|
559
|
+
let held2 = null;
|
|
560
|
+
if (draft) held2 = heldDraft(dir, { draft: draftPath(file), file, text, verdict: configured, route, agent });
|
|
561
|
+
else await store.write(file, recordText);
|
|
562
|
+
const entry = log(draft ? held2.written : file, draft ? "draft" : provenance.activity ?? "write");
|
|
563
|
+
return { ok: true, result: { written: draft ? held2.written : file, draft, gate: gate?.class ?? null, provenance: entry.hash, note: draft ? (gate?.drafts ? `${r.record} is a ${gate.class} decision (${gate.level}, owner ${gate.owner}): written as a draft for a person to accept` : held ? `decision ${held.id} ${held.why}: written as a draft for a person to accept` : configured.why) : "written", ...(held ? { held_by: held.id } : {}), ...(configured ? { gate_rule: configured.rule, ...(configured.owner ? { owner: configured.owner } : {}) } : {}), ...(held2?.variants ? { variant: held2.variant, variants: held2.variants, decision: held2.decision } : {}), ...attributesOut }, ...deprecated };
|
|
492
564
|
}
|
|
493
565
|
return { ok: false, error: `unhandled action ${action}` };
|
|
494
566
|
}
|
|
@@ -539,6 +611,7 @@ async function overview(venture, workspace) {
|
|
|
539
611
|
slot("@need-now", async () => (await run("system/need-now")).rows.filter((x) => x.status === "suggest now").map((x) => ({ record: x.type, now: x.now, why: x.why }))),
|
|
540
612
|
slot("@ready", async () => { const t = await run("system/time-to-ready"); return { running: t.running.on ?? `not yet: ${t.running.missing.join(", ")}`, layers: t.layers.map((l) => ({ layer: l.layer, ready: l.ready_on ?? `missing ${l.missing.join(", ")}` })) }; }),
|
|
541
613
|
slot("@team", async () => { const p = await read("people/team-plan"); if (typeof p === "string") return p; return p.roles.map((x) => ({ role: x.id, occupant: x.occupant, status: x.status, needed_by: x.needed_by })); }),
|
|
614
|
+
slot("@packages", () => packageSlots(dir, venture)), // P-17: a package with nothing in it shows its default
|
|
542
615
|
slot("@provenance", () => provEntries(dir).slice(-5).reverse().map((e) => ({ at: e.at, activity: e.activity, entity: e.entity, by: `${e.agent.kind}${e.agent.id ? ` ${e.agent.id}` : ""} via ${e.agent.via}` }))),
|
|
543
616
|
]),
|
|
544
617
|
};
|