@zuzuucodes/cli 1.4.0 → 1.6.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/bin/zuzuu.mjs +20 -4
- package/package.json +1 -1
- package/web-app/dist/auth.js +91 -0
- package/web-app/dist/server.js +16 -79
- package/web-app/dist/zuzuu-cli.js +124 -0
- package/web-app/dist/{zuzuu-api.js → zuzuu-routes.js} +158 -133
- package/web-app/web-dist/assets/CommandPalette-DhBdR7X3.js +45 -0
- package/web-app/web-dist/assets/DiffTab-CqxwSjI2.js +1 -0
- package/web-app/web-dist/assets/EditorPane-94QPFR9R.js +41 -0
- package/web-app/web-dist/assets/MonacoFile-D76epTrG.js +1 -0
- package/web-app/web-dist/assets/angular-html-BVBpGdXr.js +1 -0
- package/web-app/web-dist/assets/{angular-ts-CD_OonCa.js → angular-ts-BfdufMKP.js} +1 -1
- package/web-app/web-dist/assets/{apl-uOGC3x4e.js → apl-DWBSSoBH.js} +1 -1
- package/web-app/web-dist/assets/{astro-B6ybQmWG.js → astro-3LtMP0Sq.js} +1 -1
- package/web-app/web-dist/assets/{blade-B1QGRlVx.js → blade-llJRbbtR.js} +1 -1
- package/web-app/web-dist/assets/c-Wt1voDr2.js +1 -0
- package/web-app/web-dist/assets/{cobol-BgqgtYWn.js → cobol-x_HIyl2P.js} +1 -1
- package/web-app/web-dist/assets/{coffee-0wIRKYlr.js → coffee-CThvmt4R.js} +1 -1
- package/web-app/web-dist/assets/cpp-NtAeskI3.js +1 -0
- package/web-app/web-dist/assets/{crystal-CyTK3qFN.js → crystal-DNu_sX0G.js} +1 -1
- package/web-app/web-dist/assets/css-DJp_X0uY.js +1 -0
- package/web-app/web-dist/assets/{cssMode-B9jnrWOz.js → cssMode-ByQBaInt.js} +1 -1
- package/web-app/web-dist/assets/dist-DQqjtuhV.js +153 -0
- package/web-app/web-dist/assets/{edge-CvML9pwC.js → edge-ozw5tpLl.js} +1 -1
- package/web-app/web-dist/assets/{editor.api2-BmGoRSl4.js → editor.api2-C7skgoRB.js} +1 -1
- package/web-app/web-dist/assets/{elixir-CrjqTiSc.js → elixir-VhA6FeZt.js} +1 -1
- package/web-app/web-dist/assets/{elm-C4JtJ0Au.js → elm-dREJmIFz.js} +1 -1
- package/web-app/web-dist/assets/{erb-Cmeb-29V.js → erb-CIg6G69l.js} +1 -1
- package/web-app/web-dist/assets/{freemarker2-B5LAi19B.js → freemarker2-CBBwP9JV.js} +1 -1
- package/web-app/web-dist/assets/{git-rebase-CXqdToiP.js → git-rebase-B44mJPta.js} +1 -1
- package/web-app/web-dist/assets/{glimmer-js-Kq-kdTyV.js → glimmer-js-vH_gHG0-.js} +1 -1
- package/web-app/web-dist/assets/{glimmer-ts-D0RKLJNf.js → glimmer-ts--abOzSAQ.js} +1 -1
- package/web-app/web-dist/assets/glsl-Dv5r7kPw.js +1 -0
- package/web-app/web-dist/assets/graphql-CB4jsw2E.js +1 -0
- package/web-app/web-dist/assets/{hack-trjVF3Po.js → hack-DvEYX148.js} +1 -1
- package/web-app/web-dist/assets/haml-zE6W3STP.js +1 -0
- package/web-app/web-dist/assets/{handlebars-B8_x7Zx7.js → handlebars-CzBR2SDs.js} +1 -1
- package/web-app/web-dist/assets/{handlebars-g7ZhGhI_.js → handlebars-tXdfxEd6.js} +1 -1
- package/web-app/web-dist/assets/html-C8UlPnhE.js +1 -0
- package/web-app/web-dist/assets/{html-CfvRMgoC.js → html-DgPn1QYH.js} +1 -1
- package/web-app/web-dist/assets/{html-derivative-BYX_F_XH.js → html-derivative-CY6NRz-J.js} +1 -1
- package/web-app/web-dist/assets/{htmlMode-Bi8vSvwb.js → htmlMode-BtdIDgA2.js} +1 -1
- package/web-app/web-dist/assets/{http-BIVDpHT-.js → http-Cyd7bS_S.js} +1 -1
- package/web-app/web-dist/assets/{hurl-CFsshMju.js → hurl-CWPsiEpf.js} +1 -1
- package/web-app/web-dist/assets/index-B27_WOhS.css +2 -0
- package/web-app/web-dist/assets/index-De6DWTZM.js +7 -0
- package/web-app/web-dist/assets/java-CGc3VwQr.js +1 -0
- package/web-app/web-dist/assets/{javascript-Bxx2wV4w.js → javascript-5m05n-Be.js} +1 -1
- package/web-app/web-dist/assets/javascript-CUt1pgmJ.js +1 -0
- package/web-app/web-dist/assets/{jinja-_ZS5zWwe.js → jinja-CD-Z-FLd.js} +1 -1
- package/web-app/web-dist/assets/{jison-D8mMEpcs.js → jison-imPNup1l.js} +1 -1
- package/web-app/web-dist/assets/json-Bg9ijW3F.js +1 -0
- package/web-app/web-dist/assets/{jsonMode-C6ELX5GM.js → jsonMode-BG32YnTY.js} +1 -1
- package/web-app/web-dist/assets/jsx-CY6oMTks.js +1 -0
- package/web-app/web-dist/assets/{julia-D4h2DZrs.js → julia-Dc3O-irA.js} +1 -1
- package/web-app/web-dist/assets/{just-bMqQi3xg.js → just-BhOq_Kbv.js} +1 -1
- package/web-app/web-dist/assets/{latex-DThYi3CX.js → latex-Cu4Y1d5w.js} +1 -1
- package/web-app/web-dist/assets/lib-KIOQTlcs.js +1 -0
- package/web-app/web-dist/assets/{liquid-CUjzzP4r.js → liquid-3ZnQzTbs.js} +1 -1
- package/web-app/web-dist/assets/{liquid-CesB-zzl.js → liquid-CvXMrjlQ.js} +1 -1
- package/web-app/web-dist/assets/{lspLanguageFeatures-gTnJsses.js → lspLanguageFeatures-6KXALSrl.js} +1 -1
- package/web-app/web-dist/assets/lua-BjLEUjKY.js +1 -0
- package/web-app/web-dist/assets/{marko-yoGoLK2m.js → marko-DvhNOisQ.js} +1 -1
- package/web-app/web-dist/assets/{mdc-BvtXU6eH.js → mdc-Bm9TpL1X.js} +1 -1
- package/web-app/web-dist/assets/{mdx-DrXGQbNB.js → mdx-DffTEkNE.js} +1 -1
- package/web-app/web-dist/assets/{monaco-setup-CsR6EfHe.js → monaco-setup-DM3A5_VI.js} +3 -3
- package/web-app/web-dist/assets/{nginx-DoUz032F.js → nginx-Bhc82uuv.js} +1 -1
- package/web-app/web-dist/assets/{nim-B0Pl8B4R.js → nim-DXTVBFnF.js} +1 -1
- package/web-app/web-dist/assets/{perl-D2tfAALb.js → perl-C7veXV9z.js} +1 -1
- package/web-app/web-dist/assets/{php-BImCcX5X.js → php-BRiuMnnr.js} +1 -1
- package/web-app/web-dist/assets/{pug-BcnpC8P_.js → pug-C5hz5LQ7.js} +1 -1
- package/web-app/web-dist/assets/{python-ypRCBnvu.js → python-DyLAD3Wt.js} +1 -1
- package/web-app/web-dist/assets/{qml-DFDAunHY.js → qml-BdUV3aTS.js} +1 -1
- package/web-app/web-dist/assets/r-8R7vtdQc.js +1 -0
- package/web-app/web-dist/assets/{razor-aqrhpwqZ.js → razor-C49xQTPQ.js} +1 -1
- package/web-app/web-dist/assets/{razor-1_376SZM.js → razor-DRL52XO2.js} +1 -1
- package/web-app/web-dist/assets/react-vendor-CCIEwYL0.js +9 -0
- package/web-app/web-dist/assets/regexp-Omp9DhTb.js +1 -0
- package/web-app/web-dist/assets/{rst-2vG6f11Y.js → rst-BHX71KW9.js} +1 -1
- package/web-app/web-dist/assets/{ruby-Dj6bCFXR.js → ruby-B--HzjGU.js} +1 -1
- package/web-app/web-dist/assets/{sas-BhVZ4qL2.js → sas-DrLaYOK_.js} +1 -1
- package/web-app/web-dist/assets/scss-DdSxiZKl.js +1 -0
- package/web-app/web-dist/assets/shellscript-DwcUjJBL.js +1 -0
- package/web-app/web-dist/assets/{shellsession-CyO2fnhB.js → shellsession-CPZkydE6.js} +1 -1
- package/web-app/web-dist/assets/{soy-DIkw6E88.js → soy-Br5FhD7c.js} +1 -1
- package/web-app/web-dist/assets/sql-DNssxck8.js +1 -0
- package/web-app/web-dist/assets/{stata-DvkM932O.js → stata-DXn1tqOr.js} +1 -1
- package/web-app/web-dist/assets/{surrealql-B4-Q8tqV.js → surrealql-IeLNQw0f.js} +1 -1
- package/web-app/web-dist/assets/{svelte-p6yBy-Ki.js → svelte-DOdLCIlh.js} +1 -1
- package/web-app/web-dist/assets/{templ-C7EkuiZr.js → templ-CIwIngms.js} +1 -1
- package/web-app/web-dist/assets/{tex-DkmD8uFC.js → tex-D8QMumu5.js} +1 -1
- package/web-app/web-dist/assets/{ts-tags-U-hncHg4.js → ts-tags-BMVY4q-l.js} +1 -1
- package/web-app/web-dist/assets/{tsMode-a8OvovQd.js → tsMode-BndVBac5.js} +1 -1
- package/web-app/web-dist/assets/tsx-5Eka4NBX.js +1 -0
- package/web-app/web-dist/assets/{twig-CU0OP-IA.js → twig-C8o_5mgw.js} +1 -1
- package/web-app/web-dist/assets/{typescript-DnLjiKtn.js → typescript-B1w9vqKF.js} +1 -1
- package/web-app/web-dist/assets/typescript-DOu2WMV5.js +1 -0
- package/web-app/web-dist/assets/{vue-Db7nY3ba.js → vue-BU18DNDL.js} +1 -1
- package/web-app/web-dist/assets/{vue-html-BvAbiAw1.js → vue-html-BeluIYX0.js} +1 -1
- package/web-app/web-dist/assets/{vue-vine-BEaIQIlA.js → vue-vine-DGUAbOCX.js} +1 -1
- package/web-app/web-dist/assets/{xml-an4Nuuqq.js → xml-D8uAlVv5.js} +1 -1
- package/web-app/web-dist/assets/xml-DIqSwXR3.js +1 -0
- package/web-app/web-dist/assets/{xsl-D3NQgH22.js → xsl-Ct_-YIAy.js} +1 -1
- package/web-app/web-dist/assets/xterm-B1ffpRuj.js +36 -0
- package/web-app/web-dist/assets/xterm-addons-psDEiUMC.js +136 -0
- package/web-app/web-dist/assets/{yaml-Diiu6O9P.js → yaml-Bb7jXyQv.js} +1 -1
- package/web-app/web-dist/assets/yaml-DTtCYNlS.js +1 -0
- package/web-app/web-dist/index.html +6 -3
- package/zuzuu/actions/convert.mjs +10 -9
- package/zuzuu/actions/dispatch.mjs +12 -7
- package/zuzuu/actions/inbox.mjs +5 -5
- package/zuzuu/actions/manifest.mjs +48 -30
- package/zuzuu/actions/schema.mjs +9 -3
- package/zuzuu/actions/trail.mjs +1 -1
- package/zuzuu/commands/act-author.mjs +23 -13
- package/zuzuu/commands/act.mjs +4 -6
- package/zuzuu/commands/capture.mjs +2 -2
- package/zuzuu/commands/code.mjs +2 -2
- package/zuzuu/commands/digest.mjs +2 -2
- package/zuzuu/commands/distill.mjs +15 -16
- package/zuzuu/commands/doctor.mjs +41 -19
- package/zuzuu/commands/enable.mjs +1 -1
- package/zuzuu/commands/eval.mjs +3 -36
- package/zuzuu/commands/explain.mjs +4 -4
- package/zuzuu/commands/faculty.mjs +158 -0
- package/zuzuu/commands/generation.mjs +5 -8
- package/zuzuu/commands/hook.mjs +14 -12
- package/zuzuu/commands/inbox.mjs +1 -6
- package/zuzuu/commands/init.mjs +18 -4
- package/zuzuu/commands/knowledge.mjs +1 -1
- package/zuzuu/commands/migrations/home.mjs +96 -0
- package/zuzuu/commands/migrations/index.mjs +48 -0
- package/zuzuu/commands/migrations/items.mjs +360 -0
- package/zuzuu/commands/migrations/proposals.mjs +100 -0
- package/zuzuu/commands/proposals.mjs +131 -0
- package/zuzuu/commands/review.mjs +13 -227
- package/zuzuu/commands/session.mjs +8 -2
- package/zuzuu/commands/sessions.mjs +159 -0
- package/zuzuu/commands/status.mjs +3 -3
- package/zuzuu/commands/trace.mjs +1 -1
- package/zuzuu/{capture-core.mjs → core/capture-core.mjs} +3 -3
- package/zuzuu/{store.mjs → core/store.mjs} +1 -1
- package/zuzuu/digest/compose.mjs +96 -0
- package/zuzuu/eval/score.mjs +14 -1
- package/zuzuu/faculties/actions/index.mjs +283 -0
- package/zuzuu/faculties/guardrails/index.mjs +320 -0
- package/zuzuu/faculties/instructions/index.mjs +288 -0
- package/zuzuu/faculties/knowledge/index.mjs +185 -0
- package/zuzuu/faculties/memory/index.mjs +124 -0
- package/zuzuu/faculty/envelope.mjs +290 -0
- package/zuzuu/faculty/generation/read.mjs +206 -0
- package/zuzuu/faculty/generation/write.mjs +207 -0
- package/zuzuu/faculty/items.mjs +81 -0
- package/zuzuu/faculty/module.mjs +74 -0
- package/zuzuu/faculty/pending.mjs +63 -0
- package/zuzuu/faculty/registry.mjs +204 -18
- package/zuzuu/faculty/render.mjs +59 -0
- package/zuzuu/faculty/trail.mjs +1 -1
- package/zuzuu/guardrails/engine.mjs +137 -0
- package/zuzuu/{scaffold.mjs → home/scaffold.mjs} +110 -39
- package/zuzuu/knowledge/items.mjs +56 -91
- package/zuzuu/live/install.mjs +1 -1
- package/zuzuu/live/live-store.mjs +2 -2
- package/zuzuu/live/reconcile.mjs +2 -2
- package/zuzuu/sessions/git.mjs +47 -0
- package/zuzuu/{session-git.mjs → sessions/session-git.mjs} +5 -43
- package/web-app/web-dist/assets/DiffTab-BuWonUNJ.js +0 -1
- package/web-app/web-dist/assets/MonacoFile-CL3DhFKG.js +0 -1
- package/web-app/web-dist/assets/angular-html-CmT26mqM.js +0 -1
- package/web-app/web-dist/assets/c-BvoqrSVH.js +0 -1
- package/web-app/web-dist/assets/cpp-BXsk94m0.js +0 -1
- package/web-app/web-dist/assets/css-Z8oOGxII.js +0 -1
- package/web-app/web-dist/assets/dist-ChcDQ_7s.js +0 -153
- package/web-app/web-dist/assets/glsl-KwyfU2aa.js +0 -1
- package/web-app/web-dist/assets/graphql-DSeOUAa2.js +0 -1
- package/web-app/web-dist/assets/haml-azVoxQRV.js +0 -1
- package/web-app/web-dist/assets/html-D_7P5S4m.js +0 -1
- package/web-app/web-dist/assets/index--5yy8RbA.js +0 -267
- package/web-app/web-dist/assets/index-BVG4hgk7.css +0 -2
- package/web-app/web-dist/assets/java-D4RbCvBe.js +0 -1
- package/web-app/web-dist/assets/javascript-Cb010CKM.js +0 -1
- package/web-app/web-dist/assets/json-DWgqV4D1.js +0 -1
- package/web-app/web-dist/assets/jsx-CZjSJa1f.js +0 -1
- package/web-app/web-dist/assets/lua-TGj_6NzO.js +0 -1
- package/web-app/web-dist/assets/r-fCpuAR7u.js +0 -1
- package/web-app/web-dist/assets/regexp-B4yxx-Ty.js +0 -1
- package/web-app/web-dist/assets/scss-QdjMO_xV.js +0 -1
- package/web-app/web-dist/assets/shellscript-BnlgeVVx.js +0 -1
- package/web-app/web-dist/assets/sql-DGnQv6iD.js +0 -1
- package/web-app/web-dist/assets/tsx-MJ0-9sYG.js +0 -1
- package/web-app/web-dist/assets/typescript-C17ZkDe8.js +0 -1
- package/web-app/web-dist/assets/xml-CA9lHFQV.js +0 -1
- package/web-app/web-dist/assets/yaml-CwRYMJka.js +0 -1
- package/zuzuu/actions/adapter.mjs +0 -130
- package/zuzuu/commands/migrate.mjs +0 -225
- package/zuzuu/digest.mjs +0 -149
- package/zuzuu/faculty/generation.mjs +0 -392
- package/zuzuu/guardrails/adapter.mjs +0 -134
- package/zuzuu/guardrails.mjs +0 -89
- package/zuzuu/instructions/adapter.mjs +0 -93
- package/zuzuu/knowledge/adapter.mjs +0 -99
- package/zuzuu/memory/adapter.mjs +0 -121
- package/zuzuu/miners/actions.mjs +0 -118
- package/zuzuu/miners/guardrails.mjs +0 -179
- package/zuzuu/miners/instructions.mjs +0 -157
- package/zuzuu/miners/knowledge.mjs +0 -25
- package/zuzuu/miners/memory.mjs +0 -27
- package/zuzuu/miners/registry.mjs +0 -31
- /package/web-app/web-dist/assets/{chunk-QTnfLwEv.js → rolldown-runtime-QTnfLwEv.js} +0 -0
- /package/zuzuu/{session.mjs → core/session.mjs} +0 -0
- /package/zuzuu/{inject.mjs → home/inject.mjs} +0 -0
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
// zuzuu/faculty/envelope.mjs — the Faculty Standard envelope (W24).
|
|
2
|
+
//
|
|
3
|
+
// ONE storage format across all five faculties: one file per item, markdown
|
|
4
|
+
// prose + a strict constrained-YAML frontmatter we control on both ends (no
|
|
5
|
+
// YAML lib — grown from the knowledge items grammar):
|
|
6
|
+
//
|
|
7
|
+
// ---
|
|
8
|
+
// id: test-command # required, slug
|
|
9
|
+
// faculty: knowledge # required, one of the 5
|
|
10
|
+
// kind: command # required; per-faculty kinds (FACULTY_KINDS)
|
|
11
|
+
// title: "Test command" # required, single line
|
|
12
|
+
// status: active # active | archived
|
|
13
|
+
// created_at: 2026-06-12T00:00:00Z
|
|
14
|
+
// updated_at: … # optional
|
|
15
|
+
// provenance: # optional list of flat maps {session, ref}
|
|
16
|
+
// - session: ses_abc
|
|
17
|
+
// ref: occurrences=12
|
|
18
|
+
// payload: # faculty-typed machine fields
|
|
19
|
+
// type: command # scalar
|
|
20
|
+
// attributes: # one-level map of scalars
|
|
21
|
+
// command: npm test
|
|
22
|
+
// relations: # list of flat maps
|
|
23
|
+
// - type: relates-to
|
|
24
|
+
// target: ci-pipeline
|
|
25
|
+
// ---
|
|
26
|
+
// <markdown prose body>
|
|
27
|
+
//
|
|
28
|
+
// Grammar (deliberately small): top-level scalar keys; `provenance` = a list of
|
|
29
|
+
// flat maps; `payload` = a block of scalars, one-level maps of scalars, lists
|
|
30
|
+
// of flat maps, or lists of scalars. Values are single-line strings (JSON
|
|
31
|
+
// double-quoting when they carry specials — round-trip exact, incl. backslashes
|
|
32
|
+
// in guardrail regexes). Anything outside this grammar is a parse error.
|
|
33
|
+
//
|
|
34
|
+
// API: parseEnvelope(text) → {ok, item, errors} (never throws) ·
|
|
35
|
+
// serializeEnvelope(item) → text · validateEnvelope(item, payloadSchema).
|
|
36
|
+
// Payload validation rides the shared JSON-Schema-subset checker
|
|
37
|
+
// (actions/schema.mjs — type/required/enum/pattern on flat fields).
|
|
38
|
+
|
|
39
|
+
import { FACULTIES } from './contract.mjs';
|
|
40
|
+
import { validate as validateSchema } from '../actions/schema.mjs';
|
|
41
|
+
|
|
42
|
+
/** Per-faculty item kinds. `null` = open set (knowledge kinds are governed by
|
|
43
|
+
* the knowledge registry's types.json, not pinned here). */
|
|
44
|
+
export const FACULTY_KINDS = {
|
|
45
|
+
knowledge: null, // registry-governed (seed: fact|entity|command|decision)
|
|
46
|
+
memory: ['episode'],
|
|
47
|
+
actions: ['runbook', 'script'],
|
|
48
|
+
instructions: ['steering', 'amendment'],
|
|
49
|
+
guardrails: ['rule'],
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/** Default payload schemas (JSON-Schema subset) — also seeded to
|
|
53
|
+
* .zuzuu/<faculty>/schema.json by `zuzuu init`. We author both ends. */
|
|
54
|
+
export const PAYLOAD_SCHEMAS = {
|
|
55
|
+
knowledge: {
|
|
56
|
+
type: 'object',
|
|
57
|
+
required: ['type'],
|
|
58
|
+
properties: {
|
|
59
|
+
type: { type: 'string', pattern: '^[a-z0-9][a-z0-9-]*$' },
|
|
60
|
+
attributes: { type: 'object' },
|
|
61
|
+
relations: {
|
|
62
|
+
type: 'array',
|
|
63
|
+
items: { type: 'object', required: ['type', 'target'], properties: { type: { type: 'string' }, target: { type: 'string' }, commentary: { type: 'string' } } },
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
memory: {
|
|
68
|
+
type: 'object',
|
|
69
|
+
properties: {
|
|
70
|
+
sessions: { type: 'array', items: { type: 'string' } },
|
|
71
|
+
hosts: { type: 'array', items: { type: 'string' } },
|
|
72
|
+
tags: { type: 'array', items: { type: 'string' } },
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
actions: {
|
|
76
|
+
type: 'object',
|
|
77
|
+
properties: {
|
|
78
|
+
exec: { type: 'string', pattern: '^[A-Za-z0-9][A-Za-z0-9._-]*$' }, // sibling file name, no path escape
|
|
79
|
+
args: { type: 'object' },
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
instructions: {
|
|
83
|
+
type: 'object',
|
|
84
|
+
properties: { scope: { type: 'string' } },
|
|
85
|
+
},
|
|
86
|
+
guardrails: {
|
|
87
|
+
type: 'object',
|
|
88
|
+
required: ['action', 'pattern', 'reason'],
|
|
89
|
+
properties: {
|
|
90
|
+
action: { type: 'string', enum: ['deny', 'ask', 'allow'] },
|
|
91
|
+
tool: { type: 'string' },
|
|
92
|
+
pattern: { type: 'string', minLength: 1 },
|
|
93
|
+
reason: { type: 'string', minLength: 1 },
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const ID_RE = /^[a-z0-9][a-z0-9_-]*$/; // spec is [a-z0-9-]; `_` tolerated for action slugs
|
|
99
|
+
const ISO_RE = /^\d{4}-\d{2}-\d{2}([T ]\d{2}:\d{2}(:\d{2})?(\.\d+)?(Z|[+-]\d{2}:?\d{2})?)?$/;
|
|
100
|
+
const STATUSES = new Set(['active', 'archived']);
|
|
101
|
+
const TOP_KEYS = ['id', 'faculty', 'kind', 'title', 'status', 'created_at', 'updated_at'];
|
|
102
|
+
|
|
103
|
+
// --- scalar quoting (round-trip exact, incl. backslashes) --------------------
|
|
104
|
+
|
|
105
|
+
const unquote = (s) => {
|
|
106
|
+
const t = s.trim();
|
|
107
|
+
if (t.startsWith('"') && t.endsWith('"') && t.length >= 2) {
|
|
108
|
+
try { return JSON.parse(t); } catch { return t.slice(1, -1); }
|
|
109
|
+
}
|
|
110
|
+
if (t.startsWith("'") && t.endsWith("'") && t.length >= 2) return t.slice(1, -1);
|
|
111
|
+
return t;
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const quoteIfNeeded = (s) => {
|
|
115
|
+
const t = String(s);
|
|
116
|
+
if (t.includes('\n')) throw new Error('envelope values must be single-line');
|
|
117
|
+
return /[:#'"\\\[\]{}]|^-\s|^\s|\s$/.test(t) || t === '' ? JSON.stringify(t) : t;
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const KV = /^([A-Za-z_][\w-]*):\s*(.*)$/;
|
|
121
|
+
|
|
122
|
+
// --- parse -------------------------------------------------------------------
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Parse an envelope file's text. Never throws.
|
|
126
|
+
* @returns {{ok: boolean, item: object|null, errors: string[]}}
|
|
127
|
+
*/
|
|
128
|
+
export function parseEnvelope(text) {
|
|
129
|
+
const errors = [];
|
|
130
|
+
const m = String(text).match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
|
|
131
|
+
if (!m) return { ok: false, item: null, errors: ['no frontmatter block'] };
|
|
132
|
+
const [, fm, body] = m;
|
|
133
|
+
const item = { provenance: [], payload: {}, body: body.trim() };
|
|
134
|
+
|
|
135
|
+
let section = null; // 'provenance' | 'payload' | null
|
|
136
|
+
let payloadKey = null; // current sub-block key inside payload
|
|
137
|
+
let current = null; // current list entry (flat map) being filled
|
|
138
|
+
|
|
139
|
+
for (const raw of fm.split('\n')) {
|
|
140
|
+
if (!raw.trim()) continue;
|
|
141
|
+
const indent = raw.match(/^ */)[0].length;
|
|
142
|
+
const line = raw.trim();
|
|
143
|
+
|
|
144
|
+
if (indent === 0) {
|
|
145
|
+
section = null; payloadKey = null; current = null;
|
|
146
|
+
const kv = line.match(KV);
|
|
147
|
+
if (!kv) { errors.push(`bad line: ${line}`); continue; }
|
|
148
|
+
const [, key, val] = kv;
|
|
149
|
+
if (key === 'provenance' || key === 'payload') {
|
|
150
|
+
if (val) { errors.push(`${key} must be a block`); continue; }
|
|
151
|
+
section = key;
|
|
152
|
+
} else {
|
|
153
|
+
item[key] = unquote(val);
|
|
154
|
+
}
|
|
155
|
+
} else if (section === 'provenance') {
|
|
156
|
+
if (line.startsWith('- ')) {
|
|
157
|
+
const kv = line.slice(2).match(KV);
|
|
158
|
+
if (!kv) { errors.push(`bad provenance entry: ${line}`); continue; }
|
|
159
|
+
current = { [kv[1]]: unquote(kv[2]) };
|
|
160
|
+
item.provenance.push(current);
|
|
161
|
+
} else {
|
|
162
|
+
const kv = line.match(KV);
|
|
163
|
+
if (!current || !kv) { errors.push(`bad provenance line: ${line}`); continue; }
|
|
164
|
+
current[kv[1]] = unquote(kv[2]);
|
|
165
|
+
}
|
|
166
|
+
} else if (section === 'payload') {
|
|
167
|
+
if (indent === 2 && !line.startsWith('- ')) {
|
|
168
|
+
current = null;
|
|
169
|
+
const kv = line.match(KV);
|
|
170
|
+
if (!kv) { errors.push(`bad payload line: ${line}`); continue; }
|
|
171
|
+
const [, key, val] = kv;
|
|
172
|
+
if (val === '') { payloadKey = key; /* shape resolved by first child */ }
|
|
173
|
+
else { payloadKey = null; item.payload[key] = unquote(val); }
|
|
174
|
+
} else if (indent >= 4 && payloadKey) {
|
|
175
|
+
const slot = item.payload[payloadKey];
|
|
176
|
+
if (line.startsWith('- ')) {
|
|
177
|
+
const rest = line.slice(2);
|
|
178
|
+
if (!Array.isArray(slot)) {
|
|
179
|
+
if (slot !== undefined && typeof slot === 'object') { errors.push(`payload ${payloadKey}: mixed map/list`); continue; }
|
|
180
|
+
item.payload[payloadKey] = [];
|
|
181
|
+
}
|
|
182
|
+
const kv = rest.match(KV);
|
|
183
|
+
if (kv) {
|
|
184
|
+
current = { [kv[1]]: unquote(kv[2]) };
|
|
185
|
+
item.payload[payloadKey].push(current);
|
|
186
|
+
} else {
|
|
187
|
+
current = null;
|
|
188
|
+
item.payload[payloadKey].push(unquote(rest));
|
|
189
|
+
}
|
|
190
|
+
} else if (current && Array.isArray(slot)) {
|
|
191
|
+
const kv = line.match(KV);
|
|
192
|
+
if (!kv) { errors.push(`bad payload ${payloadKey} entry line: ${line}`); continue; }
|
|
193
|
+
current[kv[1]] = unquote(kv[2]);
|
|
194
|
+
} else {
|
|
195
|
+
// one-level map of scalars
|
|
196
|
+
if (slot === undefined) item.payload[payloadKey] = {};
|
|
197
|
+
else if (Array.isArray(item.payload[payloadKey])) { errors.push(`payload ${payloadKey}: mixed list/map`); continue; }
|
|
198
|
+
const kv = line.match(KV);
|
|
199
|
+
if (!kv) { errors.push(`bad payload ${payloadKey} line: ${line}`); continue; }
|
|
200
|
+
item.payload[payloadKey][kv[1]] = unquote(kv[2]);
|
|
201
|
+
}
|
|
202
|
+
} else {
|
|
203
|
+
errors.push(`unexpected indented line: ${line}`);
|
|
204
|
+
}
|
|
205
|
+
} else {
|
|
206
|
+
errors.push(`unexpected indented line: ${line}`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// empty sub-blocks (key with no children) resolve to {} — already the default
|
|
211
|
+
if (!item.id) errors.push('item missing id');
|
|
212
|
+
if (!item.faculty) errors.push('item missing faculty');
|
|
213
|
+
if (!item.kind) errors.push('item missing kind');
|
|
214
|
+
return { ok: errors.length === 0, item, errors };
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// --- serialize ----------------------------------------------------------------
|
|
218
|
+
|
|
219
|
+
/** Serialize an envelope item → file text (the exact grammar parseEnvelope reads). */
|
|
220
|
+
export function serializeEnvelope(item) {
|
|
221
|
+
const lines = ['---'];
|
|
222
|
+
for (const key of TOP_KEYS) {
|
|
223
|
+
if (item[key] != null && item[key] !== '') lines.push(`${key}: ${quoteIfNeeded(item[key])}`);
|
|
224
|
+
}
|
|
225
|
+
const prov = item.provenance ?? [];
|
|
226
|
+
if (prov.length) {
|
|
227
|
+
lines.push('provenance:');
|
|
228
|
+
for (const entry of prov) {
|
|
229
|
+
Object.keys(entry).forEach((k, i) => lines.push(` ${i === 0 ? '- ' : ' '}${k}: ${quoteIfNeeded(entry[k])}`));
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
const payload = item.payload ?? {};
|
|
233
|
+
const pkeys = Object.keys(payload).filter((k) => payload[k] != null);
|
|
234
|
+
if (pkeys.length) {
|
|
235
|
+
lines.push('payload:');
|
|
236
|
+
for (const k of pkeys) {
|
|
237
|
+
const v = payload[k];
|
|
238
|
+
if (Array.isArray(v)) {
|
|
239
|
+
if (!v.length) continue;
|
|
240
|
+
lines.push(` ${k}:`);
|
|
241
|
+
for (const entry of v) {
|
|
242
|
+
if (entry !== null && typeof entry === 'object') {
|
|
243
|
+
Object.keys(entry).forEach((ek, i) => lines.push(` ${i === 0 ? '- ' : ' '}${ek}: ${quoteIfNeeded(entry[ek])}`));
|
|
244
|
+
} else {
|
|
245
|
+
lines.push(` - ${quoteIfNeeded(entry)}`);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
} else if (v !== null && typeof v === 'object') {
|
|
249
|
+
const entries = Object.entries(v);
|
|
250
|
+
if (!entries.length) continue;
|
|
251
|
+
lines.push(` ${k}:`);
|
|
252
|
+
for (const [mk, mv] of entries) lines.push(` ${mk}: ${quoteIfNeeded(mv)}`);
|
|
253
|
+
} else {
|
|
254
|
+
lines.push(` ${k}: ${quoteIfNeeded(v)}`);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
lines.push('---', '');
|
|
259
|
+
return lines.join('\n') + (item.body ? String(item.body).trim() + '\n' : '');
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// --- validate -----------------------------------------------------------------
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Validate an envelope item: required envelope fields + (optionally) its payload
|
|
266
|
+
* against a JSON-Schema-subset payload schema.
|
|
267
|
+
* @returns {{ok: boolean, errors: string[]}}
|
|
268
|
+
*/
|
|
269
|
+
export function validateEnvelope(item, payloadSchema = null) {
|
|
270
|
+
const errors = [];
|
|
271
|
+
if (!item || typeof item !== 'object') return { ok: false, errors: ['not an item'] };
|
|
272
|
+
if (!item.id || !ID_RE.test(item.id)) errors.push(`id must match ${ID_RE} (got '${item.id}')`);
|
|
273
|
+
if (!FACULTIES.includes(item.faculty)) errors.push(`faculty must be one of ${FACULTIES.join('|')} (got '${item.faculty}')`);
|
|
274
|
+
const kinds = FACULTY_KINDS[item.faculty];
|
|
275
|
+
if (!item.kind || !/^[a-z0-9][a-z0-9-]*$/.test(item.kind)) errors.push(`kind must be a slug (got '${item.kind}')`);
|
|
276
|
+
else if (Array.isArray(kinds) && !kinds.includes(item.kind)) errors.push(`kind must be one of ${kinds.join('|')} for ${item.faculty} (got '${item.kind}')`);
|
|
277
|
+
if (!item.title || typeof item.title !== 'string' || item.title.includes('\n')) errors.push('title is required (single line)');
|
|
278
|
+
if (item.status != null && !STATUSES.has(item.status)) errors.push(`status must be active|archived (got '${item.status}')`);
|
|
279
|
+
if (!item.created_at || !ISO_RE.test(String(item.created_at))) errors.push(`created_at must be ISO (got '${item.created_at}')`);
|
|
280
|
+
if (item.updated_at != null && !ISO_RE.test(String(item.updated_at))) errors.push(`updated_at must be ISO (got '${item.updated_at}')`);
|
|
281
|
+
if (item.provenance != null && !Array.isArray(item.provenance)) errors.push('provenance must be a list');
|
|
282
|
+
if (payloadSchema) errors.push(...validateSchema(payloadSchema, item.payload ?? {}, 'payload'));
|
|
283
|
+
return { ok: errors.length === 0, errors };
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/** Derive a single-line title (first body line, de-markdowned) — fallback id. */
|
|
287
|
+
export function deriveTitle(body, id) {
|
|
288
|
+
const first = String(body ?? '').split('\n').map((l) => l.replace(/^#+\s*/, '').trim()).find(Boolean);
|
|
289
|
+
return (first || String(id || 'item')).slice(0, 80);
|
|
290
|
+
}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
// zuzuu/faculty/generation/read.mjs — the generation READ side (WS3-T1, split
|
|
2
|
+
// per the 2026-06-13 overhaul): paths, the pinned-set enumerators, snapshot
|
|
3
|
+
// hashing, list/show/diff. Minting + rollback live in write.mjs.
|
|
4
|
+
//
|
|
5
|
+
// A *generation* is an immutable, content-addressed snapshot of the agent's
|
|
6
|
+
// pinned faculties (the lockfile). Minting freezes the current faculty state;
|
|
7
|
+
// rollback restores any past generation by *content* (we copy each pinned item's
|
|
8
|
+
// bytes into generations/snapshots/<id>/ at mint time, so a rollback works even
|
|
9
|
+
// for items that were never committed). Identity: Agent → Generation → Run —
|
|
10
|
+
// rollback = flip the active pointer + restore content; never `git revert`.
|
|
11
|
+
//
|
|
12
|
+
// Layout under .zuzuu/:
|
|
13
|
+
// generations/active {active: "gen_NNN"} — the live pointer
|
|
14
|
+
// generations/<id>.json the lockfile (content-addressed manifest)
|
|
15
|
+
// generations/snapshots/<id>/<faculty>/... pinned item bytes (rollback source)
|
|
16
|
+
|
|
17
|
+
import { createHash } from 'node:crypto';
|
|
18
|
+
import { join, dirname } from 'node:path';
|
|
19
|
+
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
20
|
+
|
|
21
|
+
/** Hex sha256 of a string or Buffer. */
|
|
22
|
+
export function sha256(buf) {
|
|
23
|
+
return createHash('sha256').update(buf).digest('hex');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const read = (p) => readFileSync(p, 'utf8');
|
|
27
|
+
export const readJson = (p) => JSON.parse(read(p));
|
|
28
|
+
|
|
29
|
+
// --- paths ------------------------------------------------------------------
|
|
30
|
+
|
|
31
|
+
export const generationsDir = (agentDir) => join(agentDir, 'generations');
|
|
32
|
+
export const snapshotsDir = (agentDir) => join(generationsDir(agentDir), 'snapshots');
|
|
33
|
+
export const activePath = (agentDir) => join(generationsDir(agentDir), 'active');
|
|
34
|
+
export const lockfilePath = (agentDir, id) => join(generationsDir(agentDir), `${id}.json`);
|
|
35
|
+
export const agentJsonPath = (agentDir) => join(agentDir, 'agent.json');
|
|
36
|
+
|
|
37
|
+
// --- faculty file enumeration (the pinned set) ------------------------------
|
|
38
|
+
// Each entry: { id, faculty, src (absolute live path), rel (path under the
|
|
39
|
+
// faculty snapshot dir), hash }. `rel` is what we mirror into snapshots/<id>/.
|
|
40
|
+
|
|
41
|
+
function sortDirents(dir) {
|
|
42
|
+
if (!existsSync(dir)) return [];
|
|
43
|
+
return readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function knowledgeFiles(agentDir) {
|
|
47
|
+
const dir = join(agentDir, 'knowledge', 'items');
|
|
48
|
+
return sortDirents(dir)
|
|
49
|
+
.filter((e) => e.isFile() && e.name.endsWith('.md'))
|
|
50
|
+
.map((e) => {
|
|
51
|
+
const src = join(dir, e.name);
|
|
52
|
+
return { id: e.name.replace(/\.md$/, ''), faculty: 'knowledge', src, rel: e.name, hash: sha256(readFileSync(src)) };
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function actionFiles(agentDir) {
|
|
57
|
+
const dir = join(agentDir, 'actions');
|
|
58
|
+
return sortDirents(dir)
|
|
59
|
+
.filter((e) => e.isDirectory() && e.name !== 'inbox' && e.name !== 'proposals' && e.name !== '_rolledback')
|
|
60
|
+
.map((e) => {
|
|
61
|
+
const adir = join(dir, e.name);
|
|
62
|
+
// Hash the dir's defining files concatenated: the ACTION.md envelope
|
|
63
|
+
// (W24) + sibling scripts (*.mjs — run.mjs and any payload.exec module).
|
|
64
|
+
const parts = sortDirents(adir)
|
|
65
|
+
.filter((f) => f.isFile() && (f.name === 'ACTION.md' || f.name.endsWith('.mjs')))
|
|
66
|
+
.map((f) => join(adir, f.name));
|
|
67
|
+
const concat = Buffer.concat(parts.map((p) => readFileSync(p)));
|
|
68
|
+
return {
|
|
69
|
+
id: e.name, faculty: 'actions', files: parts.map((p) => p.slice(adir.length + 1)),
|
|
70
|
+
adir, hash: parts.length ? sha256(concat) : null,
|
|
71
|
+
};
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Flat envelope-item faculties share one enumerator. */
|
|
76
|
+
function mdItemFiles(agentDir, faculty, ...segments) {
|
|
77
|
+
const dir = join(agentDir, ...segments);
|
|
78
|
+
return sortDirents(dir)
|
|
79
|
+
.filter((e) => e.isFile() && e.name.endsWith('.md'))
|
|
80
|
+
.map((e) => {
|
|
81
|
+
const src = join(dir, e.name);
|
|
82
|
+
return { id: e.name.replace(/\.md$/, ''), faculty, src, rel: e.name, hash: sha256(readFileSync(src)) };
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export const guardrailFiles = (agentDir) => mdItemFiles(agentDir, 'guardrails', 'guardrails', 'items');
|
|
87
|
+
export const instructionFiles = (agentDir) => mdItemFiles(agentDir, 'instructions', 'instructions', 'items');
|
|
88
|
+
|
|
89
|
+
export function memoryFiles(agentDir) {
|
|
90
|
+
const dir = join(agentDir, 'memory', 'entries');
|
|
91
|
+
return sortDirents(dir)
|
|
92
|
+
.filter((e) => e.isFile() && e.name.endsWith('.md'))
|
|
93
|
+
.map((e) => {
|
|
94
|
+
const src = join(dir, e.name);
|
|
95
|
+
return { id: e.name.replace(/\.md$/, ''), faculty: 'memory', src, rel: e.name, hash: sha256(readFileSync(src)) };
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function registryHash(agentDir) {
|
|
100
|
+
const dir = join(agentDir, 'knowledge', 'registry');
|
|
101
|
+
const files = sortDirents(dir).filter((e) => e.isFile() && e.name.endsWith('.json'));
|
|
102
|
+
if (!files.length) return null;
|
|
103
|
+
return sha256(Buffer.concat(files.map((e) => readFileSync(join(dir, e.name)))));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Snapshot the current faculty state → the `faculties` manifest object.
|
|
108
|
+
* Tolerates missing files (empty arrays / null hashes).
|
|
109
|
+
*/
|
|
110
|
+
export function snapshotFaculties(agentDir) {
|
|
111
|
+
return {
|
|
112
|
+
knowledge: {
|
|
113
|
+
items: knowledgeFiles(agentDir).map(({ id, hash }) => ({ id, hash })),
|
|
114
|
+
registryHash: registryHash(agentDir),
|
|
115
|
+
},
|
|
116
|
+
actions: {
|
|
117
|
+
items: actionFiles(agentDir).map(({ id, hash }) => ({ id, hash })),
|
|
118
|
+
},
|
|
119
|
+
guardrails: {
|
|
120
|
+
items: guardrailFiles(agentDir).map(({ id, hash }) => ({ id, hash })),
|
|
121
|
+
},
|
|
122
|
+
instructions: {
|
|
123
|
+
items: instructionFiles(agentDir).map(({ id, hash }) => ({ id, hash })),
|
|
124
|
+
},
|
|
125
|
+
memory: {
|
|
126
|
+
items: memoryFiles(agentDir).map(({ id, hash }) => ({ id, hash })),
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// --- agent identity ---------------------------------------------------------
|
|
132
|
+
|
|
133
|
+
/** Stable agent id derived from the repo root: agt_<first12 of sha256(root)>. */
|
|
134
|
+
export function agentId(agentDir) {
|
|
135
|
+
// agentDir is the .zuzuu/ dir; the repo root is its parent.
|
|
136
|
+
const root = dirname(agentDir);
|
|
137
|
+
return 'agt_' + sha256(root).slice(0, 12);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// --- generation read/list ---------------------------------------------------
|
|
141
|
+
|
|
142
|
+
/** The active generation id, or null. */
|
|
143
|
+
export function activeGeneration(agentDir) {
|
|
144
|
+
const p = activePath(agentDir);
|
|
145
|
+
if (!existsSync(p)) return null;
|
|
146
|
+
try { return readJson(p).active ?? null; } catch { return null; }
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** All generation ids in ascending order. */
|
|
150
|
+
export function listGenerations(agentDir) {
|
|
151
|
+
const dir = generationsDir(agentDir);
|
|
152
|
+
if (!existsSync(dir)) return [];
|
|
153
|
+
return readdirSync(dir)
|
|
154
|
+
.filter((f) => /^gen_\d+\.json$/.test(f))
|
|
155
|
+
.map((f) => f.replace(/\.json$/, ''))
|
|
156
|
+
.sort();
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Read one lockfile, or null. */
|
|
160
|
+
export function readGeneration(agentDir, id) {
|
|
161
|
+
const p = lockfilePath(agentDir, id);
|
|
162
|
+
return existsSync(p) ? readJson(p) : null;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** Diff two item-manifest arrays → {added, changed, removed} (id lists). */
|
|
166
|
+
function diffItems(parentItems = [], childItems = []) {
|
|
167
|
+
const p = new Map(parentItems.map((i) => [i.id, i.hash]));
|
|
168
|
+
const c = new Map(childItems.map((i) => [i.id, i.hash]));
|
|
169
|
+
const added = [], changed = [], removed = [];
|
|
170
|
+
for (const [id, hash] of c) {
|
|
171
|
+
if (!p.has(id)) added.push(id);
|
|
172
|
+
else if (p.get(id) !== hash) changed.push(id);
|
|
173
|
+
}
|
|
174
|
+
for (const id of p.keys()) if (!c.has(id)) removed.push(id);
|
|
175
|
+
return { added: added.sort(), changed: changed.sort(), removed: removed.sort() };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Per-faculty diff of generation `id` against its forkedFrom parent (pure).
|
|
180
|
+
* ALL five faculties are item lists under the Faculty Standard (W24) —
|
|
181
|
+
* added/changed/removed id lists per faculty; knowledge additionally reports
|
|
182
|
+
* registryChanged. When there is no parent (forkedFrom null), everything
|
|
183
|
+
* present counts as added. Returns null for an unknown id.
|
|
184
|
+
*/
|
|
185
|
+
export function diffGenerations(agentDir, id) {
|
|
186
|
+
const child = readGeneration(agentDir, id);
|
|
187
|
+
if (!child) return null;
|
|
188
|
+
const parent = child.forkedFrom ? readGeneration(agentDir, child.forkedFrom) : null;
|
|
189
|
+
const cf = child.faculties || {};
|
|
190
|
+
const pf = parent?.faculties || {};
|
|
191
|
+
const faculties = {};
|
|
192
|
+
for (const f of ['knowledge', 'actions', 'memory', 'guardrails', 'instructions']) {
|
|
193
|
+
faculties[f] = diffItems(pf[f]?.items, cf[f]?.items);
|
|
194
|
+
// knowledge also has a registry hash
|
|
195
|
+
if (f === 'knowledge') {
|
|
196
|
+
faculties[f].registryChanged = (cf.knowledge?.registryHash ?? null) !== (pf.knowledge?.registryHash ?? null);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return {
|
|
200
|
+
id,
|
|
201
|
+
forkedFrom: child.forkedFrom ?? null,
|
|
202
|
+
mintedFrom: Array.isArray(child.mintedFrom) ? child.mintedFrom : [],
|
|
203
|
+
mintedAt: child.mintedAt ?? null,
|
|
204
|
+
faculties,
|
|
205
|
+
};
|
|
206
|
+
}
|