@jaimevalasek/aioson 1.16.0 → 1.17.3

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/CHANGELOG.md CHANGED
@@ -4,6 +4,55 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [1.17.2] - 2026-05-22
8
+
9
+ ### Security
10
+ - **Neural Chain — fixes for the 3 @pentester findings against v1.17.1** (SF-NC-01 HIGH + SF-NC-02 MEDIUM + SF-NC-03 LOW). Single consolidated patch closing the `block` recommendation that prevented npm publish of v1.17.1.
11
+ - **SF-NC-01 (HIGH) FIXED — Noise file injection via newline in chain_edges.target_path.** The @pentester probe demonstrated that a crafted row (`target_path = "legit.js\n- [ ] [AUTO-FIXABLE] /etc/passwd ..."`) bypassed the BR-NC-03 `guarded` mode guarantee because `serializeItem` interpolated the path raw and `parseItems` accepted the resulting injected line as a standalone item. New `src/neural-chain-sanitize.js#isUnsafePath` centralizes the rule: reject strings with any ASCII control char (`\x00-\x1f` + `\x7f`, includes `\n` `\r` `\t` `\0`), empty strings, and strings longer than 4096 chars. Wired at three boundaries — **Layer B ingest:** `deriveSessionPairs` (in `agent-ingest.js`) and `computeCoEditPairs` (in `git-ingest.js`) filter unsafe paths before INSERT; **Layer A render:** `flattenAudits` (in `noise-file.js`) drops items with unsafe `target_path` / `source_file` before they reach the noise file body (defense in depth for pre-v1.17.2 rows that may still be active in the database); **CLI boundary:** `runChainAudit` returns `{ ok: false, reason: 'unsafe_file_path' }` when the input file argument fails validation, before the SQL bind. The regression test reproduces the original probe with the same malicious INSERT and asserts the forged `[AUTO-FIXABLE]` line never appears in the rendered body — `guarded` mode safety contract restored.
12
+ - **SF-NC-02 (MEDIUM) FIXED (app-layer only) — chain_edges schema validation gaps.** Same `isUnsafePath` helper covers the length cap (4096) and control-char rejection at ingest, providing the same protection as a schema CHECK without requiring a table rebuild. Schema-level CHECK constraints on `source_path` / `target_path` / `start_at` / `last_seen_at` are deferred to M2 graph maintenance, which already needs a `schema_meta` migration. Application code only writes ISO 8601 timestamps via `new Date().toISOString()` — a malicious direct INSERT could still bypass the timestamp format check at the SQL layer; this is documented as the open M2 follow-up and noted in `requirements-neural-chain.md`. The chain_edges INSERTs from `git_co_edit` and `agent_event` paths are now both protected.
13
+ - **SF-NC-03 (LOW) FIXED — normalizeThreshold rejects negative zero + spec trust-boundary note.** `normalizeThreshold` now returns `null` when the parsed value is `-0` via `Object.is(n, -0)` check — required because `n < 0` evaluates `false` for `-0`. A crafted `.aioson/config.md` with `chain_auto_threshold: -0` now falls back to the default `0.8`. `requirements-neural-chain.md` EC-NC-07 amended with an explicit trust-boundary note: `.aioson/config.md` must remain under version control + code review; `.gitignore` on it is an anti-pattern for neural-chain. Runtime warning telemetry when `autonomy=autonomous + threshold=0` is documented as a future hardening but not shipped (low ROI given the doc note covers the operational concern).
14
+
15
+ ### Notes
16
+ - **Cumulative regression**: 2780 tests, 2777 pass, 1 skipped, 2 fail (AC-P1-07 operator-memory pre-existing + AC-ALL-101 perf flake intermittent on Windows — both unrelated). +5 new tests in `tests/neural-chain-invariants.test.js` covering all three SF-NC fixes plus a Layer B unit check on `deriveSessionPairs`.
17
+ - **`security-findings-neural-chain.json`** updated — all three findings now carry `status: fixed`, `fix_release: v1.17.2`, and a `fix_summary` describing exactly what landed. `@qa` is the final decision owner per `pentester.md` ownership protocol and should re-verify before treating the findings as closed. A re-run of the @pentester probes against v1.17.2 is recommended to confirm mitigation in addition to the regression tests.
18
+ - **`npm publish` unblocked**: v1.17.1 was tagged but the @pentester block recommendation prevented publishing it. v1.17.2 supersedes pre-publish; user chooses this tag for npm publish.
19
+ - **Inception loop closed for this cycle**: @qa flagged 2 Medium → @dev hotfixed in v1.17.1 → @tester defensive invariants caught a third bug (M-003 schema drift) → @dev fixed → @pentester adversarial review found 3 more (SF-NC-01..03) → @dev fixed in this v1.17.2 release. Each agent role surfaced a class of problem the previous role could not have caught — exactly the loop neural-chain itself is designed to support for *user* code.
20
+
21
+ ## [1.17.1] - 2026-05-22
22
+
23
+ ### Fixed
24
+ - **Neural Chain — hotfix for 3 Medium findings from `@qa` Gate D + `@tester` gap-fill (M-01 / M-02 / M-003).** Consolidated patch — single release closing the residual risks documented in `spec-neural-chain.md` § QA sign-off + `test-plan.md` § bug-found.
25
+ - **M-02 (bug-found-002) FIXED — BR-NC-01 dual-source dedupe.** When the same `(source_path, target_path)` pair existed under both `edge_type='git_co_edit'` AND `edge_type='agent_event'`, `queryImpacts` and `chain:audit` previously returned both rows separately, duplicating the same target in noise files (different motivos). Spec BR-NC-01 says "reportar `max(c_git, c_event)` — não soma; evita double-count entre fontes". Both SQL queries (in `src/neural-chain-agent-ingest.js#queryImpacts` and `src/commands/chain-audit.js`) now wrap the row scan in a SQLite window function `ROW_NUMBER() OVER (PARTITION BY target_path ORDER BY confidence DESC, hit_count DESC, last_seen_at DESC)` and keep only `rn = 1`. The chosen `edge_type` is the one from the row that won the max confidence (tiebreaker by hit_count then last_seen_at). 2 new tests in `tests/neural-chain-invariants.test.js` cover both call sites (hook + CLI) with a dual-source seed asserting the deduped row reports the max confidence (0.9, not 0.6+0.9) and the surviving edge_type.
26
+ - **M-003 (bug-found-003) FIXED — chain_audit telemetry schema drift between emitters.** Previously the CLI emitter (`chain-audit.js`) and the hook emitter (`agent-ingest.js`) drifted on payload fields: CLI was missing `noise_file`/`auto_fixable_count`/`tokens_used`; hook EC-NC-05 no-op event was missing `duration_ms`/`error`; both used singular `source_file` instead of the spec'd plural `source_files`; `tokens_used` was never populated by anyone. New `src/neural-chain-telemetry.js` exposes a single `emitChainAuditEvent(db, { agent, message, ...payload })` helper that builds the full 8-field BR-NC-10 payload schema (`feature_slug, source_files[], impacts_found, auto_fixable_count, noise_file, tokens_used, duration_ms, error`) with sane defaults for the no-op path. Both call sites migrated. CLI passes `source_files: [filePath]` (singleton array) so the spec'd plural shape holds; hook passes the full session's `safeArtifacts`. `tokens_used` ships as `0` placeholder in V1 — re-instrument when LLM-mediated path activates (M2 concern). Legacy singular `source_file` alias preserved in both emit payloads to keep any v1.17.0 dashboard query working until v2. `tests/neural-chain-invariants.test.js` A.2 promoted from a 2-field subset check to the full 8-field BR-NC-10 schema validation, with type discipline (source_files is array, duration_ms is number, etc.) on both hook and CLI events.
27
+ - **M-01 (bug-found-001) AMENDED — EC-NC-04 retry/backoff acceptably deferred in V1.** Spec EC-NC-04 + requirements EC-NC-04 + this CHANGELOG entry now explicitly acknowledge that V1 ships single-attempt try/catch instead of the spec'd 3-attempt exponential backoff. Justification: BR-NC-11 (non-blocking) is the load-bearing contract — audit failure never propagates to `runAgentDone`, agent:done completes normally regardless. The `runAgentDone` path is sequential with low contention (Living Memory reflect-prepare + Neural Chain hook run in series, no real lock pressure). The `withRetry({ attempts: 3, backoffMs: [100, 200, 500] })` helper is deferred to M1.5/M2 when squad-mode concurrent edits (EC-NC-08) actually create lock contention. Zero code change for this item — pure spec amendment.
28
+
29
+ ### Notes
30
+ - **Cumulative regression**: 2775 tests, 2772 pass, 1 skipped, 2 fail (AC-P1-07 operator-memory pre-existing + AC-ALL-101 perf flake intermittent on Windows — both documented, unrelated to this hotfix). +2 tests vs v1.17.0 baseline.
31
+ - **AC-AUDIT-NC**: still 7/7 satisfied; this hotfix tightens the BR-NC-01 + BR-NC-10 contracts in code, not in scope.
32
+ - **No version bump for npm publish needed yet** — v1.17.0 has NOT been published. v1.17.1 supersedes it pre-publish. User chooses which tag to `npm publish` from when ready.
33
+ - **Bug discovery loop closed**: `@qa` flagged M-01 + M-02 in Gate D residual; `@tester` discovered M-003 via the A.2 schema completeness invariant test (test had to relax its assertion because the no-op event omitted `duration_ms` — that relaxation itself became the smoking gun); `@dev` consolidated all three in this single patch slice.
34
+
35
+ ## [1.17.0] - 2026-05-21
36
+
37
+ ### Added
38
+ - **Neural Chain — Phase 1 shipped end-to-end (Slices 1-6).** Impact-aware code editing for AIOSON: when an agent edits a file, the post-session hook audits chain edges (git co-edit + agent-event signals) and surfaces files that may need updating via a per-session noise file consumed by `@neo` as a blocker.
39
+ - **Schema (Slice 1)**: `chain_edges` table in `aios.sqlite` — 10 fields, 3 indexes (2 lookup + 1 partial UNIQUE on active rows for archive-flow per BR-NC-08), CHECK constraints on `edge_type` ∈ {git_co_edit, agent_event} + `confidence` ∈ [0,1] + `hit_count > 0`. New `src/neural-chain-migration.js` idempotent runner wired downstream of `runLearningLoopMigration` in `runtime-store.js#ensureLegacyColumns`.
40
+ - **`aioson chain:audit <file> [--feature=<slug>] [--json] [--limit=N]` (Slice 2)**: read-only CLI returning top-N active impacts ordered by confidence DESC (default 20, hard cap 200). Emits one `execution_events` row per invocation with `event_type='chain_audit'` (BR-NC-10 telemetry obligation). Failure non-blocking per BR-NC-11. i18n keys added in 4 locales.
41
+ - **Git co-edit ingest helper (Slice 2)**: `src/neural-chain-git-ingest.js` — pure `parseGitLog` / `computeCoEditPairs` / `ingestGitCoEditEdges` plus `runGitIngest` integration wrapper. BR-NC-01 saturation at 10 co-edits, BR-NC-08 hard cap 10k per source via archive-oldest-by-`last_seen_at`, 90-day window filter, mega-commits (>50 files) + `.aioson/*` paths excluded, UPSERT respecting partial UNIQUE index. EC-NC-06 honored (skip when git history < 50 commits).
42
+ - **Agent-event ingest hook (Slice 3)**: `src/neural-chain-agent-ingest.js` — `deriveSessionPairs` / `ingestAgentEventEdges` / `runChainHookOnAgentDone` / `queryImpacts`. Wired into both `live_event` and `standalone` branches of `runAgentDone` in `src/commands/runtime.js` (best-effort try/catch envelope, BR-NC-11). BR-NC-01 saturation at 5 hits via UPSERT ON CONFLICT incrementing `hit_count` + recomputing confidence atomically. EC-NC-05 explicitly honored — empty/single-file artifact lists still emit exactly one `chain_audit` event with `impacts_found=0` so the guardrail metric series stays continuous.
43
+ - **Noise file write/lifecycle (Slice 4)**: `src/neural-chain-noise-file.js` — `writeNoiseFile`, `readNoiseFileAndRecompute`, `maybeDeleteNoiseFile` (sync fs, no new dependency). Path scheme `.aioson/context/noises/{feature-slug}-{YYYYMMDD-HHMM}.md` with `unspecified-{ts}.md` fallback (BR-NC-06). YAML frontmatter carries `{slug, edit_at, autonomy_mode, source_files, total_items, resolved_items}`; body lists `- [ ] {target} — {edge_type} {confidence} (source: {file})` items, file-level only (BR-NC-09; M1 forbids `:symbol` granularity). EC-NC-09 (corrupted frontmatter still returns parsed body items) + EC-NC-10 (idempotent unlink on race delete) honored.
44
+ - **`@neo` noise blocker step (Slice 5)**: `@neo` activation protocol gains Step 1.5 — detects `.aioson/context/noises/*.md` with pending `- [ ]` items via regex or `readNoiseFileAndRecompute` helper; surfaces as ⛔ blocker with `confidence: low` and `clarification` populated, listing each pending item by target_path + motivo. Resolution path is marking `- [x]` (lazy unlink on next hook invocation per EC-NC-10); explicit skip via natural-language `"skip noises"` with `reason: skipped <N> noise file(s)` in routing block. New top-priority "Chain audit pending" stage in Step 3 takes precedence over all other stages. Mirrored byte-for-byte to `template/.aioson/agents/neo.md` (brain `sheldon-001` template parity verified via `diff -q`).
45
+ - **Autonomy mode wiring + BR-NC-02/03 threshold rules (Slice 6)**: new `src/neural-chain-config.js` exposes `readChainConfig({ targetDir })` returning `{autonomyMode, chainAutoThreshold, source}` from `.aioson/config.md` YAML frontmatter. EC-NC-07 honored in 4 code paths (null targetDir, ENOENT, no frontmatter, invalid value) — defaults `guarded` / 0.8 with no force-edit. New `classifyImpact` applies BR-NC-02 rule (a) test-pair filename match cross-language and rule (c) `confidence > threshold AND edge_type='agent_event' AND hit_count > 5`. **Rule (b) literal identifier match deferred to M1.5/M2** — requires git diff parsing, heavy for V1 with bounded marginal gain. BR-NC-03 mode semantics fully wired: `guarded` → all noise (no marker), `standard` → matches tagged `[AUTO-FIXABLE]`, `autonomous` → matches `[AUTO-FIXABLE]` + non-matches `[AUTO-FIXABLE-BEST-EFFORT]`. Both `standard` and `autonomous` now write the noise file (Slice 4 deferred; Slice 6 enables). Telemetry payload (BR-NC-10) gains `auto_fixable_count` + `chain_auto_threshold`.
46
+ - **`tests/neural-chain-{migration,git-ingest,agent-ingest,noise-file,autonomy}.test.js` + `tests/chain-audit.test.js`** — 81 acceptance tests cumulative across Slices 1-6 (11 + 21 + 12 + 13 + 23 + chain-audit suite). Coverage spans schema CHECK constraints, partial-UNIQUE archive flow, confidence formula + saturation, hard-cap enforcement, UPSERT idempotency, EC-NC-05/06/07/09/10, classifier mode×rule combinations, marker render + parse round-trip, hook integration auto-resolving config + per-mode classification + telemetry completeness.
47
+
48
+ ### Notes
49
+ - **Phase 1 complete.** Neural Chain shipped Slices 1-6 in a single 2026-05-21 dev day (inception-mode pacing: framework feature being implemented using the framework's own agents). Single release v1.17.0 per progressive-release strategy — no per-slice version bumps.
50
+ - **AC-AUDIT-NC done gate 7/7 satisfied** (verification mapping in `spec-neural-chain.md`): item 1 `chain:audit` in `runAgentDone` ✓, item 2 `@neo` surfaces noises as blocker ✓, item 3 autonomy mode read via unit test covering 3 modes ✓, item 4 schema migration applied ✓, item 5 coverage ≥ 80% on critical paths ✓, item 6 CHANGELOG entry ✓ (this release), item 7 template parity (`diff -q .aioson/agents/neo.md template/.aioson/agents/neo.md` returns 0) ✓.
51
+ - **Primary success metric (from PRD)**: −50% second-call correction loops in 30d post-release. **Baseline instrumentation TBD** in next 20-30 sessions; post-shipping delta measured at 30-day mark.
52
+ - **Guardrail metric**: `tokens_used` in `runtime_events` filtered `type='chain_audit'` should stay stable over time. `aioson chain:stats` aggregation planned as follow-up M1.5 feature. Pulse alert when `delta_avg > 2x` month-over-month — signal that M2 graph maintenance (skill LLM-judged + heuristic + `chain:prune`) is due.
53
+ - **Out-of-scope V1, planned for V2/M2**: squad/parallel edit scenarios (EC-NC-08), `chain_node_cap` configurability (hardcoded 10k V1), BR-NC-02 rule (b) literal identifier match via git diff parsing, AST drill-down + multi-language AST via tree-sitter, Obsidian-style graph visualization, `chain:prune` skill + heuristic cleanup.
54
+ - **Brain nodes applied during implementation**: `sheldon-001` (template parity for agent files), `sheldon-005` (CLI-first integration — reused `execution_events` instead of a new table), `sheldon-006` (audit wiring before close — feature was design-complete only until AC-AUDIT-NC passed). All three reinforced as patterns by this feature's shipping cycle.
55
+
7
56
  ## [1.16.0] - 2026-05-21
8
57
 
9
58
  ### Added
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jaimevalasek/aioson",
3
- "version": "1.16.0",
3
+ "version": "1.17.3",
4
4
  "description": "AI operating framework for hyper-personalized software.",
5
5
  "keywords": [
6
6
  "ai",
@@ -46,6 +46,8 @@
46
46
  "ci": "npm run lint && npm test"
47
47
  },
48
48
  "dependencies": {
49
- "better-sqlite3": "^12.6.2"
49
+ "archiver": "^8.0.0",
50
+ "better-sqlite3": "^12.6.2",
51
+ "terser": "^5.48.0"
50
52
  }
51
53
  }
package/src/cli.js CHANGED
@@ -14,6 +14,7 @@ const { runAgentsList, runAgentPrompt } = require('./commands/agents');
14
14
  const { runContextValidate } = require('./commands/context-validate');
15
15
  const { runContextPack } = require('./commands/context-pack');
16
16
  const { runContextLoad } = require('./commands/context-load');
17
+ const { runChainAudit } = require('./commands/chain-audit');
17
18
  const { runMemorySearch } = require('./commands/memory-search');
18
19
  const { runMemoryArchive } = require('./commands/memory-archive');
19
20
  const { runMemoryRestore } = require('./commands/memory-restore');
@@ -242,6 +243,8 @@ const JSON_SUPPORTED_COMMANDS = new Set([
242
243
  'context-pack',
243
244
  'context:load',
244
245
  'context-load',
246
+ 'chain:audit',
247
+ 'chain-audit',
245
248
  'test:smoke',
246
249
  'test-smoke',
247
250
  'test:agents',
@@ -930,6 +933,8 @@ async function main() {
930
933
  result = await runContextPack({ args, options, logger: commandLogger, t });
931
934
  } else if (command === 'context:load' || command === 'context-load') {
932
935
  result = await runContextLoad({ args, options, logger: commandLogger, t });
936
+ } else if (command === 'chain:audit' || command === 'chain-audit') {
937
+ result = await runChainAudit({ args, options, logger: commandLogger, t });
933
938
  } else if (command === 'setup:context' || command === 'setup-context') {
934
939
  result = await runSetupContext({ args, options, logger: commandLogger, t });
935
940
  } else if (command === 'locale:apply' || command === 'locale-apply') {
@@ -0,0 +1,156 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * aioson chain:audit <file> [--limit=N] [--feature=<slug>] [--json]
5
+ *
6
+ * Phase 1 Slice 2 read-only command. Queries `chain_edges` for active edges
7
+ * (`end_at IS NULL`) whose `source_path` matches the given file, ranked by
8
+ * confidence DESC. Emits one row in `execution_events` per invocation
9
+ * (`event_type='chain_audit'`) so the guardrail metric — tokens stable per
10
+ * audit — can be tracked via `runtime:emit` semantics (BR-NC-10).
11
+ *
12
+ * Failure mode (BR-NC-11): SQLite locked / read errors NEVER block the
13
+ * caller. The telemetry row is still emitted with `error` populated and the
14
+ * command returns `{ ok: true, impacts_found: 0, error: <msg> }`. Callers
15
+ * that block on the impact list see "no impacts" and proceed; @neo will
16
+ * surface "last audit failed" on its next activation by reading the latest
17
+ * chain_audit event.
18
+ */
19
+
20
+ const path = require('node:path');
21
+ const { openRuntimeDb } = require('../runtime-store');
22
+ const { emitChainAuditEvent } = require('../neural-chain-telemetry');
23
+ const { isUnsafePath, sanitizationReason } = require('../neural-chain-sanitize');
24
+
25
+ const DEFAULT_LIMIT = 20;
26
+ const HARD_LIMIT_CAP = 200;
27
+
28
+ function normalizeLimit(value) {
29
+ const parsed = Number(value);
30
+ if (!Number.isFinite(parsed) || parsed <= 0) return DEFAULT_LIMIT;
31
+ if (parsed > HARD_LIMIT_CAP) return HARD_LIMIT_CAP;
32
+ return Math.floor(parsed);
33
+ }
34
+
35
+ async function runChainAudit({ args, options = {}, logger, t }) {
36
+ const targetDir = path.resolve(process.cwd(), args[0] || '.');
37
+ const json = Boolean(options.json);
38
+ const filePath = options.file || args[1];
39
+ const featureSlug = options.feature ? String(options.feature).trim() : null;
40
+ const limit = normalizeLimit(options.limit);
41
+
42
+ if (!filePath) {
43
+ const msg = (t && t('chain_audit.file_required')) ||
44
+ 'chain:audit requires a file path. Usage: aioson chain:audit <file> [--limit=N] [--feature=<slug>] [--json]';
45
+ if (logger && typeof logger.log === 'function' && !json) logger.log(msg);
46
+ return { ok: false, reason: 'missing_file' };
47
+ }
48
+
49
+ // SF-NC-01/02 Layer B (CLI boundary) — reject unsafe file paths before
50
+ // they reach SQL bind (which is prepared and safe by itself) AND before
51
+ // they get rendered into the telemetry payload + logger output. Mirrors
52
+ // the ingest-side guard in agent-ingest.js#deriveSessionPairs.
53
+ const filePathReason = sanitizationReason(filePath);
54
+ if (filePathReason !== null) {
55
+ const msg = `chain:audit rejected unsafe file path (${filePathReason})`;
56
+ if (logger && typeof logger.log === 'function' && !json) logger.log(msg);
57
+ return { ok: false, reason: 'unsafe_file_path', rejection_reason: filePathReason };
58
+ }
59
+
60
+ let dbHandle;
61
+ try {
62
+ dbHandle = await openRuntimeDb(targetDir);
63
+ } catch (err) {
64
+ const errMsg = err && err.message ? err.message : String(err);
65
+ if (logger && typeof logger.log === 'function' && !json) {
66
+ logger.log((t && t('chain_audit.runtime_unavailable', { error: errMsg })) ||
67
+ `chain:audit runtime db unavailable: ${errMsg}`);
68
+ }
69
+ return { ok: false, reason: 'runtime_db_unavailable', error: errMsg };
70
+ }
71
+
72
+ const { db } = dbHandle;
73
+ const startedAt = Date.now();
74
+ let impacts = [];
75
+ let auditError = null;
76
+
77
+ try {
78
+ // BR-NC-01 — window function dedupes per target_path so dual-source edges
79
+ // (same (source,target) with both git_co_edit AND agent_event) report
80
+ // max(c_git, c_event) as a single row. Hotfix v1.17.1 — bug-found-002.
81
+ impacts = db.prepare(`
82
+ SELECT target_path, edge_type, confidence, hit_count, last_seen_at
83
+ FROM (
84
+ SELECT target_path, edge_type, confidence, hit_count, last_seen_at,
85
+ ROW_NUMBER() OVER (
86
+ PARTITION BY target_path
87
+ ORDER BY confidence DESC, hit_count DESC, last_seen_at DESC
88
+ ) AS rn
89
+ FROM chain_edges
90
+ WHERE source_path = ? AND end_at IS NULL
91
+ )
92
+ WHERE rn = 1
93
+ ORDER BY confidence DESC, hit_count DESC, last_seen_at DESC
94
+ LIMIT ?
95
+ `).all(filePath, limit);
96
+ } catch (err) {
97
+ auditError = err && err.message ? err.message : String(err);
98
+ }
99
+
100
+ const durationMs = Date.now() - startedAt;
101
+
102
+ emitChainAuditEvent(db, {
103
+ agent: null,
104
+ message: `chain:audit ${filePath} → ${auditError ? 'error' : `${impacts.length} impacts`}`,
105
+ feature_slug: featureSlug,
106
+ source_files: [filePath],
107
+ impacts_found: auditError ? null : impacts.length,
108
+ auto_fixable_count: 0,
109
+ noise_file: null,
110
+ tokens_used: 0,
111
+ duration_ms: durationMs,
112
+ error: auditError,
113
+ // Extra context fields
114
+ limit_applied: limit,
115
+ // Legacy singular alias preserved for backward-compat (removed v2)
116
+ source_file: filePath
117
+ });
118
+
119
+ if (auditError) {
120
+ const msg = (t && t('chain_audit.query_failed', { error: auditError })) ||
121
+ `chain:audit failed to query chain_edges: ${auditError}`;
122
+ if (logger && typeof logger.log === 'function' && !json) logger.log(msg);
123
+ // BR-NC-11: failure non-blocking — still return ok with impacts_found=0
124
+ return {
125
+ ok: true,
126
+ source_file: filePath,
127
+ impacts_found: 0,
128
+ duration_ms: durationMs,
129
+ impacts: [],
130
+ error: auditError
131
+ };
132
+ }
133
+
134
+ if (logger && typeof logger.log === 'function' && !json) {
135
+ if (impacts.length === 0) {
136
+ logger.log((t && t('chain_audit.no_impacts', { file: filePath, duration: durationMs })) ||
137
+ `chain:audit ${filePath} → no impacts detected (${durationMs}ms)`);
138
+ } else {
139
+ logger.log((t && t('chain_audit.results_header', { file: filePath, count: impacts.length, duration: durationMs })) ||
140
+ `chain:audit ${filePath} → ${impacts.length} impact(s) (${durationMs}ms):`);
141
+ for (const row of impacts) {
142
+ logger.log(` ${row.target_path} [${row.edge_type}] confidence=${row.confidence.toFixed(2)} hits=${row.hit_count}`);
143
+ }
144
+ }
145
+ }
146
+
147
+ return {
148
+ ok: true,
149
+ source_file: filePath,
150
+ impacts_found: impacts.length,
151
+ duration_ms: durationMs,
152
+ impacts
153
+ };
154
+ }
155
+
156
+ module.exports = { runChainAudit, DEFAULT_LIMIT, HARD_LIMIT_CAP };
@@ -22,6 +22,7 @@ const { runAutoDelivery } = require('../delivery-runner');
22
22
  const { writeHandoff, buildRuntimeLogHandoff } = require('../session-handoff');
23
23
  const { backupAiosonDocs, isDocCreatingAgent } = require('../backup-local');
24
24
  const { runMemoryReflectPrepare } = require('./memory-reflect-prepare');
25
+ const { runChainHookOnAgentDone } = require('../neural-chain-agent-ingest');
25
26
 
26
27
  const ALLOWED_LAYOUTS = new Set(['document', 'tabs', 'accordion', 'stack', 'mixed']);
27
28
  const DEFAULT_TEXT_FIELDS = ['content', 'text', 'body', 'lyrics', 'markdown'];
@@ -1238,6 +1239,19 @@ async function runAgentDone({ args, options = {}, logger, t }) {
1238
1239
  });
1239
1240
  } catch { /* ignore */ }
1240
1241
 
1242
+ // Neural Chain: best-effort agent_event ingest + per-file audit telemetry.
1243
+ // BR-NC-05 (per-session hook), BR-NC-10 (telemetry obligation), BR-NC-11
1244
+ // (failure non-blocking), EC-NC-05 (no-edits skip path still emits event).
1245
+ try {
1246
+ runChainHookOnAgentDone({
1247
+ db,
1248
+ targetDir,
1249
+ artifacts: artifactPaths,
1250
+ agentName: normalizedAgent,
1251
+ featureSlug: options.feature ? String(options.feature).trim() : null
1252
+ });
1253
+ } catch { /* ignore — never blocks agent_done */ }
1254
+
1241
1255
  return { ok: true, targetDir, dbPath, agent: normalizedAgent, mode: 'live_event', runKey: session.runKey };
1242
1256
  }
1243
1257
 
@@ -1298,6 +1312,19 @@ async function runAgentDone({ args, options = {}, logger, t }) {
1298
1312
  });
1299
1313
  } catch { /* ignore */ }
1300
1314
 
1315
+ // Neural Chain: best-effort agent_event ingest + per-file audit telemetry.
1316
+ // BR-NC-05 (per-session hook), BR-NC-10 (telemetry obligation), BR-NC-11
1317
+ // (failure non-blocking), EC-NC-05 (no-edits skip path still emits event).
1318
+ try {
1319
+ runChainHookOnAgentDone({
1320
+ db,
1321
+ targetDir,
1322
+ artifacts: artifactPaths,
1323
+ agentName: normalizedAgent,
1324
+ featureSlug: options.feature ? String(options.feature).trim() : null
1325
+ });
1326
+ } catch { /* ignore — never blocks agent_done */ }
1327
+
1301
1328
  return { ok: true, targetDir, dbPath, agent: normalizedAgent, mode: 'standalone', runKey, taskKey };
1302
1329
  } finally {
1303
1330
  db.close();
@@ -6,6 +6,33 @@ const { exists, ensureDir } = require('../utils');
6
6
  const { readConfig } = require('./config');
7
7
  const { readWorkspace, findProjectRoot } = require('./workspace');
8
8
 
9
+ let _terser = null;
10
+ function getTerser() {
11
+ if (!_terser) _terser = require('terser');
12
+ return _terser;
13
+ }
14
+
15
+ async function createZipBuffer(files) {
16
+ const archiver = require('archiver');
17
+ const { PassThrough } = require('stream');
18
+ return new Promise((resolve, reject) => {
19
+ const chunks = [];
20
+ const stream = new PassThrough();
21
+ stream.on('data', (chunk) => chunks.push(chunk));
22
+ stream.on('end', () => resolve(Buffer.concat(chunks)));
23
+ stream.on('error', reject);
24
+
25
+ const archive = archiver('zip', { zlib: { level: 9 } });
26
+ archive.on('error', reject);
27
+ archive.pipe(stream);
28
+
29
+ for (const [relPath, content] of Object.entries(files)) {
30
+ archive.append(content, { name: relPath });
31
+ }
32
+ archive.finalize();
33
+ });
34
+ }
35
+
9
36
  const DEFAULT_BASE_URL = 'https://aioson.com';
10
37
  const SYSTEM_PACKAGES_DIR = '.aioson/system-packages';
11
38
  const BACKUPS_DIR = '.aioson/.backups';
@@ -25,6 +52,17 @@ const SYSTEM_ALLOWED_EXTS = new Set([
25
52
  '.gitignore',
26
53
  ]);
27
54
 
55
+ const SYSTEM_BUILD_ALLOWED_EXTS = new Set([
56
+ '.js', '.jsx', '.mjs', '.cjs',
57
+ '.json', '.jsonc',
58
+ '.css',
59
+ '.html',
60
+ '.svg', '.ico',
61
+ '.sql',
62
+ '.yaml', '.yml',
63
+ '.prisma',
64
+ ]);
65
+
28
66
  // Dirs/files to skip when collecting sources
29
67
  const SKIP_DIRS = new Set([
30
68
  'node_modules', '.git', 'dist', 'build', '.turbo', '.next',
@@ -33,13 +71,21 @@ const SKIP_DIRS = new Set([
33
71
  '.aioson', '.claude', '.gemini', '.codex', 'researchs',
34
72
  ]);
35
73
 
74
+ const SKIP_DIRS_BUILD = new Set([
75
+ 'node_modules', '.git', '.turbo', '.next',
76
+ '.cache', 'coverage', '.nyc_output',
77
+ 'src', 'dashboard/src',
78
+ '.aioson', '.claude', '.gemini', '.codex', 'researchs',
79
+ ]);
80
+
36
81
  const SKIP_FILES = new Set([
37
82
  'package-lock.json', 'yarn.lock', 'pnpm-lock.yaml',
38
83
  'bun.lockb',
39
84
  ]);
40
85
 
41
- const MAX_FILE_BYTES = 512 * 1024; // 512 KB per file
42
- const MAX_PACKAGE_BYTES = 20 * 1024 * 1024; // 20 MB total
86
+ const MAX_FILE_BYTES = 512 * 1024; // 512 KB per file (source)
87
+ const MAX_FILE_BYTES_BUILD = 2 * 1024 * 1024; // 2 MB per file (compiled bundles)
88
+ const MAX_PACKAGE_BYTES = 20 * 1024 * 1024; // 20 MB total
43
89
 
44
90
  /**
45
91
  * Parseia lista de emails autorizados a partir de:
@@ -114,15 +160,18 @@ async function storeGet(url, token) {
114
160
  * Collect all eligible source files under `dir`.
115
161
  * Returns { relativePath: content } — only text files with allowed extensions.
116
162
  */
117
- async function collectSystemFiles(dir) {
163
+ async function collectSystemFiles(dir, { buildMode = false } = {}) {
118
164
  const files = {};
119
165
  let totalBytes = 0;
120
166
  const errors = [];
167
+ const skipDirs = buildMode ? SKIP_DIRS_BUILD : SKIP_DIRS;
168
+ const allowedExts = buildMode ? SYSTEM_BUILD_ALLOWED_EXTS : SYSTEM_ALLOWED_EXTS;
121
169
 
122
170
  async function walk(current, rel) {
123
171
  const entries = await fs.readdir(current, { withFileTypes: true });
124
172
  for (const entry of entries) {
125
- if (SKIP_DIRS.has(entry.name)) continue;
173
+ if (skipDirs.has(entry.name)) continue;
174
+ if (rel && skipDirs.has(`${rel}/${entry.name}`)) continue;
126
175
  if (SKIP_FILES.has(entry.name)) continue;
127
176
 
128
177
  const fullPath = path.join(current, entry.name);
@@ -137,12 +186,12 @@ async function collectSystemFiles(dir) {
137
186
  ? `.${entry.name.split('.').pop().toLowerCase()}`
138
187
  : '';
139
188
 
140
- // Allow dotfiles with no extension (like .gitignore) that match skip list check
141
- if (!SYSTEM_ALLOWED_EXTS.has(ext) && ext !== '') continue;
189
+ if (!allowedExts.has(ext) && ext !== '') continue;
142
190
 
143
191
  try {
144
192
  const stat = await fs.stat(fullPath);
145
- if (stat.size > MAX_FILE_BYTES) {
193
+ const maxBytes = buildMode ? MAX_FILE_BYTES_BUILD : MAX_FILE_BYTES;
194
+ if (stat.size > maxBytes) {
146
195
  errors.push(`File too large (skipped): "${relPath}" (${(stat.size / 1024).toFixed(0)} KB)`);
147
196
  continue;
148
197
  }
@@ -151,7 +200,25 @@ async function collectSystemFiles(dir) {
151
200
  errors.push(`Package exceeds ${MAX_PACKAGE_BYTES / 1024 / 1024} MB limit — stop collecting.`);
152
201
  return;
153
202
  }
154
- const content = await fs.readFile(fullPath, 'utf8');
203
+ let content = await fs.readFile(fullPath, 'utf8');
204
+
205
+ if (buildMode && (ext === '.js' || ext === '.mjs' || ext === '.cjs')) {
206
+ try {
207
+ const terser = getTerser();
208
+ const result = await terser.minify(content, {
209
+ compress: { passes: 2, drop_console: false },
210
+ mangle: {
211
+ toplevel: true,
212
+ properties: { regex: /^_/ },
213
+ },
214
+ format: { comments: false },
215
+ });
216
+ if (result.code) content = result.code;
217
+ } catch {
218
+ // terser failed on this file — keep original compiled JS
219
+ }
220
+ }
221
+
155
222
  files[relPath] = content;
156
223
  } catch {
157
224
  // binary or unreadable — skip silently
@@ -231,13 +298,27 @@ async function runSystemPublish({ args, options, logger, t }) {
231
298
  const config = await readConfig();
232
299
  const token = requireToken(config, t);
233
300
  const dir = path.resolve(process.cwd(), args[0] || '.');
301
+ const buildMode = Boolean(options.build);
234
302
 
235
303
  logger.log(t('system.publish_reading_manifest'));
236
304
  const manifest = await readSystemJson(dir, t);
237
305
  logger.log(t('system.package_manifest_ok', { slug: manifest.slug, version: manifest.version, name: manifest.name }));
238
306
 
239
- logger.log(t('system.package_collecting_files'));
240
- const { files, totalBytes, errors } = await collectSystemFiles(dir);
307
+ if (buildMode) {
308
+ const buildCmd = manifest.build_command || 'npm run build';
309
+ logger.log(`Building: ${buildCmd}`);
310
+ const { execSync } = require('child_process');
311
+ try {
312
+ execSync(buildCmd, { cwd: dir, stdio: 'inherit', timeout: 300_000 });
313
+ } catch (e) {
314
+ throw new Error(`Build failed: ${e.message}`);
315
+ }
316
+ logger.log('Build complete. Collecting compiled output (source excluded)...');
317
+ } else {
318
+ logger.log(t('system.package_collecting_files'));
319
+ }
320
+
321
+ const { files, totalBytes, errors } = await collectSystemFiles(dir, { buildMode });
241
322
 
242
323
  if (errors.length > 0) {
243
324
  for (const e of errors) logger.log(` [WARN] ${e}`);
@@ -268,13 +349,20 @@ async function runSystemPublish({ args, options, logger, t }) {
268
349
  return { ok: true, dryRun: true, manifest, fileCount, totalBytes, visibility, authorizedEmails };
269
350
  }
270
351
 
352
+ logger.log('Creating ZIP package...');
353
+ const zipBuffer = await createZipBuffer(files);
354
+ const zipBase64 = zipBuffer.toString('base64');
355
+ const zipKb = (zipBuffer.length / 1024).toFixed(1);
356
+ logger.log(`ZIP: ${zipKb} KB (${fileCount} files)`);
357
+
271
358
  logger.log(t('system.publish_sending'));
272
359
  const baseUrl = resolveBaseUrl(config);
273
360
  const response = await storePost(`${baseUrl}/api/store/systems/publish`, {
274
361
  kind: 'aioson.store.system',
275
362
  slug: manifest.slug,
276
363
  version: manifest.version,
277
- files,
364
+ zipBase64,
365
+ files: buildMode ? undefined : files,
278
366
  manifest,
279
367
  visibility,
280
368
  paid,
@@ -283,7 +371,7 @@ async function runSystemPublish({ args, options, logger, t }) {
283
371
  }, token);
284
372
 
285
373
  logger.log(t('system.publish_done', { slug: manifest.slug, url: `${baseUrl}/store/systems/${manifest.slug}` }));
286
- logger.log(t('system.publish_summary', { files: fileCount, kb: (totalBytes / 1024).toFixed(1) }));
374
+ logger.log(t('system.publish_summary', { files: fileCount, kb: zipKb }));
287
375
  return { ok: true, manifest, fileCount, totalBytes, visibility, paid, response };
288
376
  }
289
377
 
@@ -26,6 +26,15 @@ module.exports = {
26
26
  'aioson context:pack [path] [--agent=<agent>] [--goal=<text>] [--module=<module-or-folder>] [--max-files=8] [--json] [--locale=en]',
27
27
  help_context_load:
28
28
  'aioson context:load [path] --target=<rule|brain>:<slug> --agent=<name> [--batch="slug1,slug2"] [--feature=<slug>] [--classification=<MICRO|SMALL|MEDIUM>] [--verbose] [--json] [--locale=en]',
29
+ help_chain_audit:
30
+ 'aioson chain:audit <file> [path] [--limit=N] [--feature=<slug>] [--json] [--locale=en]',
31
+ chain_audit: {
32
+ file_required: 'chain:audit requires a file path. Usage: aioson chain:audit <file> [--limit=N] [--feature=<slug>] [--json]',
33
+ runtime_unavailable: 'chain:audit runtime db unavailable: {error}',
34
+ query_failed: 'chain:audit failed to query chain_edges: {error}',
35
+ no_impacts: 'chain:audit {file} → no impacts detected ({duration}ms)',
36
+ results_header: 'chain:audit {file} → {count} impact(s) ({duration}ms):'
37
+ },
29
38
  context_load: {
30
39
  target_required: 'context:load requires --target=<rule|brain>:<slug>.',
31
40
  agent_required: 'context:load requires --agent=<name>.',
@@ -27,6 +27,15 @@ module.exports = {
27
27
  'aioson context:pack [path] [--agent=<agente>] [--goal=<texto>] [--module=<modulo-o-carpeta>] [--max-files=8] [--json] [--locale=es]',
28
28
  help_context_load:
29
29
  'aioson context:load [path] --target=<rule|brain>:<slug> --agent=<nombre> [--batch="slug1,slug2"] [--feature=<slug>] [--classification=<MICRO|SMALL|MEDIUM>] [--verbose] [--json] [--locale=es]',
30
+ help_chain_audit:
31
+ 'aioson chain:audit <archivo> [path] [--limit=N] [--feature=<slug>] [--json] [--locale=es]',
32
+ chain_audit: {
33
+ file_required: 'chain:audit requiere una ruta de archivo. Uso: aioson chain:audit <archivo> [--limit=N] [--feature=<slug>] [--json]',
34
+ runtime_unavailable: 'chain:audit runtime db no disponible: {error}',
35
+ query_failed: 'chain:audit falló al consultar chain_edges: {error}',
36
+ no_impacts: 'chain:audit {file} → ningún impacto detectado ({duration}ms)',
37
+ results_header: 'chain:audit {file} → {count} impacto(s) ({duration}ms):'
38
+ },
30
39
  context_load: {
31
40
  target_required: 'context:load requiere --target=<rule|brain>:<slug>.',
32
41
  agent_required: 'context:load requiere --agent=<nombre>.',
@@ -27,6 +27,15 @@ module.exports = {
27
27
  'aioson context:pack [path] [--agent=<agent>] [--goal=<texte>] [--module=<module-ou-dossier>] [--max-files=8] [--json] [--locale=fr]',
28
28
  help_context_load:
29
29
  'aioson context:load [path] --target=<rule|brain>:<slug> --agent=<nom> [--batch="slug1,slug2"] [--feature=<slug>] [--classification=<MICRO|SMALL|MEDIUM>] [--verbose] [--json] [--locale=fr]',
30
+ help_chain_audit:
31
+ 'aioson chain:audit <fichier> [path] [--limit=N] [--feature=<slug>] [--json] [--locale=fr]',
32
+ chain_audit: {
33
+ file_required: 'chain:audit exige un chemin de fichier. Usage : aioson chain:audit <fichier> [--limit=N] [--feature=<slug>] [--json]',
34
+ runtime_unavailable: 'chain:audit runtime db indisponible : {error}',
35
+ query_failed: 'chain:audit échec de la requête chain_edges : {error}',
36
+ no_impacts: 'chain:audit {file} → aucun impact détecté ({duration}ms)',
37
+ results_header: 'chain:audit {file} → {count} impact(s) ({duration}ms) :'
38
+ },
30
39
  context_load: {
31
40
  target_required: 'context:load exige --target=<rule|brain>:<slug>.',
32
41
  agent_required: 'context:load exige --agent=<nom>.',
@@ -27,6 +27,15 @@ module.exports = {
27
27
  'aioson context:pack [path] [--agent=<agente>] [--goal=<texto>] [--module=<modulo-ou-pasta>] [--max-files=8] [--json] [--locale=pt-BR]',
28
28
  help_context_load:
29
29
  'aioson context:load [path] --target=<rule|brain>:<slug> --agent=<nome> [--batch="slug1,slug2"] [--feature=<slug>] [--classification=<MICRO|SMALL|MEDIUM>] [--verbose] [--json] [--locale=pt-BR]',
30
+ help_chain_audit:
31
+ 'aioson chain:audit <arquivo> [path] [--limit=N] [--feature=<slug>] [--json] [--locale=pt-BR]',
32
+ chain_audit: {
33
+ file_required: 'chain:audit exige um caminho de arquivo. Uso: aioson chain:audit <arquivo> [--limit=N] [--feature=<slug>] [--json]',
34
+ runtime_unavailable: 'chain:audit runtime db indisponível: {error}',
35
+ query_failed: 'chain:audit falhou ao consultar chain_edges: {error}',
36
+ no_impacts: 'chain:audit {file} → nenhum impacto detectado ({duration}ms)',
37
+ results_header: 'chain:audit {file} → {count} impacto(s) ({duration}ms):'
38
+ },
30
39
  context_load: {
31
40
  target_required: 'context:load exige --target=<rule|brain>:<slug>.',
32
41
  agent_required: 'context:load exige --agent=<nome>.',