@polycode-projects/the-mechanical-code-talker 2.2.0 → 2.3.1
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/tmct.mjs +4 -5
- package/corpus/LICENSES.json +19 -4
- package/corpus/README.md +48 -0
- package/corpus/generated/README.md +24 -9
- package/corpus/generated/ace-surface-variants.jsonl +4 -1
- package/corpus/generated/manifest.json +4 -4
- package/corpus/prose/manifest.json +512 -0
- package/corpus/prose/sqlite/LICENSE-NOTICE +53 -0
- package/corpus/prose/sqlite/arch.txt +213 -0
- package/corpus/prose/sqlite/atomiccommit.txt +1117 -0
- package/corpus/prose/sqlite/faq.txt +473 -0
- package/corpus/prose/sqlite/fileformat.txt +1589 -0
- package/corpus/prose/sqlite/lang_createtable.txt +1339 -0
- package/corpus/prose/sqlite/lang_insert.txt +580 -0
- package/corpus/prose/sqlite/lang_select.txt +3293 -0
- package/corpus/prose/sqlite/optoverview.txt +908 -0
- package/corpus/prose/sqlite/queryplanner.txt +447 -0
- package/corpus/prose/sqlite/transactional.txt +41 -0
- package/corpus/prose/sqlite/wal.txt +567 -0
- package/corpus/prose/sqlite/whentouse.txt +300 -0
- package/corpus/prose/wikipedia/Apple.txt +4 -0
- package/corpus/prose/wikipedia/Attempto_Controlled_English.txt +169 -0
- package/corpus/prose/wikipedia/Automated_planning_and_scheduling.txt +67 -0
- package/corpus/prose/wikipedia/Bee.txt +7 -0
- package/corpus/prose/wikipedia/Bird.txt +8 -0
- package/corpus/prose/wikipedia/Bone.txt +4 -0
- package/corpus/prose/wikipedia/Book.txt +7 -0
- package/corpus/prose/wikipedia/Bread.txt +6 -0
- package/corpus/prose/wikipedia/Butterfly.txt +6 -0
- package/corpus/prose/wikipedia/Car.txt +1 -0
- package/corpus/prose/wikipedia/Cat.txt +1 -0
- package/corpus/prose/wikipedia/Child.txt +3 -0
- package/corpus/prose/wikipedia/City.txt +2 -0
- package/corpus/prose/wikipedia/Clock.txt +2 -0
- package/corpus/prose/wikipedia/Cooking.txt +1 -0
- package/corpus/prose/wikipedia/Description_logic.txt +660 -0
- package/corpus/prose/wikipedia/Doctor.txt +6 -0
- package/corpus/prose/wikipedia/Dog.txt +4 -0
- package/corpus/prose/wikipedia/Eagle.txt +4 -0
- package/corpus/prose/wikipedia/Emotion.txt +9 -0
- package/corpus/prose/wikipedia/Eye.txt +5 -0
- package/corpus/prose/wikipedia/Family.txt +3 -0
- package/corpus/prose/wikipedia/Farm.txt +4 -0
- package/corpus/prose/wikipedia/Fear.txt +4 -0
- package/corpus/prose/wikipedia/First-order_logic.txt +1518 -0
- package/corpus/prose/wikipedia/Fish.txt +10 -0
- package/corpus/prose/wikipedia/Flower.txt +3 -0
- package/corpus/prose/wikipedia/Food.txt +10 -0
- package/corpus/prose/wikipedia/Grass.txt +9 -0
- package/corpus/prose/wikipedia/Hand.txt +2 -0
- package/corpus/prose/wikipedia/Happiness.txt +3 -0
- package/corpus/prose/wikipedia/Heart.txt +4 -0
- package/corpus/prose/wikipedia/Horse.txt +4 -0
- package/corpus/prose/wikipedia/House.txt +6 -0
- package/corpus/prose/wikipedia/Human.txt +4 -0
- package/corpus/prose/wikipedia/Insect.txt +6 -0
- package/corpus/prose/wikipedia/Interactive_fiction.txt +112 -0
- package/corpus/prose/wikipedia/Knowledge.txt +5 -0
- package/corpus/prose/wikipedia/Knowledge_representation_and_reasoning.txt +87 -0
- package/corpus/prose/wikipedia/LICENSE-NOTICE +94 -0
- package/corpus/prose/wikipedia/Language.txt +10 -0
- package/corpus/prose/wikipedia/Learning.txt +4 -0
- package/corpus/prose/wikipedia/Mammal.txt +3 -0
- package/corpus/prose/wikipedia/Memory.txt +5 -0
- package/corpus/prose/wikipedia/Milk.txt +1 -0
- package/corpus/prose/wikipedia/Mountain.txt +1 -0
- package/corpus/prose/wikipedia/Natural_language_processing.txt +211 -0
- package/corpus/prose/wikipedia/Ostrich.txt +2 -0
- package/corpus/prose/wikipedia/Owl.txt +2 -0
- package/corpus/prose/wikipedia/Penguin.txt +2 -0
- package/corpus/prose/wikipedia/Plant.txt +5 -0
- package/corpus/prose/wikipedia/Rain.txt +1 -0
- package/corpus/prose/wikipedia/Resource_Description_Framework.txt +184 -0
- package/corpus/prose/wikipedia/River.txt +1 -0
- package/corpus/prose/wikipedia/School.txt +8 -0
- package/corpus/prose/wikipedia/Sea.txt +1 -0
- package/corpus/prose/wikipedia/Semantic_Web.txt +114 -0
- package/corpus/prose/wikipedia/Semantic_reasoner.txt +29 -0
- package/corpus/prose/wikipedia/Snow.txt +5 -0
- package/corpus/prose/wikipedia/Sun.txt +5 -0
- package/corpus/prose/wikipedia/Teacher.txt +4 -0
- package/corpus/prose/wikipedia/Team.txt +3 -0
- package/corpus/prose/wikipedia/Text-based_game.txt +17 -0
- package/corpus/prose/wikipedia/Tool.txt +4 -0
- package/corpus/prose/wikipedia/Tree.txt +7 -0
- package/corpus/prose/wikipedia/Weather.txt +4 -0
- package/corpus/prose/wikipedia/Web_Ontology_Language.txt +133 -0
- package/corpus/prose/wikipedia/Wind.txt +8 -0
- package/corpus/prose/wikipedia/Writing.txt +5 -0
- package/corpus/wordnet/generate.mjs +6 -7
- package/package.json +31 -2
- package/src/adapters/corpus/conceptnet.mjs +1 -1
- package/src/adapters/graph-build.mjs +3 -3
- package/src/adapters/memory/blocks.mjs +2 -2
- package/src/adapters/memory/core.mjs +5 -5
- package/src/adapters/providers/bootstrap.mjs +1 -1
- package/src/adapters/providers/fixture.mjs +1 -1
- package/src/adapters/wink-model.mjs +1 -1
- package/src/adapters/wordnet-source.mjs +70 -0
- package/src/domain/answer-variants.json +1 -1
- package/src/domain/ask-vocab.mjs +2 -2
- package/src/domain/ask.mjs +4 -4
- package/src/domain/codegraph.mjs +3 -3
- package/src/domain/corpus-matrix.mjs +87 -0
- package/src/domain/grammar/ace.mjs +11 -11
- package/src/domain/grammar/lexicon.mjs +3 -3
- package/src/domain/inflect.mjs +67 -0
- package/src/domain/interpret/fuzzy.mjs +1 -1
- package/src/domain/interpret/merge.mjs +1 -1
- package/src/domain/interpret/normalize.mjs +1 -1
- package/src/domain/licences.mjs +68 -0
- package/src/domain/memory/capability.mjs +1 -1
- package/src/domain/memory/trust.mjs +2 -2
- package/src/domain/persona/codegen.mjs +123 -0
- package/src/domain/persona/examples.mjs +26 -0
- package/src/domain/persona/tiers.mjs +270 -0
- package/src/domain/publish-gate.mjs +41 -0
- package/src/domain/router/call-validator.mjs +1 -1
- package/src/domain/router/drive.mjs +3 -4
- package/src/domain/router/registry.mjs +12 -13
- package/src/domain/router/resolver.mjs +18 -5
- package/src/domain/router/results.mjs +3 -3
- package/src/domain/router/taught.mjs +4 -3
- package/src/domain/schemaorg/turtle.mjs +25 -0
- package/src/domain/semcor/parse.mjs +87 -0
- package/src/domain/syllogise.mjs +6 -6
- package/src/domain/version-stamp.mjs +36 -0
- package/src/domain/wordnet/yaml.mjs +133 -0
- package/src/services/chat-session.mjs +2 -2
- package/src/services/chat.mjs +2 -2
- package/src/services/cli-args.mjs +4 -4
- package/src/services/finish.mjs +1 -1
- package/src/services/ledger-viz.mjs +2 -3
- package/src/services/sessions.mjs +4 -4
- package/src/services/viz-theme.mjs +3 -4
- package/src/surfaces/web/memory-ask-browser.bundle.js +1 -18
- package/src/domain/router/guardrail.mjs +0 -116
|
@@ -43,7 +43,7 @@ export const IN_REPLY_TO_PROP = "mgx:inReplyTo";
|
|
|
43
43
|
// The provenance-link predicate family: one umbrella object property with two
|
|
44
44
|
// workhorse subproperties, minted in the owned mgx: namespace to match
|
|
45
45
|
// tmct-core.ttl's object-property style.
|
|
46
|
-
|
|
46
|
+
const DERIVED_FROM_PROP = "mgx:derivedFrom"; // umbrella: Fact → Source|Fact
|
|
47
47
|
export const STATED_BY_PROP = "mgx:statedBy"; // a Source directly asserts a Fact
|
|
48
48
|
export const CANONICALISED_FROM_PROP = "mgx:canonicalisedFrom"; // a canonical Fact ← its raw form
|
|
49
49
|
export const SOURCE_RELIABILITY_PROP = "mgx:sourceReliability"; // actor-level (session-scoped) trust nudge on a Source, [0.5,1.5]
|
|
@@ -52,7 +52,7 @@ export const SOURCE_RELIABILITY_PROP = "mgx:sourceReliability"; // actor-level (
|
|
|
52
52
|
// with no session-id segment. A tag that does carry one mints its own
|
|
53
53
|
// per-session Source instead (`${ID}:<sessionId>`, sourceIdFor below).
|
|
54
54
|
export const OPERATOR_SOURCE_ID = "src:operator-chat";
|
|
55
|
-
|
|
55
|
+
const TEACH_SOURCE_ID = "src:teach-chat";
|
|
56
56
|
|
|
57
57
|
const ROLES = new Set(["visitor", "tmct"]);
|
|
58
58
|
const LABEL_CAP = 48; // utterance/fact labels stay skimmable in renders
|
|
@@ -1113,13 +1113,13 @@ export const RULE_KIND_ACTION_SIGNATURE = "action-signature";
|
|
|
1113
1113
|
export const RULE_KIND_ACTION_PRECOND = "action-precond";
|
|
1114
1114
|
export const RULE_KIND_ACTION_EFFECT = "action-effect";
|
|
1115
1115
|
export const RULE_KIND_ACTION_CONSTRAINT = "action-constraint";
|
|
1116
|
-
|
|
1116
|
+
const RULE_KINDS = Object.freeze([
|
|
1117
1117
|
RULE_KIND_COMPOSE2, RULE_KIND_FILTER, RULE_KIND_RECURSIVE,
|
|
1118
1118
|
RULE_KIND_ACTION_SIGNATURE, RULE_KIND_ACTION_PRECOND, RULE_KIND_ACTION_EFFECT,
|
|
1119
1119
|
RULE_KIND_ACTION_CONSTRAINT,
|
|
1120
1120
|
]);
|
|
1121
1121
|
|
|
1122
|
-
|
|
1122
|
+
const RULE_NAME_PROP = "mgx:ruleName";
|
|
1123
1123
|
export const RULE_KIND_PROP = "mgx:ruleKind";
|
|
1124
1124
|
|
|
1125
1125
|
// Per-kind slot contract: JS slot key -> the mgx: attribute it's written under.
|
|
@@ -1514,7 +1514,7 @@ export async function removeFacts(dir, ids) {
|
|
|
1514
1514
|
|
|
1515
1515
|
/** The trust floor a fact must clear before a differing object counts as a real
|
|
1516
1516
|
* contradiction (below it the fact is too weak to contradict anything). */
|
|
1517
|
-
|
|
1517
|
+
const CONTRADICTION_TRUST_FLOOR = 0.5;
|
|
1518
1518
|
|
|
1519
1519
|
export const HAS_A_PREDICATE = "mgx:hasA";
|
|
1520
1520
|
export const CAPABLE_OF_PREDICATE = "mgx:capableOf";
|
|
@@ -13,7 +13,7 @@ import { emptyEntities } from "../source.mjs";
|
|
|
13
13
|
import { createGraphService } from "./graph-service.mjs";
|
|
14
14
|
|
|
15
15
|
/** The parsed empty bootstrap graph. */
|
|
16
|
-
|
|
16
|
+
function bootstrapGraph() {
|
|
17
17
|
return parseEntities(emptyEntities());
|
|
18
18
|
}
|
|
19
19
|
|
|
@@ -15,7 +15,7 @@ import { createGraphService } from "./graph-service.mjs";
|
|
|
15
15
|
/** A compact but type-complete entities payload: modules, a class hierarchy
|
|
16
16
|
* (Base ← Widget ← Button), a method with a full signature, an attribute, a
|
|
17
17
|
* module global, and a commit — wired by one edge of every closed kind. */
|
|
18
|
-
|
|
18
|
+
const FIXTURE_ENTITIES = Object.freeze({
|
|
19
19
|
generated_at: "2026-07-05T00:00:00.000Z",
|
|
20
20
|
bootstrap: false,
|
|
21
21
|
prefixes: { seon: "http://se-on.org/ontologies/seon.owl#", mgx: "urn:tmct:mgx#" },
|
|
@@ -26,7 +26,7 @@ export function registerWinkModel(factory) {
|
|
|
26
26
|
|
|
27
27
|
/** Load `{ winkNLP, model }` once, or null when wink isn't available. Prefers a
|
|
28
28
|
* registered browser factory; otherwise falls back to Node module resolution. */
|
|
29
|
-
|
|
29
|
+
function loadWinkModel() {
|
|
30
30
|
if (cached !== undefined) return cached;
|
|
31
31
|
try {
|
|
32
32
|
const pair = injected ? injected() : nodeRequireWink();
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// wordnet-source.mjs — reads a LOCAL Open English WordNet clone off disk and
|
|
2
|
+
// indexes it. The clone is never vendored, never committed, never part of the
|
|
3
|
+
// npm package: point TMCT_WORDNET_SRC at it, or keep it at the default path.
|
|
4
|
+
//
|
|
5
|
+
// This is the disk half of the WordNet reader. The parsing half is pure and
|
|
6
|
+
// lives in src/domain/wordnet/yaml.mjs, so it is testable with no clone
|
|
7
|
+
// present; everything here needs the real files.
|
|
8
|
+
|
|
9
|
+
import { readFile, readdir } from "node:fs/promises";
|
|
10
|
+
import { existsSync } from "node:fs";
|
|
11
|
+
import { homedir } from "node:os";
|
|
12
|
+
import { join } from "node:path";
|
|
13
|
+
import { parseYaml } from "../domain/wordnet/yaml.mjs";
|
|
14
|
+
|
|
15
|
+
export const WORDNET_SRC = process.env.TMCT_WORDNET_SRC || join(homedir(), "projects", "globalwordnet", "english-wordnet");
|
|
16
|
+
export const WORDNET_YAML_DIR = join(WORDNET_SRC, "src", "yaml");
|
|
17
|
+
|
|
18
|
+
/** True iff a WordNet clone is readable at `yamlDir`. Callers use this to fail
|
|
19
|
+
* with a one-line message rather than a stack trace: these are maintainer
|
|
20
|
+
* tools, never a build dependency. */
|
|
21
|
+
export function hasWordnetSource(yamlDir = WORDNET_YAML_DIR) {
|
|
22
|
+
return existsSync(yamlDir);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Load one or more noun.<x>/verb.<x>.yaml files into a flat synset-id -> record map. */
|
|
26
|
+
export async function loadSynsets(files, yamlDir = WORDNET_YAML_DIR) {
|
|
27
|
+
const map = new Map();
|
|
28
|
+
for (const f of files) {
|
|
29
|
+
const path = join(yamlDir, f);
|
|
30
|
+
if (!existsSync(path)) continue;
|
|
31
|
+
const parsed = parseYaml(await readFile(path, "utf8"));
|
|
32
|
+
for (const [id, rec] of Object.entries(parsed)) map.set(id, rec);
|
|
33
|
+
}
|
|
34
|
+
return map;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Load the entries-<letter>.yaml files that could contain any of `words`
|
|
38
|
+
* (only the letters actually needed — 28 files, ~1MB-3MB each, no reason to
|
|
39
|
+
* load all 28 when a clump only needs a handful of letters). Returns
|
|
40
|
+
* word -> { n: [{id, synset}], v: [...], a: [...] }. */
|
|
41
|
+
export async function loadEntriesFor(words, yamlDir = WORDNET_YAML_DIR) {
|
|
42
|
+
const letters = new Set();
|
|
43
|
+
for (const w of words) {
|
|
44
|
+
const c = w[0].toLowerCase();
|
|
45
|
+
letters.add(/[a-z]/.test(c) ? c : "0");
|
|
46
|
+
}
|
|
47
|
+
const index = new Map();
|
|
48
|
+
for (const letter of letters) {
|
|
49
|
+
const path = join(yamlDir, `entries-${letter}.yaml`);
|
|
50
|
+
if (!existsSync(path)) continue;
|
|
51
|
+
const parsed = parseYaml(await readFile(path, "utf8"));
|
|
52
|
+
for (const [word, byPos] of Object.entries(parsed)) {
|
|
53
|
+
if (!words.has(word)) continue;
|
|
54
|
+
const senses = {};
|
|
55
|
+
for (const [pos, rec] of Object.entries(byPos || {})) {
|
|
56
|
+
if (pos === "form") continue;
|
|
57
|
+
const list = Array.isArray(rec?.sense) ? rec.sense : [];
|
|
58
|
+
senses[pos] = list.map((s) => ({ id: s.id, synset: s.synset })).filter((s) => s.synset);
|
|
59
|
+
}
|
|
60
|
+
index.set(word, senses);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return index;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Every noun.*.yaml synset in the clone. */
|
|
67
|
+
export async function loadAllNounSynsets(yamlDir = WORDNET_YAML_DIR) {
|
|
68
|
+
const files = (await readdir(yamlDir)).filter((f) => f.startsWith("noun."));
|
|
69
|
+
return loadSynsets(files, yamlDir);
|
|
70
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 1,
|
|
3
|
-
"curation": "Hand-curated
|
|
3
|
+
"curation": "Hand-curated, and it has to stay that way: each pool was cross-checked against real Open English WordNet synsets where a sense fit, but most of these are domain-specific code-graph connector phrases whose sense-1 convention resolves to the wrong sense -- e.g. 'locate' -> 'turn up', 'record' -> 'enter, put down', 'module' -> 'mental faculty'. So every entry below was accepted or rejected by hand against the exact rendered sentence it replaces, never auto-accepted from a raw synset match. That is why no generator reproduces this file and no drift guard can check it. See src/domain/answer-variants.mjs for how these are selected at render time.",
|
|
4
4
|
"pools": {
|
|
5
5
|
"defined-in": {
|
|
6
6
|
"base": "defined in",
|
package/src/domain/ask-vocab.mjs
CHANGED
|
@@ -211,7 +211,7 @@ export function stripTrailingScopeFiller(text) {
|
|
|
211
211
|
|
|
212
212
|
/** Trailing bare discourse tags ("how many of those then"). "too" can stack
|
|
213
213
|
* ("is UserController a validator too then"), hence the double pass below. */
|
|
214
|
-
|
|
214
|
+
const TRAILING_DISCOURSE_TAG = Object.freeze(["then", "though", "too"]);
|
|
215
215
|
|
|
216
216
|
const TRAILING_DISCOURSE_TAG_RE = new RegExp(
|
|
217
217
|
`\\s+(?:${TRAILING_DISCOURSE_TAG.join("|")})\\s*[?.!]*$`, "i",
|
|
@@ -219,7 +219,7 @@ const TRAILING_DISCOURSE_TAG_RE = new RegExp(
|
|
|
219
219
|
|
|
220
220
|
/** Trailing comma-delimited discourse clauses ("what is a class, please
|
|
221
221
|
* explain"), anchored on a literal comma so this never fires mid-phrase. */
|
|
222
|
-
|
|
222
|
+
const TRAILING_DISCOURSE_CLAUSE = Object.freeze(["please explain", "explain"]);
|
|
223
223
|
|
|
224
224
|
const TRAILING_DISCOURSE_CLAUSE_RE = new RegExp(
|
|
225
225
|
`,\\s*(?:${TRAILING_DISCOURSE_CLAUSE.join("|")})\\s*[?.!]*$`, "i",
|
package/src/domain/ask.mjs
CHANGED
|
@@ -1786,7 +1786,7 @@ function evalQualCheck(graph, ast, opts) {
|
|
|
1786
1786
|
|
|
1787
1787
|
/** Compile any compositional AST to a result object traverse() returns for the
|
|
1788
1788
|
* simple path — {matches, …} plus compositeKind/compositeMiss flags render() reads. */
|
|
1789
|
-
|
|
1789
|
+
function evalComposite(graph, ast, opts = {}) {
|
|
1790
1790
|
if (ast.node === "miss") return { compositeMiss: true, reason: ast.reason || null, matches: [] };
|
|
1791
1791
|
if (ast.node === "exists") return evalExists(graph, ast);
|
|
1792
1792
|
if (ast.node === "qualCheck") return evalQualCheck(graph, ast, opts);
|
|
@@ -1828,7 +1828,7 @@ const compositeList = (matches) => listJoin(matches.slice(0, OVERFLOW_CAP)
|
|
|
1828
1828
|
+ (matches.length > OVERFLOW_CAP ? `, …and ${matches.length - OVERFLOW_CAP} more` : "");
|
|
1829
1829
|
|
|
1830
1830
|
/** A compositional worked example for the rephrase hint. */
|
|
1831
|
-
|
|
1831
|
+
function compositionalHint() {
|
|
1832
1832
|
return 'compositional queries also work: "which functions call X and call Y", "what calls something that imports X", "public methods of X", "list functions" / "show me the classes", "how many classes", "which module has the most imports", "find me the payment class", or (after a listing) "which of those are tested"';
|
|
1833
1833
|
}
|
|
1834
1834
|
|
|
@@ -2047,7 +2047,7 @@ export function rephraseHint() {
|
|
|
2047
2047
|
* It gets a line that says what the store actually holds instead. A NULL
|
|
2048
2048
|
* graph is UNKNOWN, not empty (see chat.mjs's noCodeGraph), so it keeps the
|
|
2049
2049
|
* index-shaped advice. */
|
|
2050
|
-
|
|
2050
|
+
function touchesRephraseHint(graph = null) {
|
|
2051
2051
|
if (graph && moduleCountOf(graph) === 0) {
|
|
2052
2052
|
return "This store holds no code index, so it records no modules or commits to look through.";
|
|
2053
2053
|
}
|
|
@@ -3516,7 +3516,7 @@ function isHelpRequest(query) {
|
|
|
3516
3516
|
* they are content that may honestly fail to resolve.
|
|
3517
3517
|
* 3. SYNONYM — rewrite surviving near-canonical words to the closed vocab.
|
|
3518
3518
|
* Bounded (one token removed per noise iteration; hard guard) and deterministic. */
|
|
3519
|
-
|
|
3519
|
+
function relaxParse(graph, query, { nlp = undefined, contextId = null, prev = null } = {}) {
|
|
3520
3520
|
const from = applyNegationFrames(normalizeQuery(String(query || "")));
|
|
3521
3521
|
let tokens = splitWords(from);
|
|
3522
3522
|
if (!tokens.length) return null;
|
package/src/domain/codegraph.mjs
CHANGED
|
@@ -128,11 +128,11 @@ function basename(p) {
|
|
|
128
128
|
|
|
129
129
|
const isProvRef = (r) => /^(git|turn):/.test(String(r || ""));
|
|
130
130
|
|
|
131
|
-
|
|
131
|
+
function turnRefCount(ind) {
|
|
132
132
|
return (ind?.derived_from || []).filter(isProvRef).length;
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
-
|
|
135
|
+
function mentionTotal(ind) {
|
|
136
136
|
const fromMentions = (ind?.mentions || []).reduce((n, m) => n + (Number(m?.count) || 0), 0);
|
|
137
137
|
return fromMentions + turnRefCount(ind);
|
|
138
138
|
}
|
|
@@ -1402,7 +1402,7 @@ const MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "
|
|
|
1402
1402
|
|
|
1403
1403
|
/** Map of lowercased author name → that author's Commit individuals (payload order).
|
|
1404
1404
|
* Tolerates both attribute-key conventions (author / commitAuthor), like commitLine. */
|
|
1405
|
-
|
|
1405
|
+
function authorIndex(graph) {
|
|
1406
1406
|
const idx = new Map();
|
|
1407
1407
|
for (const ind of graph?.individuals || []) {
|
|
1408
1408
|
if ((ind.class || "") !== "Commit") continue;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// corpus-matrix.mjs — the fold and the two gap heuristics behind the
|
|
2
|
+
// capability-by-lane coverage matrix, plus the table renderer. Pure: rows in,
|
|
3
|
+
// counts and text out, so the heuristics can be tested against a handful of
|
|
4
|
+
// made-up rows instead of whatever test/corpus happens to hold today.
|
|
5
|
+
//
|
|
6
|
+
// scripts/corpus-matrix.mjs keeps the readdir, the readFile and the printing.
|
|
7
|
+
|
|
8
|
+
/** A row's capability group: the first two dot-segments of its key, so
|
|
9
|
+
* "ask.alias.two-hop" and "ask.alias.miss" are one capability. */
|
|
10
|
+
export const groupOfKey = (key) => key.split(".").slice(0, 2).join(".");
|
|
11
|
+
|
|
12
|
+
/** The key a row is counted under. A row with no key is still a row, and
|
|
13
|
+
* hiding it would understate the lane. */
|
|
14
|
+
export const keyOfRow = (row) => String(row.key ?? "(no key)");
|
|
15
|
+
|
|
16
|
+
/** A key segment naming a miss, a guard or a negation — the row that pins what
|
|
17
|
+
* a capability DECLINES to do, as opposed to its happy path. */
|
|
18
|
+
const NEGATIVE_RE = /(honest-miss|miss|guard|negation|negative|never|decline|refus|unsolvable|unknown|hedge|no-antecedent|untouched|empty)/;
|
|
19
|
+
|
|
20
|
+
export const isNegativeKey = (key) => NEGATIVE_RE.test(key);
|
|
21
|
+
|
|
22
|
+
/** Fold `{ lane, row }` pairs into the two indexes every view needs: the count
|
|
23
|
+
* per group per lane, and the full keys each group was built from. */
|
|
24
|
+
export function tallyRows(entries) {
|
|
25
|
+
const counts = new Map(); // group -> Map<lane, rowCount>
|
|
26
|
+
const fullKeys = new Map(); // group -> Set<full key>
|
|
27
|
+
for (const { lane, row } of entries) {
|
|
28
|
+
const key = keyOfRow(row);
|
|
29
|
+
const group = groupOfKey(key);
|
|
30
|
+
if (!counts.has(group)) counts.set(group, new Map());
|
|
31
|
+
const perLane = counts.get(group);
|
|
32
|
+
perLane.set(lane, (perLane.get(lane) ?? 0) + 1);
|
|
33
|
+
if (!fullKeys.has(group)) fullKeys.set(group, new Set());
|
|
34
|
+
fullKeys.get(group).add(key);
|
|
35
|
+
}
|
|
36
|
+
return { counts, fullKeys };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** The groups the gap heuristics judge. bench.* rows assert a rig runs rather
|
|
40
|
+
* than pinning a capability, so "no negative row" says nothing there. */
|
|
41
|
+
export const behaviourGroups = ({ counts }) =>
|
|
42
|
+
[...counts.keys()].filter((g) => !g.startsWith("bench.")).sort();
|
|
43
|
+
|
|
44
|
+
const rowTotal = (counts, group) => [...counts.get(group).values()].reduce((a, b) => a + b, 0);
|
|
45
|
+
|
|
46
|
+
/** Groups a single row pins end to end. A review candidate, not a hole. */
|
|
47
|
+
export function thinGroups(tally) {
|
|
48
|
+
return behaviourGroups(tally).filter((g) => rowTotal(tally.counts, g) === 1);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Groups whose keys never name a miss, guard or negation — a happy path is
|
|
52
|
+
* pinned and the decline is not. A review candidate, not a hole. */
|
|
53
|
+
export function groupsWithNoNegativeRow(tally) {
|
|
54
|
+
return behaviourGroups(tally).filter((g) => ![...tally.fullKeys.get(g)].some(isNegativeKey));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** The lanes a group has rows in, in the order given. */
|
|
58
|
+
export const lanesOfGroup = ({ counts }, group) => [...counts.get(group).keys()];
|
|
59
|
+
|
|
60
|
+
/** One row per group, one column per lane, an empty cell where a lane has no
|
|
61
|
+
* row for that group. The header row comes first. */
|
|
62
|
+
export function matrixRows({ counts }, lanes) {
|
|
63
|
+
const groups = [...counts.keys()].sort();
|
|
64
|
+
return [
|
|
65
|
+
["key", ...lanes],
|
|
66
|
+
...groups.map((group) => [
|
|
67
|
+
group,
|
|
68
|
+
...lanes.map((lane) => {
|
|
69
|
+
const n = counts.get(group).get(lane);
|
|
70
|
+
return n ? String(n) : "";
|
|
71
|
+
}),
|
|
72
|
+
]),
|
|
73
|
+
];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** `rows` (header first) as fixed-width text, with a rule under the header.
|
|
77
|
+
* Each column is as wide as its widest cell; trailing padding is trimmed. */
|
|
78
|
+
export function renderTable(rows) {
|
|
79
|
+
const [header, ...body] = rows;
|
|
80
|
+
const widths = header.map((h, col) => Math.max(h.length, ...body.map((r) => r[col].length)));
|
|
81
|
+
const renderLine = (cells) => cells.map((c, col) => c.padEnd(widths[col])).join(" ").trimEnd();
|
|
82
|
+
return [
|
|
83
|
+
renderLine(header),
|
|
84
|
+
renderLine(widths.map((w) => "-".repeat(w))),
|
|
85
|
+
...body.map(renderLine),
|
|
86
|
+
].join("\n");
|
|
87
|
+
}
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// punctuation, morphology is the lexicon's suffix fold.
|
|
9
9
|
//
|
|
10
10
|
// parseAce(sentence, lexicon) → { pattern, triples, residue } | null
|
|
11
|
-
// pattern one of the PATTERNS below
|
|
11
|
+
// pattern one of the PATTERNS below.
|
|
12
12
|
// triples [{ subject, predicate, object, kind, n? }] — OWL-labelled string
|
|
13
13
|
// triples shaped for src/adapters/memory/core.mjs's appendFact (which
|
|
14
14
|
// normalizes subject/object via normFactTerm: "tmct:module" is
|
|
@@ -36,18 +36,18 @@ import {
|
|
|
36
36
|
// singularOnly below, and lexicon.mjs's lookupNoun doc for what this prunes).
|
|
37
37
|
const SINGULAR_ONLY_DET = new Set(["a", "an"]);
|
|
38
38
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
39
|
+
const PATTERN_SUB_CLASS_OF = "subClassOf";
|
|
40
|
+
const PATTERN_TYPE_ASSERTION = "typeAssertion";
|
|
41
|
+
const PATTERN_RELATION = "relation";
|
|
42
|
+
const PATTERN_SOME_VALUES_FROM = "someValuesFrom";
|
|
43
|
+
const PATTERN_CARDINALITY = "cardinality";
|
|
44
|
+
const PATTERN_DISJOINT_WITH = "disjointWith";
|
|
45
|
+
const PATTERN_POSSESSIVE = "possessive";
|
|
46
|
+
const PATTERN_ADJECTIVE = "adjective";
|
|
47
|
+
const PATTERN_CAPABILITY = "capability";
|
|
48
48
|
|
|
49
49
|
/** The pattern field's full domain, in the README's table order. */
|
|
50
|
-
|
|
50
|
+
const PATTERNS = Object.freeze([
|
|
51
51
|
PATTERN_SUB_CLASS_OF, PATTERN_TYPE_ASSERTION, PATTERN_RELATION, PATTERN_SOME_VALUES_FROM,
|
|
52
52
|
PATTERN_CARDINALITY, PATTERN_DISJOINT_WITH, PATTERN_POSSESSIVE, PATTERN_ADJECTIVE,
|
|
53
53
|
PATTERN_CAPABILITY,
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
import coreLexiconRaw from "./lexicon-core.json" with { type: "json" };
|
|
26
26
|
|
|
27
27
|
/** The CURIE namespace every tmct lexicon mints terms under. */
|
|
28
|
-
|
|
28
|
+
const DEFAULT_NS = "tmct:";
|
|
29
29
|
|
|
30
30
|
/** Determiner tokens the grammar consumes (pattern table's every/a/no…). */
|
|
31
31
|
export const DETERMINERS = Object.freeze({
|
|
@@ -160,7 +160,7 @@ export function lookupNoun(lexicon, word, opts = {}) {
|
|
|
160
160
|
/** Every lexicon entry `word` could plausibly resolve to, ranked the same as
|
|
161
161
|
* lookupNoun's top choice but without discarding a genuine alternate (e.g.
|
|
162
162
|
* die/dice returns both entries). */
|
|
163
|
-
|
|
163
|
+
function lookupNounCandidates(lexicon, word, opts = {}) {
|
|
164
164
|
const w = String(word ?? "").toLowerCase();
|
|
165
165
|
const standalone = lexicon.nouns.get(w);
|
|
166
166
|
const irregular = lexicon.nounPlurals.get(w);
|
|
@@ -195,7 +195,7 @@ export function lookupVerb(lexicon, word) {
|
|
|
195
195
|
|
|
196
196
|
/** Every verb entry `word` could plausibly resolve to via foldCandidates,
|
|
197
197
|
* most-specific-fold-first — the verb sibling of lookupNounCandidates. */
|
|
198
|
-
|
|
198
|
+
function lookupVerbCandidates(lexicon, word) {
|
|
199
199
|
const w = String(word ?? "").toLowerCase();
|
|
200
200
|
const out = [];
|
|
201
201
|
const seen = new Set();
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// inflect.mjs — the regular English -s/-ed/-ing rules, applied to a lemma.
|
|
2
|
+
//
|
|
3
|
+
// WordNet carries lemmas only ("rest" is present, "rests" is absent), and it is
|
|
4
|
+
// the inflected forms that collide with the fuzzy repair tier's targets —
|
|
5
|
+
// "rests" is one edit from "tests". So the real-word collision table expands
|
|
6
|
+
// every lemma through these rules before it looks for collisions.
|
|
7
|
+
//
|
|
8
|
+
// These are the REGULAR rules and nothing else. No irregular table, no stress
|
|
9
|
+
// model: pastOf("run") is "runned" and pastOf("make") is "maked". That is the
|
|
10
|
+
// intended shape. The table's job is to name words the repair tier must not
|
|
11
|
+
// rewrite, and inflectionsOf is generous on purpose (see below) — an extra form
|
|
12
|
+
// costs one repair we decline to make, and the sentence misses honestly, while
|
|
13
|
+
// a missing form costs a real word rewritten into a different question,
|
|
14
|
+
// answered with confidence. The first is the cheaper mistake.
|
|
15
|
+
|
|
16
|
+
import { STOPWORDS } from "./interpret/normalize.mjs";
|
|
17
|
+
import {
|
|
18
|
+
FUZZY_TARGET_WORDS, FUZZY_REPAIR_MIN_LENGTH, fuzzyMatchInSet, fuzzyBound,
|
|
19
|
+
} from "./interpret/fuzzy.mjs";
|
|
20
|
+
|
|
21
|
+
const VOWELS = new Set(["a", "e", "i", "o", "u"]);
|
|
22
|
+
const isVowel = (c) => VOWELS.has(c);
|
|
23
|
+
|
|
24
|
+
/** A single final consonant after a single vowel doubles before -ed/-ing
|
|
25
|
+
* ("run" -> "running"). w, x and y never double. Stress is not modelled, so a
|
|
26
|
+
* second syllable doubles too ("visit" -> "visitting"). */
|
|
27
|
+
export function doublesFinalConsonant(w) {
|
|
28
|
+
const [c3, c2, c1] = [w.at(-3), w.at(-2), w.at(-1)];
|
|
29
|
+
if (!c3 || isVowel(c1) || "wxy".includes(c1)) return false;
|
|
30
|
+
return isVowel(c2) && !isVowel(c3);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function pluralOf(w) {
|
|
34
|
+
if (/(?:s|x|z|ch|sh)$/.test(w)) return `${w}es`;
|
|
35
|
+
if (/[^aeiou]y$/.test(w)) return `${w.slice(0, -1)}ies`;
|
|
36
|
+
return `${w}s`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function pastOf(w) {
|
|
40
|
+
if (w.endsWith("e")) return `${w}d`;
|
|
41
|
+
if (/[^aeiou]y$/.test(w)) return `${w.slice(0, -1)}ied`;
|
|
42
|
+
if (doublesFinalConsonant(w)) return `${w}${w.at(-1)}ed`;
|
|
43
|
+
return `${w}ed`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function gerundOf(w) {
|
|
47
|
+
if (w.endsWith("ie")) return `${w.slice(0, -2)}ying`;
|
|
48
|
+
if (w.endsWith("e") && !/(?:ee|oe|ye)$/.test(w)) return `${w.slice(0, -1)}ing`;
|
|
49
|
+
if (doublesFinalConsonant(w)) return `${w}${w.at(-1)}ing`;
|
|
50
|
+
return `${w}ing`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Every surface form of `w` the collision table counts as real English. */
|
|
54
|
+
export const inflectionsOf = (w) => [w, pluralOf(w), pastOf(w), gerundOf(w)];
|
|
55
|
+
|
|
56
|
+
/** The words in `realWords` that the repair tier would rewrite onto one of its
|
|
57
|
+
* targets: long enough to reach the tier, not a stopword, not a target itself,
|
|
58
|
+
* and within the fuzzy bound of some target. Sorted, so the table it feeds is
|
|
59
|
+
* reproducible. */
|
|
60
|
+
export function collisionsFrom(realWords) {
|
|
61
|
+
return [...realWords]
|
|
62
|
+
.filter((w) => w.length >= FUZZY_REPAIR_MIN_LENGTH)
|
|
63
|
+
.filter((w) => !STOPWORDS.has(w))
|
|
64
|
+
.filter((w) => !FUZZY_TARGET_WORDS.includes(w))
|
|
65
|
+
.filter((w) => fuzzyMatchInSet(w, FUZZY_TARGET_WORDS, fuzzyBound(w)) !== null)
|
|
66
|
+
.sort();
|
|
67
|
+
}
|
|
@@ -80,7 +80,7 @@ const REAL_WORD_COLLISIONS = new Set(collisionData.words);
|
|
|
80
80
|
* what it says, and if we don't record that relation the honest answer is a
|
|
81
81
|
* miss. Only the words the tier could actually reach are tabled; anything else
|
|
82
82
|
* never gets this far. */
|
|
83
|
-
|
|
83
|
+
const isRealEnglishWord = (w) => REAL_WORD_COLLISIONS.has(w);
|
|
84
84
|
|
|
85
85
|
/** A query word may be canonicalized only if it is plain alphabetic, not a
|
|
86
86
|
* stopword, and not already vocabulary. Dotted/digit terms (file names, shas)
|
|
@@ -123,7 +123,7 @@ export function mergeStrategyResults(results) {
|
|
|
123
123
|
* for alternateLines. Template only, reads straight off the parsed fields (the
|
|
124
124
|
* same discipline as ask.mjs's describeParse; callers with richer noun tables
|
|
125
125
|
* may pass their own describe). */
|
|
126
|
-
|
|
126
|
+
function describeAlternate(p) {
|
|
127
127
|
if (!p) return "something else";
|
|
128
128
|
if (p.ambiguousParse) return "one of several readings";
|
|
129
129
|
const obj = p.object ?? p.subject ?? "?";
|
|
@@ -404,7 +404,7 @@ export function applyNegationFrames(text) {
|
|
|
404
404
|
// Phrasing frames: route natural phrasings of a members-of-class or
|
|
405
405
|
// where-defined question onto the canonical shape the grammar answers.
|
|
406
406
|
// First match wins; run after applyNegationFrames.
|
|
407
|
-
|
|
407
|
+
const PHRASING_FRAMES = Object.freeze([
|
|
408
408
|
// MEMBERS-of-class → "what does X contain".
|
|
409
409
|
{ re: /^what\s+(?:functions?|methods?|members?|attributes?|fields?|properties)\s+(?:are|is)\s+(?:in|inside|within)\s+(?:the\s+)?(.+?)\??$/i, to: (m) => `what does ${m[1]} contain` },
|
|
410
410
|
{ re: /^what\s+(?:functions?|methods?|members?|attributes?|fields?|properties)\s+(?:does|do)\s+(.+?)\s+have\??$/i, to: (m) => `what does ${m[1]} contain` },
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// licences.mjs — the allowlist, the SPDX expression rule, and the two pure
|
|
2
|
+
// reads the licence check needs. Pure: objects and strings in, verdicts out, so
|
|
3
|
+
// the rule that gates CI can be tested without an installed node_modules tree.
|
|
4
|
+
//
|
|
5
|
+
// The check itself (scripts/check-licences.mjs) shells out to `npm ls` and
|
|
6
|
+
// reads each package's package.json off disk. That is the disk half. This is
|
|
7
|
+
// the deciding half, and the deciding half is where the edge cases are.
|
|
8
|
+
|
|
9
|
+
export const ALLOWED = new Set([
|
|
10
|
+
"MIT",
|
|
11
|
+
"ISC",
|
|
12
|
+
"BSD-2-Clause",
|
|
13
|
+
"BSD-3-Clause",
|
|
14
|
+
"Apache-2.0",
|
|
15
|
+
"MPL-2.0",
|
|
16
|
+
"0BSD",
|
|
17
|
+
"CC0-1.0",
|
|
18
|
+
"Unlicense",
|
|
19
|
+
]);
|
|
20
|
+
|
|
21
|
+
/** The licence `pkg` declares, across the three shapes npm has used: the
|
|
22
|
+
* current string, the legacy `{ type }` object, and the legacy `licenses[]`
|
|
23
|
+
* array (read as a choice, so it joins with OR). */
|
|
24
|
+
export function licenseFromPackageJson(pkg) {
|
|
25
|
+
if (typeof pkg.license === "string") return pkg.license;
|
|
26
|
+
if (pkg.license && typeof pkg.license.type === "string") return pkg.license.type;
|
|
27
|
+
if (Array.isArray(pkg.licenses)) return pkg.licenses.map((l) => l.type).join(" OR ");
|
|
28
|
+
return "(none declared)";
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** True iff `license` is inside the allowlist.
|
|
32
|
+
*
|
|
33
|
+
* A flat OR expression passes when any part is allowlisted, because OR is a
|
|
34
|
+
* choice and we can take the allowlisted one. A flat AND expression needs
|
|
35
|
+
* every part. Everything else — nesting, a WITH exception, an OR and an AND in
|
|
36
|
+
* the same expression, even a redundantly parenthesised single licence like
|
|
37
|
+
* "(MIT)" — returns false and gets reviewed by hand. Those are rare enough
|
|
38
|
+
* that a parser would be more code than the reviews it saves, and false is the
|
|
39
|
+
* safe direction: it stops CI and asks a human, rather than waving through an
|
|
40
|
+
* expression it only half understood. */
|
|
41
|
+
export function isAllowed(license) {
|
|
42
|
+
if (ALLOWED.has(license)) return true;
|
|
43
|
+
const inner = license.replace(/^\(/, "").replace(/\)$/, "");
|
|
44
|
+
if (/[()]|\bWITH\b/.test(inner)) return false;
|
|
45
|
+
if (inner.includes(" OR ") && !inner.includes(" AND ")) {
|
|
46
|
+
return inner.split(" OR ").some((part) => ALLOWED.has(part.trim()));
|
|
47
|
+
}
|
|
48
|
+
if (inner.includes(" AND ") && !inner.includes(" OR ")) {
|
|
49
|
+
return inner.split(" AND ").every((part) => ALLOWED.has(part.trim()));
|
|
50
|
+
}
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Every installed package in an `npm ls --json` dependency tree, deduped by
|
|
55
|
+
* name@version and sorted by name. A node with no `path` is a peer or optional
|
|
56
|
+
* dependency that was never installed, so it cannot ship and is skipped. */
|
|
57
|
+
export function installedPackages(deps) {
|
|
58
|
+
const seen = new Map();
|
|
59
|
+
(function walk(level) {
|
|
60
|
+
for (const [name, node] of Object.entries(level ?? {})) {
|
|
61
|
+
if (node && node.path && node.version) {
|
|
62
|
+
seen.set(`${name}@${node.version}`, { name, version: node.version, path: node.path });
|
|
63
|
+
}
|
|
64
|
+
if (node) walk(node.dependencies);
|
|
65
|
+
}
|
|
66
|
+
})(deps);
|
|
67
|
+
return [...seen.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
68
|
+
}
|
|
@@ -55,7 +55,7 @@ export const NEG_CAPABLE_OF_PREDICATE = negatedPredicate(CAPABLE_OF_PREDICATE);
|
|
|
55
55
|
* order it lists them in. One constant each, in one place, so the verbosity of
|
|
56
56
|
* every case-4 answer is tuned by editing two lines. */
|
|
57
57
|
export const CAPABILITY_REPORT_CAP = 6;
|
|
58
|
-
|
|
58
|
+
const byTrustThenName = (a, b) => (b.trust || 0) - (a.trust || 0) || String(a.subject).localeCompare(String(b.subject));
|
|
59
59
|
|
|
60
60
|
const asSet = (v) => (v instanceof Set ? v : new Set(Array.isArray(v) ? v : [v]));
|
|
61
61
|
|
|
@@ -69,7 +69,7 @@ export const SOURCE_PRIOR = Object.freeze({
|
|
|
69
69
|
entailed: 0.3,
|
|
70
70
|
});
|
|
71
71
|
|
|
72
|
-
|
|
72
|
+
const RECENCY_HALF_LIFE_MS = 30 * 24 * 60 * 60 * 1000; // 30 days
|
|
73
73
|
export const RECENCY_FLOOR = 0.9; // recency multiplier stays within [0.9, 1.0]
|
|
74
74
|
|
|
75
75
|
// Actor-level (session-scoped) trust — a bounded nudge on a Source's type
|
|
@@ -156,7 +156,7 @@ export function computeTrust(fact, sourcesById = {}, opts = {}) {
|
|
|
156
156
|
|
|
157
157
|
// Laplace/"add-k" pseudo-count: without it a single data point would saturate
|
|
158
158
|
// mgx:sourceReliability to the bare max/min immediately.
|
|
159
|
-
|
|
159
|
+
const RELIABILITY_CONFIDENCE_PSEUDOCOUNT = 19;
|
|
160
160
|
|
|
161
161
|
/** Pure actor-level reliability from a session's asserted-vs-contradicted
|
|
162
162
|
* track record (findContradictions, core.mjs), confidence-scaled by sample
|