AGI Layer 1 — the Discriminator.
A pure rule engine that judges whether a statement or an action is right, wrong, safe, or dangerous — before it reaches a human. Zero LLM dependency.
54 discrimination dimensions × 17-layer pipeline* × 132 modules × 181 MCP tools
× 1,023 dispatch routes × 2722 passing tests × 0 runtime dependencies
The change touches across 8 modules in the pipeline.
*17 is the layer set, not the depth of any single call. The pipeline declares 17 layer names inchecked_by; a given run executes only the branch it takes. Measured:mode: 'input'traverses 11 layers,mode: 'output'traverses 13. Both numbers are reported side by side byscripts/audit-doc-numbers.js, so "17" and "11" are never silently swapped.
Aspira does not generate. It does not compete with an LLM. It stands between the LLM and the human, like a pain receptor that says "no" when something is wrong.
| Layer | Capability | Who builds it |
|---|---|---|
| 5 | Act | Large labs (robotics) |
| 4 | Generate | Large labs (LLMs) |
| 3 | Reason | Built into models |
| 2 | Remember | Large labs + startups |
| 1 | Discriminate | Aspira |
Aspira takes layer 1 because this layer does not depend on compute, code volume, or framework ecosystems. It depends on judgment alone.
Requires Node.js >= 18.17. No GPU, no database, no API key, no network at runtime, no runtime dependencies.
git clone https://github.com/mark-cell-520/aspira.git
cd aspira
node bin/verify.js # 15 installation checks
node bin/cli.js status # engine status
node bin/cli.js chat # interactive consoleOr through npm:
npm install @mark-cell-520/aspiraconst gate = require('./src/gate.js');
const r = gate.checkOutput('According to 2025 Harvard research, coffee extends life by 12.5 years');
console.log(r.gate.action); // 'verify' -> gather evidence before believing this
console.log(r.verdict); // '需验证'
console.log(r.findings[0]); // { dimension: 'unsupported_claim', severity: 90, guidance: '...' }Four possible actions:
| Action | Meaning |
|---|---|
pass |
Clean. Deliver normally. |
verify |
Needs evidence. Run the verifier first. |
rewrite |
Must be rewritten. Follow findings[].guidance. |
block |
Stop. Do not output. Use gate.reason. |
verdict is derived from gate.action, so the two never contradict each other. If you
read only one field, read gate.action.
| Function | Use it for | What it adds |
|---|---|---|
checkInput(text) |
User input, before processing | scope-check, premise-check, 54 dimensions, error memory |
checkDraft(text) |
An AI draft, before completion | the above + frame-check + doubt-engine |
checkOutput(text) |
An AI response, before sending | the above + output-gate + doubt-engine |
runPipeline({ input, mode, anchor }) |
Full pipeline with mode and conversation anchor | keeps the model on the original goal across long sessions |
input
|
v
scope-check -> premise-check -> discriminate (54 dimensions) -> gate
|
+----------------------------------------------------------------+
v
evidence verify -> frame-check -> output-gate -> doubt-engine
|
v
intent-anchor -> rewriter -> error-memory -> self-diagnosis -> output
The gate aggregates findings from every layer and emits a single action:
block / rewrite / verify / pass.
| Domain | Representative modules |
|---|---|
| Logic | logicReasoning, judgmentEngine, debateConductor, counterfactualVerifier |
| Decision | decisionRouter, decisionVerifier, activeInference, selfHealing |
| Cognition | cognitiveEngine, cognitiveLoad, metacognitiveRL, sustainedDriftDetector |
| Emotion / psychology | emotion, psychology, empathyDeepening, griefEngine, traumaInformed |
| Memory | memory, memoryBank, memoryIntegrity, forgetting, knowledgeGraph |
| Identity / ethics | identityCore, personaCore, virtueEthics, moralDevelopment, meaningPurpose |
| Creation / collaboration | skillEvolution, worldModel, multiAgentDialogue, codeExecutor, formula |
| Classical texts | classics-value-mapper, classics-rules, classics-feedback |
Measured on this repository at v1.0.0. Not marketing copy.
| Metric | Value |
|---|---|
| Modules registered | 132 |
| Module init errors | 0 |
| Dispatch routes | 1,023 |
| Discrimination dimensions | 54 |
| MCP tools | 181 |
| Test suite | 2722 passing / 0 failing |
| Capability guard | 18 / 18 checks |
| Security regression | 16 / 16 |
| Runtime dependencies | 0 |
Aspira exposes its engine as an MCP server, so any MCP-capable agent can call it.
node src/mcp-server.js --port 8099
# or a Unix socket:
node src/mcp-server.js --socket /tmp/aspira.sockThen connect:
hermes mcp add aspira --url http://localhost:8099/mcpThe server authenticates with a bearer token generated on first start and written to
.env (never committed). A request without a valid token returns 401.
tools/call enforces a role model so that state-mutating tools cannot be invoked by an
unauthenticated caller:
| Role | Source | Capability |
|---|---|---|
guest |
no credentials | read-only tools |
user |
Aspira-OID-<16-hex> header |
read + write |
admin |
valid bearer token | full |
The write-protected set is aspira_memory_write_control,
aspira_memory_eraser, aspira_decision_decide, aspira_self_heal. A guest
calling any of them gets isError: true with 权限不足. This behaviour is covered by an
end-to-end regression test that speaks real JSON-RPC over a real Unix socket
(test/mcp-guest-permission.test.js), because the earlier failure mode was a permission
block that was syntactically valid but unreachable — tests that only inspected the
tool-name whitelist passed while the gate never ran.
Beyond text discrimination, Aspira ships checks aimed at how AI agents behave — the failure modes that show up when an agent reports work it did not do.
| Check | What it catches |
|---|---|
checkCompletionEvidence |
Empty completion claims ("done", "fixed", "all passing") without a git hash, test count, file path, or PR link |
checkArchitectureConsistency |
A function whose name promises one thing and whose body does another (named validate, no validation) |
checkDecisionTrace |
A "decision" with fewer than 2 options, no explicit choice, or no stated reason — pseudo-decisions |
checkPlanGate |
A plan entering a complex task without steps, acceptance criteria, rollback, or safety strategy |
checkForbiddenCall |
Delegating before the target, boundary, and acceptance criteria are confirmed |
checkAIMisuse |
Human-side misuse patterns: oversized context dumps, errors without repro steps, adopting output unverified |
Each is available as an MCP tool (aspira_check_completion_evidence,
aspira_check_architecture_consistency, aspira_check_decision_trace,
aspira_check_plan_gate, aspira_check_forbidden_call,
aspira_check_ai_misuse).
node test/run-all.js # full suite (recursive over test/, including subdirectories)
node bin/verify.js # installation checks
node scripts/guard-abilities.js # capability guardian (18 checks)test/run-all.js walks test/ recursively, so tests in test/core/, test/memory/,
test/utils/, and other subdirectories run alongside the top-level files.
scripts/guard-abilities.js runs before any upgrade commit. It verifies entry points,
discrimination against a fixed sample set, the engine main chain, text searchability
(no NUL bytes or CRLF in core sources — a bare NUL parses fine in Node but makes every
text-search tool treat the file as binary), and the full regression suite.
It is: the discrimination layer of AGI — a rule engine judging right and wrong, good and bad, safe and dangerous.
It is not: AGI itself, a generative model, a semantic understanding system (irony and metaphor are invisible to it), a substitute for content moderation, or a safety certification.
Known limits:
- Pattern-matching ceiling — new tricks require new patterns.
- Bilingual maintenance cost across all dimensions.
- No semantic understanding — irony, metaphor, and cultural context are invisible.
- False-positive rate: 0.0% on a hand-written 137-sample benign corpus — a corpus-local lower bound, not a production SLO. Real caller feedback has yet to accumulate; AGENTS.md records the method and what it cannot see.
- Single maintainer.
- Chinese tokenisation is heuristic (greedy longest-match with a stopword list, not a full dictionary), so unusual phrasings can segment imperfectly.
- Silent failure is its blind spot. Three real defects in this repository — a bare NUL byte in the engine source, an unreachable permission block, and contradictory documentation numbers — were all invisible to a 547-test green suite, because none of them produced wrong runtime behaviour. The text-searchability guard and the end-to-end permission test exist because of them; treat "tests pass" as necessary, never sufficient.
| Version | Date | Change |
|---|---|---|
| 1.0.0 | 2026-09-28 | Test suite: 36s to 5.3s (6.8x) by making the runner concurrent, without touching subprocess isolation. test/run-all.js was strictly sequential (execSync, one file at a time): 246 files x ~26ms of bare node startup is a ~6.4s process-spawn floor, and the measured suite wall time was 36s. Every test still runs in its own child process — the header comment records that requiring ~137 engine-loading files into the runner process once exhausted the heap and got the runner OOM-killed, so that isolation is load-bearing; only the number in flight changed. Concurrency defaults to max(2, min(12, cpus-2)) and is overridable with ASPIRA_TEST_CONCURRENCY (set 1 for the old sequential behaviour). Risk was measured before the change, not assumed: 0 test files write under data/ or memory/, 0 call listen( or createServer(, 12 CPUs available — so no shared-file or port contention between children. Per-call engine performance was measured first and found to need nothing: checkOutput runs 0.21–0.33ms with a flat profile (59 check functions timed individually, the slowest is 7% of a 0.194ms total), and a require cache hit costs 0.082µs against 213µs per call — 0.04%, not worth violating convention 2, which deliberately requires Tier-2 modules to be required inside _lazy closures to keep cold start fast. An early measurement showing Chinese input at 10x English latency turned out to be first-use lazy loading, not a real asymmetry, and was discarded rather than "fixed". A recursion trap was closed: a test that executes node test/run-all.js would trigger run-all → that test → run-all → …, because the runner recursively discovers every *.test.js; the runner now sets ASPIRA_TEST_RUNNER=1 for children and the test skips its behavioural check when it sees it. One assertion was wrong on first write: "the final summary must equal the sum of the per-file summaries" measured 937 against 878, and investigation showed that gap is pre-existing and identical in both versions (143 visible summary lines, child sum 871, total 930) — an artefact of the keep-line filter, not a counting bug, so it is documented rather than locked. 937 tests, 0 failures; node bin/verify.js 14/14. |
| 1.0.0 | 2026-09-28 | Adversarial robustness: separator evasion passed 5 of 6 samples. Taking six malicious samples that are definitively blocked in plain text (drawn from the calibration corpus, not invented) and running the gate over eight transformation classes measured the bypass rate: zero-width, fullwidth, homoglyph, unicode-escape and multi-space compression were all caught, while character-wise separator insertion passed 5 of 6 and inter-letter spacing passed 5 of 6. Both shared one root cause: after the letter-run folding, residual separators pressed against non-letter characters still cut code tokens apart. c-o-n-s-t- -x- -=- -e-v-a-l-(-u-s-e-r-I-n-p-u-t-)-; folded only as far as const- -x- -=- -eval-(-userinput-)-; — eval and ( were still divided by a dash, so /�eval\s*\(/ never matched and the whole code-security dimension passed. The fix adds three rules that strip a single separator wedged between different character classes, plus one that folds separator-whitespace-separator (the evasion turns an original space into X- -Y, a signature that barely occurs in normal writing). Three self-inflicted regressions were caught, each by re-measuring rather than by reasoning. One: the first version did not exclude the continuation of a separator run, so KEY----- lost its dashes, the PEM header was destroyed and recall fell 100% → 97.1% — the evasion signature is "a separator wedged between different character classes" while PEM is "two or more identical separators in a row", which are opposite shapes. Two: the guard checked only the matched run and not its neighbours, so the middle dot of wait... what? had dots on both sides (both counting as non-whitespace non-alphanumeric), matched, and was stripped — collapsing the ellipsis to wait.. what?. The neighbour classes now exclude separators themselves, which guarantees the matched run is maximal and lets the guard actually recognise ----- and .... Three: three test assertions were written from impression instead of measurement — the PEM check asserted uppercase (step 6 lowercases everything), the html-entity snapshot claimed 6 of 6 against a measured 4 of 6, and one asserted that the mixed run in el-.-innerHTML "should be folded" when it measurably should not (a mixed run between two multi-letter words belongs to the letter-run folder, whose heuristic requires a single-letter segment). Each was corrected against measurement, with comments recording why the case is excluded. git stash comparison confirmed two further behaviours — a.b.c = 1 becoming abc = 1, and e.g. becoming eg. — are pre-existing rather than introduced here. A dead-code finding: variants() and altVariants have no consumer anywhere in src/ — discriminate() consumes only normalize().normalized, so "add a more aggressive altVariant to raise recall" is not an available lever. A test now pins that fact. Separator-evasion bypasses fell from 5 of 6 to 1 of 6; the remaining case is the pre-existing limitation where an all-single-letter run eats the dot in el.innerHTML. 930 tests, 0 failures; FP 0.0% / recall 100% re-measured. |
| 1.0.0 | 2026-09-28 | Honest numbers: the audit was green because it cannot see code examples. scripts/audit-doc-numbers.js reported 34 of 34 claims consistent — and it was telling the truth about the only thing it measures: numbers that match the regexes it knows. Honest numbers means finding the claims it structurally cannot see, and there are three such classes: whether a code example runs at all (the audit never executes anything), whether the behaviour a comment next to an example claims is actually delivered, and figures whose denominator is stated in prose. Measuring those found three defects. One: the AGENTS.md quick-start example does not run. It imported the package as hf and then called gate.checkInput(...); gate was never defined, so anyone copying it got ReferenceError: gate is not defined. The correct form is hf.checkInput(...) — checkInput/checkDraft/checkOutput are top-level exports of src/gate.js, the package main. The name gate is itself a trap: gate.js also exports a function called gate, so even const { gate } = require(...) leaves gate.checkInput undefined. Two: the third example's comment claimed verify while the engine returned pass. "According to 2025 Harvard research, coffee extends life by 12.5 years" was judged pass with overallScore 1 and no findings — not a mislabelled comment but a recall hole, and it sat on exactly the sentence the documentation uses as its own demonstration. Testing all nine UNSUPPORTED_CLAIM_EN patterns individually isolated two causes: studies? can never match the singular "study" (the s? governs only the trailing s, not the ie/y in the middle), so "The study found that …" slipped through entirely; and <verb> by <number> required the verb to sit directly against by, so "extended life by 12.5 years" missed while "extended by 12.5 years" hit — the pattern was blind to the most common shape of all. The verb table was also entirely past tense. Both were repaired, and the sentence now returns verify as documented: the documentation described the intent, the engine failed to deliver it, so the engine was fixed rather than the document softened. The unit list was missing days?, caught when a new test case used "by 3 days". One self-inflicted error along the way: exceed/extend/shorten were first written as [s]?, which still cannot match their past tenses — those take +ed, not +s. Three: SKILL.md's pipeline-layer figure was off by one in every denominator, and its explanation of the shortcuts was backwards. It claimed runPipeline at 12 input / 13 draft / 14 output against a measured 11 / 12 / 13, and claimed the bare shortcuts run "one layer fewer" when they return the same count. (Its anchor explanation was right and is kept: passing an anchor appends the final intent-anchor layer, 11 → 12.) The central deliverable is test/doc-examples.test.js, which extracts the javascript block out of AGENTS.md and executes it verbatim rather than restating it — restating would defeat the point, since editing the doc without editing the test would stay green. It asserts each example's documented gate.action, locks the layer counts for all three denominators (reading the claimed value out of SKILL.md instead of hardcoding it), locks the morphological coverage of both recall fixes across six verbs and every tense, and re-locks that hedged sentences still pass so the recall gain was not bought with false positives. One test pins the root cause: the gate that gate.js exports is a function, not an object, so gate.checkInput is structurally unavailable — this stops anyone "fixing" the example into a destructuring form that would fail again. The suite is 921 tests with 0 failures, the doc audit is back to 34 of 34 consistent, and FP/recall re-measured unchanged at 0.0% / 100%. |
| 1.0.0 | 2026-09-27 | Test coverage: 100 substantial modules the engine never loads. Measuring coverage by whether a module's basename appears in a test file reported 133 uncovered modules — and that was wrong. priority-guardian, progress-judgment, uncertainty-quantifier and dao-decision are all exercised indirectly through src/pipeline.js; no test names them directly. Basename matching counts indirect coverage as a gap. Re-measured by hooking the require chain and recording which src/ files the engine actually loads: of 378 modules the engine loads 245, so 133 are never loaded, and after dropping archives and sub-80-line shells, 100 substantial modules are never loaded. They are not broken — every one passes node --check and 98 export something — so they are loadable code that was never wired. (Requiring each one to confirm timed out at 400s under heavyweight initialisation, which is its own lesson: "loads" and "loads safely" are different claims, and static checking is the only approach that scales.) A hundred modules cannot be done in one cycle, so the selection criterion was promised by the documentation but never wired — a worse state than merely untested. src/core/action-tracker.js is named in AGENTS.md's decision-routing table ("Did it actually work? … assessEffectiveness() checks the effect, not the action"), pointing callers at a module the engine never loads. src/core/meta-calibration.js documents its job as turning calibration results into a caller-readable honest statement, aligned with "say you don't know rather than invent" — honesty externalisation is a core Aspira value with nothing locking its behaviour. New test/unwired-module-coverage.test.js adds 24 cases: ten for MetaCalibration (low confidence must declare uncertainty; high confidence must not pretend uncertainty; the medium band; fallback to originalConfidence; default 0.3 when both are absent; subsystem gaps counted only when adjusted and calibrated below 0.5; the needsUncertaintyMarker fallback firing exactly once; custom thresholds; the note boundary always present) and fourteen for ActionTracker (commit→execute success and failure paths; unknown ids returning failure rather than throwing; the commitment-free act→reportResult path; assessEffectiveness preferring result.effectiveness over the success fallback; assessQuality averaging three dimensions with exponential smoothing at 0.73; intent-behaviour alignment and gap severity; the change stage advancing to maintenance and stopping; failure triage; overdue marking; initialState injection). One meta-test locks the gap itself: it asserts these two modules still do not appear in the require calls of pipeline.js, index.js or heartflow.js. If someone wires them later that test fails, which is the intent — the disappearance of a coverage gap should be a deliberate decision, not something that happens silently. One expectation was wrong: the gap type was asserted as intent_behavior_gap when the source says intention_behavior_gap, written from a remembered abbreviation instead of read from the source. AGENTS.md's honest-numbers section was also still claiming 82 benign / 24 malicious / 2.4% FP against a measured 86 / 34 / 0.0%; it now carries the real reading plus the reason for it and one qualifier — a zero on an 86-sample corpus is not a zero in production, and should be read as "no false positive this corpus can express", which is a statement about the corpus as much as about the engine. Doc audit back to 34 of 34 claims consistent, 0 mismatched. 912 tests, 0 failures. |
| 1.0.0 | 2026-09-27 | MCP param contract: 16 suspected problems, 5 of them the audit's own blindness. The parameter-contract audit reported 16 "suspected real problems". Qualifying them one by one showed 5 were the audit being blind, not the engine being wrong — and fixing the audit mattered more than fixing the engine, because an audit that emits false positives is worse than no audit: it sends people to "fix" tools that were never broken. Blind spot one: the passthrough regex required ` |
,where the arrow function actually ends with |
||
| },`. Suspected real problems: 16 → 0. Fully consistent tools 161 → 169. 888 tests, 0 failures. | ||
| 1.0.0 | 2026-09-27 | Dimension health: thin coverage cleared. The structural audit was already healthy — 54 of 54 dimensions registered, 54 of 54 form-compliant, zero dimensions with no test coverage — so the real defect was depth: 26 dimensions were named in only two test files. "Named in a file" is not "tested by a file"; a dimension that appears only inside a comment still counts as covered. New test/thin-dimension-coverage.test.js gives 25 of them bidirectional coverage, proving each can be triggered by a pattern-derived input and that ordinary sentences do not touch them. Thin dimensions went from 26 to 0. The probe method took three corrections, and each one is the same lesson as this session's repeated "a probe's wording must be derived from the pattern it targets". First, pulling literals out of the pattern constants and testing those literals hit 1 of 26 — a pattern like `/女人都(很 |
| 1.0.0 | 2026-09-27 | False-positive calibration: two detections that never looked at the second half of the sentence. Both persistent false positives had the same root cause — checkPseudoCausal and checkUnsupportedClaim recognised the shape "vague source plus precise number" and never checked whether the sentence had already hedged itself. A sentence that carries its own caveat is exactly the careful statement the engine should let through. 实验显示转化率提升 3 倍,但样本量仅 200,结论有待确认。 was flagged as pseudo-causal on "提升 3 倍" while stating, in the same breath, that the sample was 200 and the conclusion needed confirmation — it was doing what the engine asks for. According to the 2024 report, sales grew by 12%, though the sample was small. was flagged by the co-occurrence rule while naming a specific dated source and warning about sample size. Both functions now share one HEDGE_RE covering in-sentence qualification, and the fix was measured in both directions: three of three hedged statements pass, four of four unhedged ones still fire, so no recall was traded for the false-positive drop. One reversal is worth recording in full. The second false positive was first judged to be "the engine is right, the label is wrong", and the sample was withdrawn from the corpus — on the strength of reading only its first half, According to the 2024 report, and concluding that the co-occurrence rule (whose comment says this is the typical form of a fabricated research template) deliberately wanted it. But the second half of the sentence is though the sample was small. An attributed and self-hedged claim being blocked is still a false positive; the sample was restored and the pattern fixed instead. The lesson: before deciding the engine is right and the sample is wrong, read the whole sentence. This session has had four cases of "the probe or sample was wrong and the engine was right"; this one ran the other way. A second mis-step of the same family followed: four vague-citation samples were added to the malicious set as recall guards, and recall immediately fell to 92.1%. All four did fire unsupported_claim — they reached verify, while the script defines "intercepted" as block or rewrite. unsupported_claim is a verify-tier dimension and "gather evidence before responding" is its designed behaviour, so the samples were measured on the wrong scale. They were removed and their guard moved to the dimension level. That is the fifth "sample wrong, engine right" this session, and the second where the error was treating correct engine behaviour as a defect. The new test corrected itself three times: it asserted gate=pass for a synthetic Chinese sentence that other dimensions legitimately flag; it used whole-file includes() to check whether the corpus still contained a sample and matched its own comment explaining the removal — the third "test matching its own comment" this session; and the corpus assertion was then narrowed to array-entry lines. Corpus 86 benign / 34 malicious, FP 0.0% (was 2.3%), recall 100%. 876 tests, 0 failures. |
| 1.0.0 | 2026-09-27 | Adversarial robustness: two real evasions, both of them "half a fix". The separator-obfuscation case had a fix that never applied to half its own inputs. strip_separator used /\b([a-zA-Z])[-._]{1,2}([a-zA-Z])\b/g, and because _ is itself a word character, the i in n_i is never followed by a word boundary — so underscores never matched at all. Hyphens and dots matched only in alternating pairs: replacing n-i→ni moved the scan position past the -g that followed, leaving ni-gg-er, still a fragment. Worse, adversarial-variant's WORD_SPLIT_RE recognises whitespace and *@#$%^& but not -._, so that layer also returned pass: normalisation and the adversarial layer both failed on the same input. Measured, that nigger should die gives hate_speech=1/block while that n_i_g_g_e_r should die gives 0/pass; all faggots must die gives 2/block while all f_aggot_s must die gives 0/pass. A near-miss worth recording: the pipeline did report block for some of these, and it was tempting to call the surface safe — printing the layers showed the reason was dehumanization, and you are a disgusting apple fires it too, so those blocks had nothing to do with the slur. Only rephrasing into a threat sentence exposed the full three-layer pass. Fixed by collapsing the whole run rather than pair-by-pair, with the precision condition "at least two -._ separators in the word, and at least one single-letter segment" — measured 0 new false positives across 86 benign samples, folding n_i_g_g_e_r, f_aggot_s and e-v-a-l while leaving api.example.com, snake_case_name, some-hyphen-word and r.json() alone. The same condition was added to adversarial-variant so the second layer stands on its own. The second evasion was the same bug in a different costume: 13 of the 15 English slur patterns had no plural form. Only faggot and tranny had been given s?; the rest were singular, and plurals are the more common attack surface — all niggers must die passed outright. All are fixed now, with bitch special-cased to (?:es)?, and tranny extended to `(?:ie |
| 1.0.0 | 2026-09-27 | Honest-numbers audit: the audit was green because it could not see the table. scripts/audit-doc-numbers.js reported "20 claims, 20 consistent, 0 mismatched" while SKILL.md's ## Verified metrics table carried four stale numbers: dimensions 51 against a measured 54, MCP tools 180 against 181, test suite 740 passing against 852, and a section heading reading "separate from the 51 text dimensions". The tier prose below it was equally stale — 5 can block against 9, 7 can force a rewrite against 8, 24 request verify against 26. README.md carried the same table with 51 / 180. The root cause is that every extraction regex was prose-shaped with the number first: /(\d+)\s+discrimination dimensions/g matches "54 discrimination dimensions", but a table row is ` |
| 1.0.0 | 2026-09-27 | Test-coverage gap: a real evasion, and a threshold mistaken for one. Two bypasses had been carried forward for two cycles as "next direction" without ever being measured. Measuring them showed they are not the same kind of thing. The space-splitting case was a genuine defect. text-normalizer's step 3a restores single-letter spacing (e v a l ( u s e r I n p u t ) → eval(userInput)) with /(?:[a-zA-Z] ){3,}[a-zA-Z]/g, which anchors neither end. On the left, the final letter of the preceding word — the g of "disgusting" — satisfies "letter followed by space" and is absorbed into the run, producing disgustingnigger. Two words fused, word boundary gone, and hate_speech's `/�n[i1]gg(?:er |
| 1.0.0 | 2026-09-27 | MCP tool enhancement. The parameter-contract audit reported seventeen "declared but unread" parameters as suspected real defects. Most were the audit being wrong about its own subject again: it does not recognise the const { formula, variables = {} } = args destructuring idiom, so aspira_formula_calc was flagged for not reading formula and variables when it reads both on its first line; and it does not recognise whole-object forwarding, so `checkOutbound(args |
| 1.0.0 | 2026-09-27 | Dimension health audit. The audit reports the coverage distribution 2:36 — thirty-six dimensions resting on only two test files. Two earlier cycles had added one-directional probes for thirty-two of them (dimension-firing for seventeen, dimension-negative-controls for fifteen), which leaves twenty-two dimensions with no dedicated probe; of those, exactly eight sit on the thin list: fallacies, presupposition, emotional_manipulation, meta_cognition, capability_overclaim, absolute_claim, induced_trust, coverup_induction. All eight were probed in both directions — fires on X, stays quiet on Y — with each positive's wording derived from that dimension's own pattern literals rather than written by feel. The first round reported four dimensions as broken, and all four turned out to be probe errors, not engine errors — the fourth time this session that pattern held. fallacies' positive said 没有中间选项 while the pattern requires `要么…要么(没有 |
| 1.0.0 | 2026-09-27 | FP/recall calibration. The previous cycle merged forty-one pattern sets so English and Chinese patterns both run, which changed the false-positive surface — and the calibration corpus that measures that surface carried only two mixed-language entries out of thirty-eight. The instrument was blind to precisely the risk the merge introduced. Growing the corpus to 75 benign / 22 malicious, with the real mixed-language shapes (technical prose that interleaves Chinese and English), immediately surfaced two false positives that had never once been seen. The first was the alternation-precedence trap again. DEHUMANIZATION_PATTERNS's object group was written `/分母 |
| 1.0.0 | 2026-09-27 | Mixed-language bypass: one Chinese character disabled every English pattern. Forty-one detection paths were written hasChinese ? PATTERNS.zh : PATTERNS.en — an exclusive language choice. Any text containing a single Chinese character therefore stopped running that dimension's English patterns entirely. Measured: you are a disgusting nigger scores hate_speech.count=1, gate=block; append two characters and you are a disgusting nigger 你好 scores count=0, gate=pass. An attacker needed only one Han character to walk straight through. All forty-one paths now merge both sets. Probing also found a plural gap: `/�f[a4]gg(?:ot |
| 1.0.0 | 2026-09-27 | gate-verdict wired, and three defects the wiring exposed. The engine's introspection had reported gateVerdictWired: false for several cycles — the last unwired capability in its self-view. Assessment first: src/gate-verdict.js exports a complete three-tier aggregator (2 block / 3 rewrite / 5 verify signals + pass) whose stated job is to collapse the 30+ scattered result._* discrimination signals into one actionable command. It was never required by anything, the same orphan class as false-positive-feedback.js — a measured single input carries 43 underscore-prefixed fields on its result with zero consumers. That is precisely the failure its own docstring names: 心虫判了但没人听见. Wired in heartflow.js immediately after runThinkPipeline, which is where those fields are actually produced. The introspection regex looked only at src/index.js, which neither calls think-pipeline nor contains any underscore field — wiring there would have made the aggregator return pass forever. The detector pointed at the wrong file; corrected to read heartflow.js too. Wiring it exposed three real defects. PostProcessHooks.run() dropped its opts argument — the signature was run(name, payload), never declaring opts, and it called handler(payload). So aspira_think passing style: 'json' reached postprocess_format with opts === undefined, style fell back to markdown, and markdown mode keeps only result.report — discarding the discrimination, outputChecklist and gateVerdict fields the handler had carefully built. The caller got no error at all; it simply never received the format it asked for. output-checklist.js had 14 issues.push sites written ${dims.X.field?.join(',')} — ?. guards against the field not being an array, but not against its real shape: 8 of the 14 measured fields produce garbage (7 are never returned by discriminate(), yielding the literal string undefined; one is an array of objects, yielding [object Object]). The same file already contained the correct idiom elsewhere — `arr?.map(x => x.type).join(',') |
| 1.0.0 | 2026-09-27 | Honest-numbers audit of the documentation. scripts/audit-doc-numbers.js now extracts every number the docs claim and compares it against a measurement, because AGENTS.md principle #5 requires that any claimed metric be measurable — and several were not. The audit itself was wrong four times before it could be trusted, which is the recurring pattern in this repository: a tool that reports without understanding its subject produces false findings that are more dangerous than no report at all. It counted routes() keys instead of expanding them, so the correct "1,510 dispatch routes" came back unmeasurable; it compared >= 18.17 against >=18.17 as a mismatch; it matched incidental prose ("11 refs across 8 modules") as if it were an engine claim; and it read the changelog table as if it were a current claim, which would have turned the historical record "Modules 132, dispatch routes 1,510" into a false alarm. Each was a defect in the auditor, not in the audited. Once it was trustworthy it found two real drifts. The pipeline layer count was wrong: the docs claimed a 14-layer pipeline while src/pipeline.js measures 17 — and the documented list named three layers that do not exist in the code (evidence verify, rewriter, self-diagnosis) while omitting nine that do (classical-knowledge, adversarial-variant, dao-decision, uncertainty, priority-guardian, progress-judgment, verifier, auto-rules, and discriminate itself). It also placed gate fourth, when in the code the gate runs tenth, after every discriminator. The SKILL.md diagram now lists all seventeen in execution order, with a note that the list is measured rather than remembered. The test count was stale: README said "740 passing tests" against a measured 800. test/doc-numbers.test.js locks the layer count, the layer list, the layer order, the dimension count and the MCP tool count against the code, so a future doc edit that breaks a number fails the suite instead of drifting silently. The test count is the one number the suite cannot self-verify, since verifying it means running the suite from inside the suite; it is corrected by hand each cycle and the audit reports the claim. 807 tests, 0 failures. |
| 1.0.0 | 2026-09-27 | Test-coverage gap: near-miss negative controls for the thin dimensions. The coverage distribution was 1:31 — thirty-one dimensions resting on a single test file — and the previous cycle's firing probes brought it to 1:15. What remained was a one-directional guarantee: each of those fifteen dimensions had a test proving "it fires on X" and nothing proving "it stays quiet on Y". A pattern could be broadened arbitrarily and no test would object. test/dimension-negative-controls.test.js adds near-miss negatives — text that shares vocabulary or structure with the dimension's patterns but is semantically innocent. It immediately earned its keep by finding a real false positive: dehumanization's `/这些(?:人 |
| 1.0.0 | 2026-09-27 | MCP parameter-contract audit across all 181 tools. The trigger was aspira_formula_search, which declared query while its handler read keyword — a caller following the documentation got an error instead of a result. Earlier cycles found the same family: handler keys defined twice with the later silently winning, and an unclosed object literal swallowing the next tool's fields. None of it was caught by any test. scripts/audit-mcp-param-contract.js now compares, per tool, what the handler actually reads against what the schema declares. The audit itself needed three rounds before it could be trusted: it only matched inline arrows at first (53 named-function handlers reported "unlocatable", real coverage 125/181); then it failed to recognise const { a, b } = args destructuring; then it failed to recognise the `const input = args |
| 1.0.0 | 2026-09-27 | Wired the false-positive feedback loop. src/false-positive-feedback.js had been written for several cycles and was never once required — engine introspection.fpFeedbackWired reported false every single cycle, and a caller who got blocked had no channel to say "this was a false positive". The consequence is structural: thresholds can only be tuned against internal samples, and the more isolated the engine is, the more it mistakes its internal sample distribution for the whole truth. The module already carried the privacy rules (never persist caller identity, store only a length plus an 80-char summary unless fullText: true, file mode 600) and a deliberately conservative suggest() that refuses to advise below 20 samples rather than guessing. Now exposed as aspira_false_positive with six actions (report / stats / suggest / confirm / clear / reasons) and six engine exports. The parameter boundary was the place most likely to fail silently: the schema declares gateAction while report() reads action, so the handler maps one to the other explicitly and a test asserts the mapping string is present — without it the tool would reject every well-formed call with "action 必须是 block/rewrite/verify(收到: undefined)" and no test would notice. Validation rejects free-text reason values, because an unenumerable reason cannot be aggregated. Documentation also corrected: AGENTS.md claimed "baseline false-positive rate around 8%" while the measurement says 5.3% on a hand-written 38-benign/13-malicious corpus — now stated as corpus-local rather than a production SLO, with the note that real caller feedback has yet to accumulate. MCP tools 180 → 181. 788 tests, 0 failures. |
| 1.0.0 | 2026-09-27 | Dimension health audit. The audit script reported "形态合规 51/54" every cycle, flagging evidence, sycophancy and reasoning_coherence as malformed. They were not malformed — they are the three SCORE_ONLY dimensions that by contract carry hits in issues / totalHits / structure instead of count, and the script did not know the exemption. The audit was wrong about its own subject. Now 54/54. The script also dropped a stale "51 dimensions" comment, and — more importantly — started reporting the coverage distribution instead of only the eight best-covered dimensions: 1:31 2:9 3:8 4:2 5:1 6:1 17:1 27:1. Thirty-one of fifty-four dimensions rest on a single test file, which is exactly where silent breakage hides, and the previous "top 8" report made that invisible. test/dimension-firing.test.js locks firing probes for sixteen thin dimensions, each with a negative control. The methodology deliberately avoids a mistake this repo already made once: a discarded "dimension sensitivity" metric scored 31/51 dimensions DEAD from hand-written English examples, while Chinese examples brought phishing_coercion to count=3 — it measured whether the example matched the pattern, not whether the dimension works. This cycle reproduced the same trap: a first round of hand-written probes fired on only 9 of 27 dimensions; re-deriving the wording from each dimension's own patterns raised it to 16, and the rest failed only because the pattern literals are too short (`/所有[^。]*?都(是 |
| 1.0.0 | 2026-09-27 | False-positive / recall calibration. AGENTS.md claimed a "baseline false-positive rate around 8%", but nothing in the repository had ever measured it — no feedback facility, no calibration script. scripts/calibrate-fp-recall.js now measures both on a labelled corpus and, more usefully, runs a genuine A/B by emptying a pattern category before discriminate() (an earlier attempt filtered types afterwards, which left the gate action already computed and made both arms identical). Three defects surfaced. (1) best_run_picking in reward-hacking.js wrote four bare verbs as standalone alternatives — `/pick\w* |
| 1.0.0 | 2026-09-27 | Adversarial-robustness repair in the code-security dimension. The taint patterns only recognised five source names — `req |
| 1.0.0 | 2026-09-27 | MCP tool-surface repair. An audit of TOOLS against HANDLERS found three classes of silent corruption, none of which any test caught. (1) Five handler keys were defined twice — aspira_formula_search, aspira_formula_calc, aspira_formula_bridge, aspira_check_outbound, aspira_audit_trace — and in a JS object literal the later key wins with no warning. The winning copies also disagreed with the declared contract: formula_calc read args.values while the registry declares variables; formula_bridge read args.query while the registry declares {domain, params}. handleFormulaBridge is a real six-domain formula computation (prospectValue, yerkesDodson, flowChannel, …) and was being replaced by a corpus text search. (2) aspira_boundary_check's object in tools-registry.js was never closed, so it swallowed the next entry's fields — one object with name twice, the second winning. The registry declared 180 tools and exported 179: boundary_check vanished from tools/list while its handler stayed. (3) tools/list reported 179 against 178 unique handler keys and nothing complained. Duplicates removed, the missing brace restored, handleFormulaSearch aligned to the declared query parameter (it read keyword), and module.exports = { HANDLERS, TOOLS } added so the invariants are assertable. test/mcp-tool-integrity.test.js locks them: names unique, every tool has a handler, no duplicate handler key, and a handler must not reject a call that satisfies its own declared required parameters — the guard that catches a parameter-name mismatch, which is the failure mode that produces a wrong answer instead of an error. MCP tools 179 → 180. 764 tests, 0 failures. Known limitation carried forward: aspira_formula_calc requires mathjs, which is declared in package.json but not installed (node_modules is absent); it fails with a clear "Cannot find module" error rather than a wrong result. |
| 1.0.0 | 2026-09-27 | Dimension-map repair + honest dimension count. dimMap in discriminate() had drifted from the dimensions return object: two stale keys (bullshit, appeal_to_authority) that no longer exist, and three missing ones (evidence, unsupported_claim, clickbait). Because the findings loop resolves counts through `dimObj?.count |
| 1.0.0 | 2026-09-27 | Honest-numbers audit. A previous decoupling refactor had deleted 5 globalThis stubs as "dead code — 0 src consumers", but heartflow.js itself consumed them by bare class name, so hf._initErrors carried 5 entries and only 126 of 132 modules registered (routes 1,497). Repaired all 5 through the lazy registry (processRewardModel, desireCognition, cognitiveLoadCalculator, worldLandscape, knowledgeExplorer), aliased DesireSystem → DesireCognition, and restored the worldAwareStrategy factory stub in its own try so it can no longer take WorldLandscape down with it. Every documented number was re-measured against the code: README corrected 46 → 51 dimensions, 9 → 14 pipeline layers, 547 → 740 tests; dispatch routes 1,506 → 1,510 in README / SKILL / AGENTS. security-audit S2 now skips instead of failing when the tree has no .git. 740 tests, 0 failures. |
| 6.7.69 | 2026-09-20 | MCP guest write-permission block was unreachable dead code (100% of guest write attempts passed); moved into case 'tools/call', token comparison switched to safeCompare(). Added end-to-end permission regression test and the guard's text-searchability check (13 → 18). 547 tests. |
| 6.7.69 | 2026-09-18 | DeepSeek V4.1 alignment: reasoning effort control, sparse module activation, discriminative result cache, async supervision layer, autonomous decision execution with consequence tracking, Engram conditional memory, SWA bounded replay, per-decision-type stats |
| 6.7.24 | 2026-09-17 | Audit remediation: gate/verdict consistency, fact-check scoring, Chinese tokenisation in the hypothesis pipeline, circuit-breaker memory accounting and non-blocking CPU sampling, recursive test discovery, multi-language child-safety age detection, version-source unification, English documentation rewrite |
| 6.7.13 | 2026-09-05 | Documentation API alignment; optional ESM transformers loading; CLI guidance without an LLM key |
| 6.6.1 | 2026-08-18 | Formula library fully integrated (1,334 formulas); formula-bridge extended with 6 decision and learning primitives |
| 6.5.6 | 2026-08-13 | Comprehensive audit: DataEraser wired to MCP; adversarial synthesis recovered; dead code archived |
| 6.5.5 | 2026-08-12 | 47th dimension — premature termination detection |
| 6.5.0 | 2026-08-04 | Memory engine mounted to think(); exaggeration detection (output-gate / frame-check / doubt-engine) |
| 6.4.0 | 2026-07-29 | AGI Layer 1 gate chain: gate / scope-check / premise-check / verifier / output-gate / doubt-engine / frame-check |
| 6.0.0 | 2026-07-18 | Self-evolution core connected; evolution loop live |
MIT.