futura-scion 0.2.4 → 0.2.5

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/scion.js CHANGED
@@ -805,10 +805,46 @@ switch (cmd || '') {
805
805
  const positional = restArgs.filter((a) => !a.startsWith('--'));
806
806
  const target = positional[0]; // output directory
807
807
  if (!target) {
808
- console.error('usage: scion scaffold <template|list> <target-dir> [--name x] [--title y] [--port N] [--param k=v] [--no-verify]');
808
+ console.error('usage: scion scaffold <template|fullstack|list> <target-dir> [--name x] [--port N] [--concurrency N] [--no-verify] [--dry-run]');
809
809
  process.exitCode = 2;
810
810
  break;
811
811
  }
812
+ if (arg === 'fullstack') {
813
+ // COMPOSABLE CONSTRUCTION: a workflow DAG that scaffolds backend,
814
+ // frontend, and CLI in parallel, then wires them into one repo.
815
+ const { planFullstack } = await import('../src/mind/plan-fullstack.js');
816
+ const params = {};
817
+ for (const f of flags) {
818
+ const m = f.match(/^--([A-Za-z_][A-Za-z0-9_]*)=(.*)$/);
819
+ if (m) params[m[1]] = m[2];
820
+ }
821
+ const manifest = planFullstack(resolve(target), {
822
+ name: params.name,
823
+ ...(params.port !== undefined ? { port: Number(params.port) } : {}),
824
+ });
825
+ if (flags.includes('--dry-run')) {
826
+ console.log(`fullstack plan for ${target}:`);
827
+ for (const t of manifest.tasks) console.log(` ${t.id.padEnd(20)} deps: ${(t.depends_on ?? []).join(', ') || '(parallel)'}`);
828
+ console.log('\nrun with --approve-as <reviewer> to construct');
829
+ break;
830
+ }
831
+ const approveIdx = restArgs.indexOf('--approve-as');
832
+ const reviewer = approveIdx !== -1 ? restArgs[approveIdx + 1] : null;
833
+ if (!reviewer) {
834
+ console.error('fullstack construction is a mutating workflow — approve it: --approve-as <reviewer> (or inspect first with --dry-run)');
835
+ process.exitCode = 2;
836
+ break;
837
+ }
838
+ const { runWorkflow } = await import('../src/kernel/workflow.js');
839
+ const conc = Number(flags.find((f) => f.startsWith('--concurrency='))?.split('=')[1]) || 3;
840
+ const r = await runWorkflow(manifest, { concurrency: conc, approval: { reviewer } });
841
+ console.log(`fullstack ${r.status}: ${r.completed}/${r.total} steps (concurrency ${conc})`);
842
+ for (const t of r.tasks ?? []) {
843
+ if (t.status !== 'completed') console.log(` ${t.status.toUpperCase()}: ${t.manifestId ?? t.id} ${t.error ?? ''}`);
844
+ }
845
+ if (r.status !== 'completed') process.exitCode = 1;
846
+ break;
847
+ }
812
848
  const tpl = loadTemplate(arg);
813
849
  const params = {};
814
850
  for (const f of flags) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "futura-scion",
3
- "version": "0.2.4",
3
+ "version": "0.2.5",
4
4
  "description": "The fused scion of cortex-os-agent + persona: one zero-LLM-dependent agent stack — Mind proposes, Muscle executes, Gate disposes.",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -86,8 +86,12 @@ export function assertPathAllowed(p) {
86
86
  throw new Error('safety: path must be a non-empty string');
87
87
  }
88
88
  const norm = p.replace(/\\/g, '/');
89
+ const base = norm.split('/').pop(); // final segment decides secrets, not substrings
89
90
  for (const d of denylist()) {
90
- if (norm.includes(d)) {
91
+ // Segment-aware matching: `.env` denies the secrets file itself, not
92
+ // `.env.example` or `backend-service/` (substring matching blocked the
93
+ // scaffolded env CONTRACT — a template file, not a secret).
94
+ if (d === '.env' ? base === '.env' : norm.includes(d)) {
91
95
  throw new Error(`safety: path denied by muscle.path_deny ("${d}"): ${p}`);
92
96
  }
93
97
  }
@@ -231,11 +231,35 @@ async function kernelCallable(fn) {
231
231
  if (fn === 'ocrImage') {
232
232
  return (args) => ocrImage(args?.image, { ...(args?.engine ? { engine: args.engine } : {}) });
233
233
  }
234
+ if (fn === 'scaffold') {
235
+ return async (args) => {
236
+ const { scaffold } = await import('../mind/scaffold.js');
237
+ const r = await scaffold(args?.template, {
238
+ params: args?.params ?? {},
239
+ ...(args?.cwd !== undefined ? { cwd: args.cwd } : {}),
240
+ ...(args?.verify !== undefined ? { verify: args.verify } : {}),
241
+ });
242
+ if (!r.ok) throw new Error(r.error ?? `scaffold ${args?.template} failed (gate: ${r.gate?.verified})`);
243
+ return { template: r.template, files: r.files, gate: r.gate?.verified ?? null };
244
+ };
245
+ }
246
+ if (fn === 'scaffold.file') {
247
+ // Wiring step: write one glue file through the transactional applier
248
+ // (snapshot + rollback) with the same path guardrails as any mutation.
249
+ return async (args) => {
250
+ const { transactionalApplier } = await import('./apply.js');
251
+ const tx = transactionalApplier({
252
+ files: [{ file: args?.file, content: args?.content ?? '', description: args?.description ?? `wire ${args?.file}` }],
253
+ });
254
+ const description = tx.apply();
255
+ return { description, file: args?.file };
256
+ };
257
+ }
234
258
  return null;
235
259
  }
236
260
 
237
261
  function callableSurface() {
238
- return 'boostConsensus, ocrImage';
262
+ return 'boostConsensus, ocrImage, scaffold, scaffold.file';
239
263
  }
240
264
 
241
265
  async function execShellStep(step) {
@@ -0,0 +1,164 @@
1
+ /**
2
+ * mind/plan-fullstack.js — COMPOSABLE CONSTRUCTION: a fullstack scaffold is
3
+ * a WORKFLOW DAG (the plan-fix lineage applied to building).
4
+ *
5
+ * Wave 1 (parallel): scaffold the backend, the frontend, and the CLI into
6
+ * their subdirectories — three independent `scaffold` kernel calls.
7
+ * Wave 2 (wiring, after all of wave 1): generate the cross-component glue
8
+ * that only makes sense once all three exist:
9
+ *
10
+ * - frontend/src/api.js — a typed-ish fetch client for the backend
11
+ * - README.md — the repo-level map of all three components
12
+ * - .env.example — shared env contract (PORT for the backend)
13
+ *
14
+ * The wiring steps are shell-independent: they are emitted as additional
15
+ * `scaffold`-adjacent `call` steps writing files through the same
16
+ * transactional path the Gate judges. Resumability inherits from the
17
+ * workflow engine: a failed wiring step can be re-driven; completed
18
+ * scaffolds never re-run.
19
+ *
20
+ * @module mind/plan-fullstack
21
+ */
22
+
23
+ 'use strict';
24
+
25
+ import * as trail from '../kernel/trail.js';
26
+
27
+ /**
28
+ * Generate the fullstack workflow manifest.
29
+ * @param {string} targetDir — repo root to construct into
30
+ * @param {object} [opts] — { name?, backend?, frontend?, cli?, port? }
31
+ * backend/frontend/cli: { template, params } overrides; defaults build
32
+ * node-api + static-web + node-cli.
33
+ * @returns {object} a workflow manifest ({ name, tasks })
34
+ */
35
+ export function planFullstack(targetDir, opts = {}) {
36
+ if (!targetDir || typeof targetDir !== 'string') {
37
+ throw new Error('planFullstack: targetDir is required');
38
+ }
39
+ const name = opts.name ?? targetDir.split(/[\\/]/).pop() ?? 'fullstack-app';
40
+ const port = String(opts.port ?? 3000);
41
+ const backend = { template: 'node-api', dir: 'backend', params: { name: `${name}-api`, port }, ...(opts.backend ?? {}) };
42
+ const frontend = { template: 'static-web', dir: 'frontend', params: { name: `${name}-web`, title: name }, ...(opts.frontend ?? {}) };
43
+ const cli = { template: 'node-cli', dir: 'cli', params: { name: `${name}-cli` }, ...(opts.cli ?? {}) };
44
+ const base = String(targetDir).replace(/\\/g, '/').replace(/\/$/, '');
45
+
46
+ const tasks = [];
47
+
48
+ // ── Wave 1: the three scaffolds, mutually independent → parallel ──
49
+ tasks.push(
50
+ { id: 'scaffold-backend', type: 'call', fn: 'scaffold', args: { template: backend.template, params: backend.params, cwd: `${base}/${backend.dir}` } },
51
+ { id: 'scaffold-frontend', type: 'call', fn: 'scaffold', args: { template: frontend.template, params: frontend.params, cwd: `${base}/${frontend.dir}` } },
52
+ { id: 'scaffold-cli', type: 'call', fn: 'scaffold', args: { template: cli.template, params: cli.params, cwd: `${base}/${cli.dir}` } },
53
+ );
54
+
55
+ // ── Wave 2: wiring — depends on ALL of wave 1 ──
56
+ tasks.push(
57
+ {
58
+ id: 'wire-frontend-api',
59
+ type: 'call',
60
+ fn: 'scaffold.file',
61
+ args: {
62
+ file: `${base}/${frontend.dir}/js/api.js`,
63
+ content: apiClientModule(port),
64
+ description: 'frontend API client for the backend',
65
+ },
66
+ depends_on: ['scaffold-backend', 'scaffold-frontend'],
67
+ },
68
+ {
69
+ id: 'wire-env-contract',
70
+ type: 'call',
71
+ fn: 'scaffold.file',
72
+ args: {
73
+ file: `${base}/.env.example`,
74
+ content: envContract(port),
75
+ description: 'shared env contract',
76
+ },
77
+ depends_on: ['scaffold-backend'],
78
+ },
79
+ {
80
+ id: 'wire-root-readme',
81
+ type: 'call',
82
+ fn: 'scaffold.file',
83
+ args: {
84
+ file: `${base}/README.md`,
85
+ content: rootReadme(name, { backend, frontend, cli, port }),
86
+ description: 'repo-level map of the three components',
87
+ },
88
+ depends_on: ['scaffold-backend', 'scaffold-frontend', 'scaffold-cli'],
89
+ },
90
+ );
91
+
92
+ const manifest = {
93
+ name: `fullstack-${name}`,
94
+ description: `Fullstack construct: ${backend.template} backend + ${frontend.template} frontend + ${cli.template} CLI, wired into one repo`,
95
+ tasks,
96
+ };
97
+ trail.journal('scaffold.fullstack-plan', { name, target: base, tasks: tasks.length });
98
+ return manifest;
99
+ }
100
+
101
+ /** The frontend's generated API client — fetch wrapper over the backend. */
102
+ function apiClientModule(port) {
103
+ return `/**
104
+ * api.js — generated by FS fullstack scaffold. The ONLY place the frontend
105
+ * knows the backend's address; change API_URL here and nowhere else.
106
+ */
107
+ const API_URL = 'http://localhost:${port}';
108
+
109
+ async function request(path, opts = {}) {
110
+ const res = await fetch(\`\${API_URL}\${path}\`, {
111
+ headers: { 'content-type': 'application/json' },
112
+ ...opts,
113
+ });
114
+ const body = await res.json().catch(() => ({}));
115
+ if (!res.ok) throw new Error(body.error ?? \`HTTP \${res.status}\`);
116
+ return body;
117
+ }
118
+
119
+ export const api = {
120
+ health: () => request('/health'),
121
+ listItems: () => request('/items'),
122
+ addItem: (title) => request('/items', { method: 'POST', body: JSON.stringify({ title }) }),
123
+ };
124
+ `;
125
+ }
126
+
127
+ /** The shared env contract. */
128
+ function envContract(port) {
129
+ return `# Generated by FS fullstack scaffold — the env contract shared by all components.
130
+ PORT=${port}
131
+ NODE_ENV=development
132
+ `;
133
+ }
134
+
135
+ /** The repo-level README. */
136
+ function rootReadme(name, { backend, frontend, cli, port }) {
137
+ return `# ${name}
138
+
139
+ Fullstack repo constructed by FS scaffold — three gate-verified components
140
+ wired into one tree. Every file passed the stack's verifier recipe (real
141
+ exit codes) before this run reported success.
142
+
143
+ ## Layout
144
+
145
+ | Component | Template | Location |
146
+ |---|---|---|
147
+ | Backend API | \`${backend.template}\` | \`${backend.dir}/\` — serves on :${port} |
148
+ | Frontend | \`${frontend.template}\` | \`${frontend.dir}/\` — open \`index.html\` |
149
+ | CLI | \`${cli.template}\` | \`${cli.dir}/\` |
150
+
151
+ ## Run
152
+
153
+ \`\`\`bash
154
+ cd ${backend.dir} && npm install && npm start # API on :${port}
155
+ cd ${frontend.dir} # open index.html (js/api.js talks to :${port})
156
+ cd ${cli.dir} && npm install && npm test
157
+ \`\`\`
158
+
159
+ ## Contract
160
+
161
+ - The frontend's \`${frontend.dir}/js/api.js\` is the single seam to the backend.
162
+ - \`.env.example\` is the shared environment contract.
163
+ `;
164
+ }