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
|
@@ -0,0 +1,497 @@
|
|
|
1
|
+
// Releases (C-10 to C-18, H-4, N-10, P-25, P-31, O-7, Z-3, I-17): naive build. A release is the
|
|
2
|
+
// venture's static shell (C-11): every record a person confirmed, frozen in as it was confirmed,
|
|
3
|
+
// with named holes for what must stay live (open decisions, streams, money, attributes another
|
|
4
|
+
// system of record masters, and answers computed per call). It is written to <venture>/.release/
|
|
5
|
+
// and read without the router (schema/release-read.mjs, schema/start-release.mjs).
|
|
6
|
+
//
|
|
7
|
+
// build(dir, { now }) check every open package can be released and write the release
|
|
8
|
+
// add(dir, route, { now }) add one new item to the release without rebuilding the rest (C-18)
|
|
9
|
+
// exportRelease(dir, out) the release as plain files, with no holes and every number dated (Z-3)
|
|
10
|
+
// cardOf(dir, manifest) the release card's data, assembled from the release's records (O-7)
|
|
11
|
+
//
|
|
12
|
+
// The rules, in the order the ledger gives them:
|
|
13
|
+
// C-10 Reading the venture's files is predictable, so it goes into a release with no step of
|
|
14
|
+
// its own: build walks the venture and freezes what it finds. Nothing calls a model and
|
|
15
|
+
// "now" is an input (--now), so the same venture gives the same release id.
|
|
16
|
+
// C-11 Frozen: a record whose last change a person made or confirmed, unchanged since (its sha256
|
|
17
|
+
// matches the provenance log). Holes: open decisions, _streams/, money accounts, attributes
|
|
18
|
+
// mastered in another source, and command routes that read runtime data (H-4).
|
|
19
|
+
// C-13 Every open package (schema/packages.mjs, "now") must be releasable: no record it requires
|
|
20
|
+
// may be an agent's unconfirmed write, a waiting draft with nothing confirmed, or changed since
|
|
21
|
+
// it was confirmed (an unconfirmed optional record is left out instead). Build names each blocker with the command that settles it and exits 1.
|
|
22
|
+
// Missing records are gaps the release names; they do not block it.
|
|
23
|
+
// C-14 Holes sit at the leaves: one decision, one stream, one money account, one field. The rest
|
|
24
|
+
// of the record, item or package is still frozen, and each package reports its shell share.
|
|
25
|
+
// C-17 Three stores: the release (in .release/, meant for git), the local cache in .naive/cache/
|
|
26
|
+
// (never released), and the agent's session (the harness's own). The cache key holds the
|
|
27
|
+
// framework version, every package definition and MENTURA_MODEL; change any and it starts fresh.
|
|
28
|
+
// H-4 A command route whose code (followed through its imports) reads the clock, calls a model,
|
|
29
|
+
// reaches the network or reads a stream is a live hole. Any other is run at build time
|
|
30
|
+
// against the confirmed records alone, and its answer is frozen in.
|
|
31
|
+
// N-10 Collection items known at build time are enumerated and frozen, one entry each.
|
|
32
|
+
// P-31 Only confirmed records ship. Drafts, _private/, raw inputs and notes (files that are not
|
|
33
|
+
// records) never enter a release; private records and private_fields are withheld.
|
|
34
|
+
|
|
35
|
+
import { spawnSync } from "node:child_process";
|
|
36
|
+
import { createHash } from "node:crypto";
|
|
37
|
+
import { existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
38
|
+
import { tmpdir } from "node:os";
|
|
39
|
+
import { dirname, join, resolve as resolvePath } from "node:path";
|
|
40
|
+
import { call, glob, routes } from "../interface/router.mjs";
|
|
41
|
+
import { agentFromEnv, entries as provEntries, record as prov } from "../interface/provenance.mjs";
|
|
42
|
+
import { attributeSources, connect } from "../knowledge/connectors/index.mjs";
|
|
43
|
+
import { FRAME, confirmedIn } from "./foundation.mjs";
|
|
44
|
+
import { HOME, packages } from "./packages.mjs";
|
|
45
|
+
import { CARD, MANIFEST, RECORDS, RELEASE_DIR, frozenText, readRelease } from "./release-read.mjs";
|
|
46
|
+
|
|
47
|
+
const repo = new URL("..", import.meta.url).pathname;
|
|
48
|
+
const load = (p) => JSON.parse(readFileSync(p, "utf8"));
|
|
49
|
+
const sha = (s) => createHash("sha256").update(s).digest("hex");
|
|
50
|
+
const sorted = (x) => Array.isArray(x) ? x.map(sorted) : x && typeof x === "object" ? Object.fromEntries(Object.keys(x).sort().map((k) => [k, sorted(x[k])])) : x;
|
|
51
|
+
const stamp = (d) => new Date(d).toISOString().replace(/\.\d+Z$/, "Z");
|
|
52
|
+
const CLI = "node interface/cli/mentura.mjs";
|
|
53
|
+
|
|
54
|
+
export const CACHE = ".naive/cache/release.json";
|
|
55
|
+
/** Record types that stay live: never frozen, always a hole (C-11, V-3). */
|
|
56
|
+
export const LIVE = { "money-account": "money moves between builds; read the account live", "data-stream": "a stream is append-only activity; read it live" };
|
|
57
|
+
/** Record types that are inputs or generated views, never shipped (P-31). */
|
|
58
|
+
export const NOT_SHIPPED = { "idea-dump": "raw input: the founder's notes", "raw-source": "raw input", "raw-text": "raw input", "venture-package": "a generated view", provenance: "the log itself", release: "the release itself" };
|
|
59
|
+
|
|
60
|
+
// ---- the cache key (C-17) ----
|
|
61
|
+
|
|
62
|
+
/** What a release and its cache are keyed by: the framework version, the package definitions and the model. */
|
|
63
|
+
export function key() {
|
|
64
|
+
const pk = readdirSync(join(repo, "packages")).filter((n) => existsSync(join(repo, "packages", n, "package.json"))).sort()
|
|
65
|
+
.map((n) => readFileSync(join(repo, "packages", n, "package.json"), "utf8"));
|
|
66
|
+
return { framework: readFileSync(join(repo, "schema/VERSION"), "utf8").trim(), packages: sha(readFileSync(join(repo, "packages/catalog.json"), "utf8") + pk.join("\n")).slice(0, 16), model: process.env.MENTURA_MODEL ?? null };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** The local cache for a key: { entries, hits, misses, fresh, get(k, f), save() }. A different key starts it fresh. */
|
|
70
|
+
export function openCache(dir, k = key()) {
|
|
71
|
+
const f = join(dir, CACHE);
|
|
72
|
+
const kh = sha(JSON.stringify(k)).slice(0, 16);
|
|
73
|
+
let old = null;
|
|
74
|
+
try { old = existsSync(f) ? load(f) : null; } catch { old = null; }
|
|
75
|
+
const fresh = !old || old.key_hash !== kh;
|
|
76
|
+
const c = { key: k, key_hash: kh, fresh, previous_key: fresh && old ? old.key : null, entries: fresh ? {} : old.entries ?? {}, hits: 0, misses: 0 };
|
|
77
|
+
c.get = (id, compute) => { if (id in c.entries) { c.hits++; return c.entries[id]; } c.misses++; const v = compute(); c.entries[id] = v; return v; };
|
|
78
|
+
c.save = () => {
|
|
79
|
+
mkdirSync(dirname(f), { recursive: true });
|
|
80
|
+
if (!existsSync(join(dir, ".naive/.gitignore"))) writeFileSync(join(dir, ".naive/.gitignore"), "# Generated locally by Naive; never released or committed (C-17).\n*\n");
|
|
81
|
+
writeFileSync(f, JSON.stringify({ key_hash: kh, key: k, entries: c.entries }) + "\n");
|
|
82
|
+
};
|
|
83
|
+
return c;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ---- the venture's files (C-10) ----
|
|
87
|
+
|
|
88
|
+
/** Every file of a venture, relative: skips dot folders, nested ventures and _-folders other than _streams. */
|
|
89
|
+
export function ventureFiles(dir, sub = "") {
|
|
90
|
+
return readdirSync(join(dir, sub), { withFileTypes: true }).flatMap((e) => {
|
|
91
|
+
const rel = sub ? `${sub}/${e.name}` : e.name;
|
|
92
|
+
if (e.name.startsWith(".") || e.name === "node_modules") return [];
|
|
93
|
+
if (e.isDirectory()) return existsSync(join(dir, rel, "venture.json")) || (e.name.startsWith("_") && e.name !== "_streams") ? [] : ventureFiles(dir, rel);
|
|
94
|
+
return [rel];
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// The record routes, each with a matcher from a venture-relative file to the concrete route.
|
|
99
|
+
function matchers() {
|
|
100
|
+
return routes().filter((r) => r.kind === "record" && r.collection !== "list").flatMap((r) => r.files.map((g) => ({ r, re: glob(g), g })));
|
|
101
|
+
}
|
|
102
|
+
function routeOf(file, ms) {
|
|
103
|
+
const hit = ms.find((m) => m.re.test(file));
|
|
104
|
+
if (!hit) return null;
|
|
105
|
+
if (hit.r.collection !== "item") return { route: hit.r.route, template: hit.r.route, record: hit.r.record, r: hit.r };
|
|
106
|
+
const re = new RegExp("^" + hit.g.split("*").map((s) => s.replace(/[.+^${}()|[\]\\]/g, "\\$&")).join("([^/]*)") + "$");
|
|
107
|
+
const id = file.match(re)?.[1];
|
|
108
|
+
return { route: hit.r.route.replace("[id]", id), template: hit.r.route, record: hit.r.record, id, r: hit.r };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ---- confirmation (C-11, P-31) ----
|
|
112
|
+
|
|
113
|
+
const who = (a) => a?.id ?? (a?.kind === "person" ? `a person (via ${a.via ?? "cli"})` : a?.kind ?? "unknown");
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Whether a person confirmed this file as it is now: { by, at } or null with why. A person's write or
|
|
117
|
+
* accept, a person's baseline, or a confirm of every foundation statement in it, after the last
|
|
118
|
+
* agent write, and the file unchanged since the last entry (its sha256 matches).
|
|
119
|
+
*/
|
|
120
|
+
export function confirmation(dir, file, type, log) {
|
|
121
|
+
const es = log.filter((e) => e.entity === file);
|
|
122
|
+
if (!es.length) return { ok: false, why: "no provenance entry: written outside the router", fix: "baseline" };
|
|
123
|
+
const last = es.at(-1);
|
|
124
|
+
const now = sha(readFileSync(join(dir, file)));
|
|
125
|
+
if (last.sha256 && last.sha256 !== now) return { ok: false, why: "changed since its last provenance entry", fix: "rewrite" };
|
|
126
|
+
const statements = FRAME.questions.flatMap((q) => q.statements).filter((s) => s.record === type && s.id);
|
|
127
|
+
const ok = confirmedIn(dir, file, null) || (statements.length > 0 && statements.every((s) => confirmedIn(dir, file, s.id)));
|
|
128
|
+
if (!ok) return { ok: false, why: "an agent wrote it and no person has confirmed it", fix: statements.length ? "confirm" : "rewrite", statements: statements.map((s) => s.id) };
|
|
129
|
+
const by = [...es].reverse().find((e) => e.agent?.kind === "person" || e.activity === "accept");
|
|
130
|
+
return { ok: true, by: who(by?.agent), at: by?.at ?? last.at };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function fixFor(c, { route, file, venture }) {
|
|
134
|
+
if (c.fix === "baseline") return `${CLI} baseline --venture ${venture} --as person`;
|
|
135
|
+
if (c.fix === "confirm") return c.statements.map((s) => `${CLI} confirm ${s} --venture ${venture} --confirm ${s}`).join(" && ");
|
|
136
|
+
if (c.fix === "accept") return `${CLI} accept ${route} --venture ${venture}`;
|
|
137
|
+
return `${CLI} ${route} write --venture ${venture} --input ${file} --as person`;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ---- command routes (H-4) ----
|
|
141
|
+
|
|
142
|
+
/** Follows a script's relative imports and reports what runtime data its code reads. */
|
|
143
|
+
export function runtimeReads(script, seen = new Set()) {
|
|
144
|
+
const f = resolvePath(repo, script);
|
|
145
|
+
const reads = new Set();
|
|
146
|
+
const walk = (p) => {
|
|
147
|
+
if (seen.has(p) || !existsSync(p)) return;
|
|
148
|
+
seen.add(p);
|
|
149
|
+
const text = readFileSync(p, "utf8");
|
|
150
|
+
const code = text.replace(/^\s*\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "");
|
|
151
|
+
if (/Date\.now\(\)|new Date\(\)/.test(code)) reads.add("the clock");
|
|
152
|
+
if (/MENTURA_AGENT_CMD|["']claude["']|jev-latest|use_model\s*\(/.test(code)) reads.add("a model");
|
|
153
|
+
if (/\bfetch\(/.test(code)) reads.add("the network");
|
|
154
|
+
if (/_streams/.test(code)) reads.add("a stream");
|
|
155
|
+
for (const m of text.matchAll(/(?:from|import\()\s*["'](\.{1,2}\/[^"']+)["']/g)) walk(resolvePath(dirname(p), m[1]));
|
|
156
|
+
};
|
|
157
|
+
walk(f);
|
|
158
|
+
return [...reads].sort();
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** How each command route enters a release: frozen when it reads no runtime data, else a hole. */
|
|
162
|
+
export function commandPlan() {
|
|
163
|
+
return routes().filter((r) => r.kind === "command").map((r) => {
|
|
164
|
+
if (r.internal === "overview") return { route: r.route, in: "hole", why: "composes the live slots and the recent provenance" };
|
|
165
|
+
if (r.internal === "robots") return { route: r.route, in: "release", why: "reads only the team plan" };
|
|
166
|
+
if (!r.actions.includes("run")) return { route: r.route, in: "hole", why: "no run action" };
|
|
167
|
+
if (r.value_flag) return { route: r.route, in: "hole", why: "takes its input per call" };
|
|
168
|
+
if (!r.script) return { route: r.route, in: "hole", why: "computed inside the router at call time" };
|
|
169
|
+
const reads = runtimeReads(r.script);
|
|
170
|
+
return reads.length ? { route: r.route, in: "hole", why: `reads ${reads.join(", ")}`, reads } : { route: r.route, in: "release", why: "reads only the venture's records" };
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// ---- the survey: what is frozen, what is a hole, what is left out, what blocks ----
|
|
175
|
+
|
|
176
|
+
function stripPrivate(text, file) {
|
|
177
|
+
if (!file.endsWith(".json")) return { text, withheld: [] };
|
|
178
|
+
const o = JSON.parse(text);
|
|
179
|
+
const fields = Array.isArray(o.private_fields) ? o.private_fields : [];
|
|
180
|
+
if (!fields.length) return { text, withheld: [] };
|
|
181
|
+
for (const f of fields) delete o[f];
|
|
182
|
+
return { text: JSON.stringify(o, null, 2) + "\n", withheld: fields };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** Everything a release would hold, read from the venture as it is. Writes nothing. */
|
|
186
|
+
export async function survey(dir, { cache } = {}) {
|
|
187
|
+
const v = load(join(dir, "venture.json"));
|
|
188
|
+
const log = provEntries(dir);
|
|
189
|
+
const ms = matchers();
|
|
190
|
+
const files = ventureFiles(dir);
|
|
191
|
+
const frozen = [], holes = [], leftOut = [], blockers = [], privateCount = { records: 0, folder: 0 };
|
|
192
|
+
const streams = new Map();
|
|
193
|
+
for (const file of files) {
|
|
194
|
+
if (["venture.json", "provenance.jsonl", "AGENTS.md"].includes(file) || file.endsWith(".gitkeep")) continue;
|
|
195
|
+
const stream = file.match(/^(.*\/_streams\/[^/]+)\//);
|
|
196
|
+
if (stream) { if (!streams.has(stream[1])) streams.set(stream[1], file); continue; }
|
|
197
|
+
if (file.endsWith(".decision.json") && file.split("/").at(-2) === "decisions") {
|
|
198
|
+
const d = load(join(dir, file));
|
|
199
|
+
const ro = routeOf(file, ms);
|
|
200
|
+
if (d.status === "decided") frozen.push({ file, record: "decision-record", route: ro?.route ?? `decisions/${d.id}`, route_template: ro?.template ?? "decisions/[id]", ...(ro?.id ? { id: ro.id } : {}),
|
|
201
|
+
as_of: d.provenance?.approved_at ?? null, confirmed_by: d.provenance?.approved_by ?? d.decided_by ?? "a person" });
|
|
202
|
+
else if (["open", "deferred"].includes(d.status)) holes.push({ kind: "decision", name: d.id, file, record: "decision-record", route: ro?.route ?? `decisions/${d.id}`, route_template: ro?.template ?? "decisions/[id]",
|
|
203
|
+
decides: d.decides?.record ?? null, why: `open decision: ${d.question}`, live: `${CLI} governance/decisions run --venture ${v.id}`, to_freeze: `decide it: ${CLI} decide ${d.id} --venture ${v.id} --by <your name>` });
|
|
204
|
+
else leftOut.push({ file, why: `decision ${d.status}` });
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
const ro = routeOf(file, ms);
|
|
208
|
+
const type = ro?.record ?? null;
|
|
209
|
+
if (!type) { leftOut.push({ file, why: "not a record: notes and readmes never ship" }); continue; }
|
|
210
|
+
if (NOT_SHIPPED[type]) { leftOut.push({ file, why: NOT_SHIPPED[type] }); continue; }
|
|
211
|
+
if (LIVE[type]) { holes.push({ kind: type === "money-account" ? "money" : "stream", name: ro.id ?? file, file, record: type, route: ro.route, route_template: ro.template, why: LIVE[type], live: `${CLI} ${ro.route} read --venture ${v.id}`, to_freeze: "stays live by design: confirm a dated summary record to freeze a number" }); continue; }
|
|
212
|
+
const text = readFileSync(join(dir, file), "utf8");
|
|
213
|
+
let parsed = null;
|
|
214
|
+
if (file.endsWith(".json")) { try { parsed = JSON.parse(text); } catch { blockers.push({ file, route: ro.route, record: type, why: "not valid JSON", fix: `fix ${file}` }); continue; } }
|
|
215
|
+
if (parsed?.private === true) { privateCount.records++; continue; }
|
|
216
|
+
const c = confirmation(dir, file, type, log);
|
|
217
|
+
if (!c.ok) { blockers.push({ file, route: ro.route, record: type, why: c.why, fix: fixFor(c, { route: ro.route, file, venture: v.id }) }); continue; }
|
|
218
|
+
// C-14: an attribute mastered in another source is a hole at the field; the rest of the record is frozen.
|
|
219
|
+
let attrs = [];
|
|
220
|
+
try { attrs = file.endsWith(".json") ? attributeSources(dir, type, ro.r.collection === "item" && (type === "product" || type === "account") ? { [type]: ro.id } : {}) : []; } catch { attrs = []; }
|
|
221
|
+
if (ro.r.schema && file.endsWith(".json")) {
|
|
222
|
+
// The schema is checked on the whole record, with each attribute as its system of record holds it, as a read sees it.
|
|
223
|
+
let whole = parsed;
|
|
224
|
+
for (const a of attrs) {
|
|
225
|
+
try { const got = await (await connect(a.source, dir)).read(file); const val = got ? JSON.parse(got.content)[a.attribute] : undefined; if (val !== undefined) whole = { ...whole, [a.attribute]: val }; } catch {}
|
|
226
|
+
}
|
|
227
|
+
const wholeText = JSON.stringify(whole);
|
|
228
|
+
const bad = (cache ?? { get: (_, f) => f() }).get(`schema:${ro.r.schema}:${sha(wholeText)}`, () => {
|
|
229
|
+
const tmp = mkdtempSync(join(tmpdir(), "naive-release-check-"));
|
|
230
|
+
try {
|
|
231
|
+
writeFileSync(join(tmp, "record.json"), wholeText);
|
|
232
|
+
const p = spawnSync("python3", [join(repo, "schema/validate.py"), "--schema", ro.r.schema.replace(/^schema\//, ""), "--file", join(tmp, "record.json")], { encoding: "utf8" });
|
|
233
|
+
return p.status === 0 ? null : (p.stdout + p.stderr).trim().split("\n").slice(-3).join(" ");
|
|
234
|
+
} finally { rmSync(tmp, { recursive: true, force: true }); }
|
|
235
|
+
});
|
|
236
|
+
if (bad) { blockers.push({ file, route: ro.route, record: type, why: `does not match ${ro.r.schema}: ${bad}`, fix: `${CLI} ${ro.route} check --venture ${v.id}` }); continue; }
|
|
237
|
+
}
|
|
238
|
+
const { text: shipped, withheld } = stripPrivate(text, file);
|
|
239
|
+
let out = shipped;
|
|
240
|
+
if (attrs.length) {
|
|
241
|
+
const o = JSON.parse(out);
|
|
242
|
+
for (const a of attrs) {
|
|
243
|
+
delete o[a.attribute];
|
|
244
|
+
holes.push({ kind: "attribute", name: `${ro.route}.${a.attribute}`, file, record: type, field: a.attribute, route: ro.route, route_template: ro.template, why: `${type}.${a.attribute} is mastered in ${a.source.id}`, live: `${CLI} ${ro.route} read --venture ${v.id}`, to_freeze: "stays live: its system of record is elsewhere" });
|
|
245
|
+
}
|
|
246
|
+
out = JSON.stringify(o, null, 2) + "\n";
|
|
247
|
+
}
|
|
248
|
+
frozen.push({ file, record: type, route: ro.route, route_template: ro.template, ...(ro.id ? { id: ro.id } : {}), as_of: c.at, confirmed_by: c.by, text: out, ...(withheld.length ? { withheld_fields: withheld } : {}) });
|
|
249
|
+
}
|
|
250
|
+
for (const [d, first] of streams) {
|
|
251
|
+
const id = d.split("/").at(-1);
|
|
252
|
+
holes.push({ kind: "stream", name: d, file: first, record: "data-stream", stream: id, route: d, route_template: null, why: "a stream is append-only activity; it is never frozen", live: `read ${d}/ in the venture`, to_freeze: "stays live by design" });
|
|
253
|
+
}
|
|
254
|
+
if (existsSync(join(dir, "_private"))) privateCount.folder = ventureFilesAll(join(dir, "_private")).length;
|
|
255
|
+
// Drafts (P-31): never shipped. One with no confirmed record behind it blocks its package.
|
|
256
|
+
const drafts = existsSync(join(dir, "_drafts")) ? readdirSync(join(dir, "_drafts")).filter((f) => f.endsWith(".draft")) : [];
|
|
257
|
+
for (const d of drafts) {
|
|
258
|
+
const file = d.replace(/\.draft$/, "").replace(/__/g, "/");
|
|
259
|
+
const ro = routeOf(file, ms);
|
|
260
|
+
if (frozen.some((x) => x.file === file)) { leftOut.push({ file: `_drafts/${d}`, why: "a draft waits; the confirmed version ships" }); continue; }
|
|
261
|
+
if (blockers.some((x) => x.file === file)) continue;
|
|
262
|
+
blockers.push({ file: `_drafts/${d}`, route: ro?.route ?? file, record: ro?.record ?? null, draft: true, why: "a draft waits for a person and nothing confirmed stands behind it", fix: fixFor({ fix: "accept" }, { route: ro?.route ?? file, venture: v.id }) });
|
|
263
|
+
}
|
|
264
|
+
return { venture: v, frozen, holes, left_out: leftOut, blockers, private: privateCount };
|
|
265
|
+
}
|
|
266
|
+
function ventureFilesAll(d) { return readdirSync(d, { withFileTypes: true }).flatMap((e) => e.isDirectory() ? ventureFilesAll(join(d, e.name)) : [e.name]); }
|
|
267
|
+
|
|
268
|
+
// ---- packages (C-13, C-14) ----
|
|
269
|
+
|
|
270
|
+
/** Each open package: frozen, holes, gaps and blockers, whether it can be released, and its shell share. */
|
|
271
|
+
export function packagePlan(dir, s) {
|
|
272
|
+
const p = packages(dir);
|
|
273
|
+
return p.core.filter((x) => x.in === "now").map((pk) => {
|
|
274
|
+
const types = new Set(pk.records.filter((r) => r.role !== "reads").map((r) => r.record));
|
|
275
|
+
const mine = (x) => types.has(x.record) || (x.kind === "decision" && types.has(x.decides));
|
|
276
|
+
const frozen = s.frozen.filter(mine).map((x) => x.file);
|
|
277
|
+
const holes = s.holes.filter(mine).map((h) => h.name);
|
|
278
|
+
// Only a required record blocks: an unconfirmed optional one is left out of the release.
|
|
279
|
+
const required = new Set(pk.records.filter((r) => r.role === "required").map((r) => r.record));
|
|
280
|
+
const blockers = s.blockers.filter((b) => required.has(b.record));
|
|
281
|
+
const present = new Set([...s.frozen, ...s.blockers].map((x) => x.record));
|
|
282
|
+
const gaps = pk.records.filter((r) => r.role === "required" && !present.has(r.record) && !s.holes.some((h) => h.record === r.record)).map((r) => r.record);
|
|
283
|
+
const total = frozen.length + holes.length;
|
|
284
|
+
return { id: pk.id, title: pk.title, releasable: blockers.length === 0, frozen, holes, gaps, blockers: blockers.map((b) => b.file), shell: total ? Math.round((frozen.length / total) * 100) / 100 : null };
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// ---- running the frozen commands against the confirmed records alone (H-4, C-10) ----
|
|
289
|
+
|
|
290
|
+
async function frozenCommands(dir, v, frozen, plan, cache, log) {
|
|
291
|
+
const ws = mkdtempSync(join(tmpdir(), "naive-release-"));
|
|
292
|
+
const shadow = join(ws, v.id);
|
|
293
|
+
try {
|
|
294
|
+
mkdirSync(shadow, { recursive: true });
|
|
295
|
+
writeFileSync(join(shadow, "venture.json"), JSON.stringify(v, null, 2) + "\n");
|
|
296
|
+
for (const f of frozen) { mkdirSync(dirname(join(shadow, f.file)), { recursive: true }); writeFileSync(join(shadow, f.file), f.text ?? readFileSync(join(dir, f.file), "utf8")); }
|
|
297
|
+
// The log, so confirmations hold in the copy; the release's own entries are left out of the key.
|
|
298
|
+
const kept = log.filter((e) => !(e.activity === "release" && e.entity === MANIFEST));
|
|
299
|
+
writeFileSync(join(shadow, "provenance.jsonl"), log.map((e) => JSON.stringify(e)).join("\n") + (log.length ? "\n" : ""));
|
|
300
|
+
const inputs = sha(JSON.stringify([v, frozen.map((f) => [f.file, sha(f.text ?? "")]), kept.map((e) => e.hash)]));
|
|
301
|
+
const out = {};
|
|
302
|
+
for (const c of plan.filter((x) => x.in === "release")) {
|
|
303
|
+
const got = await cacheAsync(cache, `command:${c.route}:${inputs}`, async () => {
|
|
304
|
+
const r = await call({ venture: v.id, route: c.route, action: "run", workspace: ws, via: "release" });
|
|
305
|
+
if (!r.ok || r.needs_input) return { empty: r.ok ? "needs input before it runs" : String(r.error).split("\n")[0].replaceAll(shadow, ".").replaceAll(ws, ".") };
|
|
306
|
+
return { result: JSON.parse(JSON.stringify(r.result ?? null).replaceAll(shadow, ".").replaceAll(ws, ".")) };
|
|
307
|
+
});
|
|
308
|
+
out[c.route] = got;
|
|
309
|
+
}
|
|
310
|
+
return out;
|
|
311
|
+
} finally { rmSync(ws, { recursive: true, force: true }); }
|
|
312
|
+
}
|
|
313
|
+
async function cacheAsync(cache, id, compute) {
|
|
314
|
+
if (id in cache.entries) { cache.hits++; return cache.entries[id]; }
|
|
315
|
+
cache.misses++;
|
|
316
|
+
const v = await compute();
|
|
317
|
+
cache.entries[id] = v;
|
|
318
|
+
return v;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// ---- the manifest ----
|
|
322
|
+
|
|
323
|
+
function routeTable(s, plan, commands) {
|
|
324
|
+
const has = new Set(s.frozen.map((f) => f.route_template));
|
|
325
|
+
const holeTemplates = new Set(s.holes.filter((h) => h.kind !== "attribute").map((h) => h.route_template));
|
|
326
|
+
return routes().map((r) => {
|
|
327
|
+
if (r.kind === "command") {
|
|
328
|
+
const c = plan.find((x) => x.route === r.route);
|
|
329
|
+
if (c.in === "hole") return { route: r.route, kind: "command", in: "hole", why: c.why, live: `${CLI} ${r.route} run --venture ${s.venture.id}` };
|
|
330
|
+
const got = commands[r.route];
|
|
331
|
+
return { route: r.route, kind: "command", in: got?.empty ? "empty" : "release", why: got?.empty ?? c.why };
|
|
332
|
+
}
|
|
333
|
+
const base = { route: r.route, kind: "record", record: r.record, ...(r.collection ? { collection: r.collection } : {}) };
|
|
334
|
+
if (NOT_SHIPPED[r.record]) return { ...base, in: "left out", why: NOT_SHIPPED[r.record] };
|
|
335
|
+
if (LIVE[r.record]) return { ...base, in: "hole", why: LIVE[r.record], live: `${CLI} ${r.route.replace("/[id]", "")} read --venture ${s.venture.id}` };
|
|
336
|
+
const item = r.collection === "list" ? `${r.route}/[id]` : r.route;
|
|
337
|
+
if (has.has(item) || has.has(r.route) || holeTemplates.has(item)) return { ...base, in: "release" };
|
|
338
|
+
return { ...base, in: "empty", why: "no confirmed record when the release was built" };
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const releaseId = (m) => sha(JSON.stringify(sorted({ venture: m.venture, phase: m.phase, key: m.key, records: m.records, holes: m.holes, routes: m.routes, commands: m.commands, packages: m.packages }))).slice(0, 16);
|
|
343
|
+
|
|
344
|
+
/** The release card's data (P-25, O-7): name, phase, the headline numbers with their dates, and who confirmed them. */
|
|
345
|
+
export function cardOf(dir, m) {
|
|
346
|
+
const latest = (rs) => rs.map((r) => r.as_of).filter(Boolean).sort().at(-1) ?? null;
|
|
347
|
+
const people = (rs) => [...new Set(rs.map((r) => r.confirmed_by).filter(Boolean))];
|
|
348
|
+
const count = (label, type) => { const rs = m.records.filter((r) => r.record === type); return rs.length ? [{ label, value: rs.length, as_of: latest(rs), confirmed_by: people(rs) }] : []; };
|
|
349
|
+
const headline = [{ label: "Confirmed records", value: m.records.length, as_of: latest(m.records), confirmed_by: people(m.records) },
|
|
350
|
+
...count("Products", "product"), ...count("Customers", "customer"), ...count("Evidence", "evidence"), ...count("Decisions made", "decision-record")];
|
|
351
|
+
const pricing = m.records.find((r) => r.record === "validation-pricing");
|
|
352
|
+
if (pricing) {
|
|
353
|
+
const pt = (JSON.parse(frozenText(dir, pricing.file) ?? "{}").points ?? []).find((x) => typeof x.amount === "number");
|
|
354
|
+
if (pt) headline.push({ label: "Price", value: pt.amount, unit: [pt.currency, pt.per ?? pt.period].filter(Boolean).join(" per ") || null, as_of: pricing.as_of, confirmed_by: [pricing.confirmed_by] });
|
|
355
|
+
}
|
|
356
|
+
headline.push({ label: "Live holes", value: m.holes.length, as_of: m.now, confirmed_by: [] });
|
|
357
|
+
return { venture: m.venture, name: m.name, phase: m.phase, release: m.release, built_at: m.built_at, headline,
|
|
358
|
+
confirmed_by: people(m.records), packages: m.packages.map((p) => ({ title: p.title, shell: p.shell, gaps: p.gaps.length })),
|
|
359
|
+
note: "Assembled by code from the release's records. Drawing it is the interface's job (interface/release-card.mjs)." };
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function checks(dir) {
|
|
363
|
+
const fails = [];
|
|
364
|
+
for (const c of ["schema/check-provenance.mjs", "schema/check-private.mjs"]) {
|
|
365
|
+
const p = spawnSync("node", [join(repo, c), dir], { encoding: "utf8" });
|
|
366
|
+
if (p.status !== 0) fails.push({ check: c, output: (p.stderr || p.stdout).trim().split("\n").slice(0, 5).join("\n") });
|
|
367
|
+
}
|
|
368
|
+
return fails;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function writeRelease(dir, m, frozen) {
|
|
372
|
+
const stage = join(dir, RELEASE_DIR, `.stage-${process.pid}`);
|
|
373
|
+
rmSync(stage, { recursive: true, force: true });
|
|
374
|
+
for (const f of frozen) {
|
|
375
|
+
const dest = join(stage, "records", f.file);
|
|
376
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
377
|
+
writeFileSync(dest, f.text ?? readFileSync(join(dir, f.file), "utf8"));
|
|
378
|
+
}
|
|
379
|
+
mkdirSync(join(stage, "records"), { recursive: true });
|
|
380
|
+
rmSync(join(dir, RECORDS), { recursive: true, force: true });
|
|
381
|
+
renameSync(join(stage, "records"), join(dir, RECORDS));
|
|
382
|
+
rmSync(stage, { recursive: true, force: true });
|
|
383
|
+
writeFileSync(join(dir, MANIFEST), JSON.stringify(m, null, 2) + "\n");
|
|
384
|
+
writeFileSync(join(dir, CARD), JSON.stringify(cardOf(dir, m), null, 2) + "\n");
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const strip = ({ text, ...rest }) => ({ ...rest, sha256: sha(text ?? "") });
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* naive build: survey the venture, check every open package can be released, and write the release.
|
|
391
|
+
* Returns { ok, manifest } or { ok: false, blockers, failed_checks, packages }.
|
|
392
|
+
*/
|
|
393
|
+
export async function build(dir, { now = new Date().toISOString(), actor = "person", dryRun = false } = {}) {
|
|
394
|
+
dir = resolvePath(dir);
|
|
395
|
+
if (!existsSync(join(dir, "venture.json"))) return { ok: false, error: `${dir} is not a venture (no venture.json)` };
|
|
396
|
+
const cache = openCache(dir);
|
|
397
|
+
const s = await survey(dir, { cache });
|
|
398
|
+
const pk = packagePlan(dir, s);
|
|
399
|
+
const failed = checks(dir);
|
|
400
|
+
const blocking = pk.filter((p) => !p.releasable);
|
|
401
|
+
const blockers = s.blockers.filter((b) => blocking.some((p) => p.blockers.includes(b.file)));
|
|
402
|
+
// An unconfirmed record outside every open package is left out, not a blocker.
|
|
403
|
+
const leftOut = [...s.left_out, ...s.blockers.filter((b) => !blockers.includes(b)).map((b) => ({ file: b.file, why: `${b.why}; not required by an open package, so left out`, fix: b.fix }))];
|
|
404
|
+
if (blockers.length || failed.length) { cache.save(); return { ok: false, blockers, failed_checks: failed, packages: pk }; }
|
|
405
|
+
const plan = commandPlan();
|
|
406
|
+
const log = provEntries(dir);
|
|
407
|
+
const commands = await frozenCommands(dir, s.venture, s.frozen, plan, cache, log);
|
|
408
|
+
const m = {
|
|
409
|
+
release: null, venture: s.venture.id, name: s.venture.name, phase: s.venture.phase,
|
|
410
|
+
built_at: stamp(Date.now()), now: stamp(now), key: cache.key,
|
|
411
|
+
records: s.frozen.map(strip).sort((a, b) => a.file.localeCompare(b.file)),
|
|
412
|
+
holes: s.holes.sort((a, b) => a.name.localeCompare(b.name)),
|
|
413
|
+
left_out: leftOut.sort((a, b) => a.file.localeCompare(b.file)), withheld: { private_records: s.private.records, private_folder_files: s.private.folder },
|
|
414
|
+
packages: pk, routes: routeTable(s, plan, commands),
|
|
415
|
+
items: Object.fromEntries([...new Set(s.frozen.filter((f) => f.id).map((f) => f.route_template))].sort().map((t) => [t, s.frozen.filter((f) => f.route_template === t).map((f) => f.id).sort()])),
|
|
416
|
+
commands: Object.fromEntries(Object.entries(commands).filter(([, v]) => !v.empty).map(([k, v]) => [k, { result: v.result }])),
|
|
417
|
+
incremental: [],
|
|
418
|
+
cache: { key_hash: cache.key_hash, fresh: cache.fresh, hits: cache.hits, misses: cache.misses, ...(cache.previous_key ? { previous_key: cache.previous_key } : {}) },
|
|
419
|
+
};
|
|
420
|
+
m.release = releaseId(m);
|
|
421
|
+
for (const c of Object.values(m.commands)) c.built_from = m.release;
|
|
422
|
+
cache.save();
|
|
423
|
+
if (dryRun) return { ok: true, manifest: m, dry_run: true };
|
|
424
|
+
writeRelease(dir, m, s.frozen);
|
|
425
|
+
prov(dir, { entity: MANIFEST, route: "system/release", activity: "release", agent: agentFromEnv(actor === "agent" ? "agent" : "person", "cli"), note: `release ${m.release}: ${m.records.length} records frozen, ${m.holes.length} live holes` });
|
|
426
|
+
return { ok: true, manifest: m };
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/** C-18: adds one item (a new customer, a new product) to the release, leaving the rest as built. */
|
|
430
|
+
export async function add(dir, route, { now = new Date().toISOString(), actor = "person" } = {}) {
|
|
431
|
+
dir = resolvePath(dir);
|
|
432
|
+
const m = readRelease(dir);
|
|
433
|
+
if (!m) return { ok: false, error: `no release in ${dir}: build one first` };
|
|
434
|
+
const r = routes().find((x) => x.kind === "record" && x.collection === "item" && route.startsWith(x.route.slice(0, -4)) && !route.slice(x.route.length - 4).includes("/"));
|
|
435
|
+
if (!r) return { ok: false, error: `${route} is not a collection item route (such as product/<id>)` };
|
|
436
|
+
const id = route.slice(r.route.length - 4);
|
|
437
|
+
if (m.records.some((x) => x.route === route)) return { ok: false, error: `${route} is already in release ${m.release}; a full build updates it` };
|
|
438
|
+
const file = r.files.map((g) => g.replace("*", id)).find((f) => existsSync(join(dir, f)));
|
|
439
|
+
if (!file) return { ok: false, error: `no ${r.record} ${id} in the venture` };
|
|
440
|
+
const s = await survey(dir);
|
|
441
|
+
const hit = s.frozen.find((f) => f.file === file);
|
|
442
|
+
if (!hit) {
|
|
443
|
+
const b = s.blockers.find((x) => x.file === file);
|
|
444
|
+
return { ok: false, error: `${route} cannot be added: ${b?.why ?? "it is not a record that ships"}`, ...(b ? { fix: b.fix } : {}) };
|
|
445
|
+
}
|
|
446
|
+
const before = m.release;
|
|
447
|
+
m.records = [...m.records, strip(hit)].sort((a, b) => a.file.localeCompare(b.file));
|
|
448
|
+
m.holes = [...m.holes, ...s.holes.filter((h) => h.file === file && !m.holes.some((x) => x.name === h.name))].sort((a, b) => a.name.localeCompare(b.name));
|
|
449
|
+
m.items[r.route] = [...new Set([...(m.items[r.route] ?? []), id])].sort();
|
|
450
|
+
for (const t of [r.route, r.route.slice(0, -5)]) { const e = m.routes.find((x) => x.route === t); if (e && e.in === "empty") { e.in = "release"; delete e.why; } }
|
|
451
|
+
for (const p of m.packages) if (HOME[r.record] === p.id) { p.frozen = [...p.frozen, file]; p.gaps = p.gaps.filter((g) => g !== r.record); const t = p.frozen.length + p.holes.length; p.shell = t ? Math.round((p.frozen.length / t) * 100) / 100 : null; }
|
|
452
|
+
m.incremental = [...(m.incremental ?? []), { route, file, from: before, at: stamp(now) }];
|
|
453
|
+
m.release = releaseId(m);
|
|
454
|
+
mkdirSync(dirname(join(dir, RECORDS, file)), { recursive: true });
|
|
455
|
+
writeFileSync(join(dir, RECORDS, file), hit.text);
|
|
456
|
+
writeFileSync(join(dir, MANIFEST), JSON.stringify(m, null, 2) + "\n");
|
|
457
|
+
writeFileSync(join(dir, CARD), JSON.stringify(cardOf(dir, m), null, 2) + "\n");
|
|
458
|
+
prov(dir, { entity: MANIFEST, route: "system/release", activity: "release", agent: agentFromEnv(actor === "agent" ? "agent" : "person", "cli"), note: `release ${m.release}: added ${route} to ${before} without rebuilding the rest` });
|
|
459
|
+
return { ok: true, manifest: m, added: { route, file, from: before } };
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// ---- export (Z-3) ----
|
|
463
|
+
|
|
464
|
+
function mdOf(x, depth = 0) {
|
|
465
|
+
const pad = " ".repeat(depth);
|
|
466
|
+
if (x === null || x === undefined || x === "") return "";
|
|
467
|
+
if (typeof x !== "object") return String(x);
|
|
468
|
+
if (Array.isArray(x)) return x.map((e) => typeof e === "object" && e ? `${pad}-\n${mdOf(e, depth + 1)}` : `${pad}- ${e}`).join("\n");
|
|
469
|
+
return Object.entries(x).filter(([, v]) => v !== null && v !== "" && !(Array.isArray(v) && !v.length)).map(([k, v]) => typeof v === "object" ? `${pad}- **${k}**\n${mdOf(v, depth + 1)}` : `${pad}- **${k}**: ${v}`).join("\n");
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/** Z-3: the release as plain files in out/: Markdown and JSON, no live holes, every number dated. */
|
|
473
|
+
export function exportRelease(dir, out) {
|
|
474
|
+
dir = resolvePath(dir); out = resolvePath(out);
|
|
475
|
+
const m = readRelease(dir);
|
|
476
|
+
if (!m) return { ok: false, error: `no release in ${dir}: build one first` };
|
|
477
|
+
if (existsSync(out) && readdirSync(out).length) return { ok: false, error: `${out} exists and is not empty: an export never writes over files` };
|
|
478
|
+
mkdirSync(out, { recursive: true });
|
|
479
|
+
const card = JSON.parse(readFileSync(join(dir, CARD), "utf8"));
|
|
480
|
+
const lines = [`# ${m.name}`, "", `Phase: ${m.phase}. Release ${m.release}, built ${m.built_at} as of ${m.now}.`, "", "## Headline numbers", "", "| Number | Value | As of | Confirmed by |", "| --- | --- | --- | --- |",
|
|
481
|
+
...card.headline.filter((h) => h.label !== "Live holes").map((h) => `| ${h.label} | ${h.value}${h.unit ? ` ${h.unit}` : ""} | ${h.as_of ?? m.now} | ${h.confirmed_by.join(", ") || "none"} |`), "", "## Records", "", "| Record | As of | Confirmed by |", "| --- | --- | --- |"];
|
|
482
|
+
for (const r of m.records) {
|
|
483
|
+
const text = frozenText(dir, r.file);
|
|
484
|
+
mkdirSync(dirname(join(out, "records", r.file)), { recursive: true });
|
|
485
|
+
writeFileSync(join(out, "records", r.file), text);
|
|
486
|
+
if (r.file.endsWith(".json")) writeFileSync(join(out, "records", r.file.replace(/\.json$/, ".md")), `# ${r.route}\n\nAs of ${r.as_of}, confirmed by ${r.confirmed_by}. Frozen in release ${m.release}.\n\n${mdOf(JSON.parse(text))}\n`);
|
|
487
|
+
lines.push(`| [${r.route}](records/${r.file.endsWith(".json") ? r.file.replace(/\.json$/, ".md") : r.file}) | ${r.as_of} | ${r.confirmed_by} |`);
|
|
488
|
+
}
|
|
489
|
+
lines.push("", `Not in this export: ${m.holes.length} live items (open decisions, streams, money and fields mastered elsewhere), read in the venture itself.`, "");
|
|
490
|
+
writeFileSync(join(out, "README.md"), lines.join("\n"));
|
|
491
|
+
const { holes, routes: rs, commands, left_out, cache, ...rest } = m;
|
|
492
|
+
const exported = { ...rest, routes: rs.filter((r) => r.in === "release").map((r) => r.route), export: { at: stamp(Date.now()), as_of: m.now, live_left_out: holes.length, holes: 0 } };
|
|
493
|
+
writeFileSync(join(out, "release.json"), JSON.stringify(exported, null, 2) + "\n");
|
|
494
|
+
writeFileSync(join(out, "card.json"), JSON.stringify({ ...card, headline: card.headline.filter((h) => h.label !== "Live holes") }, null, 2) + "\n");
|
|
495
|
+
return { ok: true, out, records: m.records.length, left_out_live: holes.length };
|
|
496
|
+
}
|
|
497
|
+
|