@polycode-projects/the-mechanical-code-talker 1.11.6 → 2.0.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/README.md +244 -48
- package/ROADMAP.md +23 -34
- package/bin/tmct.mjs +117 -81
- package/corpus/LICENSES.json +118 -0
- package/corpus/README.md +17 -13
- package/corpus/conceptnet/README.md +5 -5
- package/corpus/conceptnet/fetch-slice.mjs +1 -1
- package/corpus/conceptnet/filter-dump.mjs +1 -1
- package/corpus/generated/README.md +9 -10
- package/corpus/namenet/README.md +39 -0
- package/corpus/seon/README.md +2 -2
- package/corpus/tier2/generate.mjs +58 -9
- package/corpus/tier2/manifest.json +44 -0
- package/corpus/wordnet/README.md +37 -0
- package/corpus/wordnet/generate.mjs +1 -1
- package/data/templates/constructions/agent-noun-relations.toml +2 -2
- package/data/templates/grammar-rules.toml +1 -1
- package/package.json +13 -22
- package/src/{ask-nlp.mjs → adapters/ask-nlp.mjs} +1 -1
- package/src/{config.mjs → adapters/config.mjs} +1 -1
- package/src/{corpus → adapters/corpus}/conceptnet-map.toml +4 -4
- package/src/{corpus → adapters/corpus}/conceptnet.mjs +3 -3
- package/src/adapters/corpus/construction-banks.mjs +43 -0
- package/src/{corpus → adapters/corpus}/templates.mjs +1 -1
- package/src/{embed.mjs → adapters/embed.mjs} +1 -11
- package/src/{graph-build.mjs → adapters/graph-build.mjs} +6 -6
- package/src/{memory → adapters/memory}/blocks.mjs +2 -2
- package/src/{memory → adapters/memory}/core.mjs +38 -94
- package/src/adapters/prose-tokens.mjs +98 -0
- package/src/{providers → adapters/providers}/bootstrap.mjs +2 -2
- package/src/{providers → adapters/providers}/fixture.mjs +3 -3
- package/src/{providers → adapters/providers}/graph-service.mjs +9 -4
- package/src/{source-slice.mjs → adapters/source-slice.mjs} +2 -2
- package/src/{source.mjs → adapters/source.mjs} +1 -1
- package/src/{toml-config.mjs → adapters/toml-config.mjs} +1 -1
- package/src/{answer-variants.json → domain/answer-variants.json} +1 -1
- package/src/domain/answer-variants.mjs +23 -0
- package/src/{ask-vocab.mjs → domain/ask-vocab.mjs} +37 -5
- package/src/{ask.mjs → domain/ask.mjs} +216 -52
- package/src/{codegraph.mjs → domain/codegraph.mjs} +31 -315
- package/src/{completions → domain/completions}/complete.mjs +16 -10
- package/src/{completions → domain/completions}/graph-adapter.mjs +10 -4
- package/src/{completions → domain/completions}/group.mjs +14 -6
- package/src/{completions → domain/completions}/infer.mjs +57 -39
- package/src/domain/completions/injected.mjs +21 -0
- package/src/{completions → domain/completions}/rank.mjs +15 -8
- package/src/{completions → domain/completions}/search.mjs +4 -2
- package/src/{grammar → domain/grammar}/ace.mjs +3 -3
- package/src/{grammar → domain/grammar}/assert.mjs +12 -8
- package/src/{grammar → domain/grammar}/lexicon-core.json +1 -1
- package/src/{grammar → domain/grammar}/lexicon.mjs +4 -6
- package/src/domain/hash.mjs +147 -0
- package/src/{interpret → domain/interpret}/fuzzy.mjs +42 -4
- package/src/domain/interpret/nlp-registry.mjs +20 -0
- package/src/{interpret → domain/interpret}/normalize.mjs +35 -5
- package/src/{interpret → domain/interpret}/pipeline.mjs +1 -5
- package/src/{interpret → domain/interpret}/strategies/ace.mjs +1 -1
- package/src/{interpret → domain/interpret}/strategies/constructions.mjs +30 -52
- package/src/{interpret → domain/interpret}/strategies/keywords.mjs +48 -26
- package/src/domain/memory/capability.mjs +235 -0
- package/src/domain/memory/fold.mjs +54 -0
- package/src/domain/memory/session-turns.mjs +7 -0
- package/src/{memory → domain/memory}/trust.mjs +48 -0
- package/src/{paraphrase.mjs → domain/paraphrase.mjs} +2 -2
- package/src/{prose.mjs → domain/prose.mjs} +1 -1
- package/src/domain/real-word-collisions.json +1 -0
- package/src/{router → domain/router}/call-validator.mjs +1 -1
- package/src/{router → domain/router}/drive.mjs +34 -25
- package/src/{router → domain/router}/goal-reasoner.mjs +1 -1
- package/src/{router → domain/router}/guardrail.mjs +1 -1
- package/src/{router → domain/router}/planner.mjs +1 -1
- package/src/{router → domain/router}/registry.mjs +5 -5
- package/src/{router → domain/router}/resolver.mjs +16 -13
- package/src/{router → domain/router}/results.mjs +1 -1
- package/src/{router → domain/router}/set-algebra.mjs +1 -1
- package/src/{router → domain/router}/taught.mjs +10 -9
- package/src/{syllogise.mjs → domain/syllogise.mjs} +21 -4
- package/src/domain/vector.mjs +12 -0
- package/src/services/chat-session.mjs +451 -0
- package/src/{chat.mjs → services/chat.mjs} +1238 -680
- package/src/{cli-args.mjs → services/cli-args.mjs} +2 -2
- package/src/services/completions.mjs +55 -0
- package/src/{extensions.mjs → services/extensions.mjs} +7 -7
- package/src/{finish.mjs → services/finish.mjs} +2 -2
- package/src/{memory → services}/fold.mjs +0 -0
- package/src/{import-file.mjs → services/import-file.mjs} +3 -3
- package/src/{index.mjs → services/index.mjs} +21 -12
- package/src/{init.mjs → services/init.mjs} +9 -9
- package/src/{ledger-viz.mjs → services/ledger-viz.mjs} +38 -7
- package/src/{plan-viz.mjs → services/plan-viz.mjs} +65 -26
- package/src/{sentences.mjs → services/sentences.mjs} +1 -1
- package/src/{sessions.mjs → services/sessions.mjs} +4 -5
- package/src/{telemetry.mjs → services/telemetry.mjs} +1 -1
- package/src/{server-http.mjs → surfaces/http/server-http.mjs} +11 -65
- package/src/{tui → surfaces/tui}/app.mjs +3 -3
- package/src/{memory-ask-browser-entry.mjs → surfaces/web/memory-ask-browser-entry.mjs} +5 -5
- package/src/{memory-ask-browser.bundle.js → surfaces/web/memory-ask-browser.bundle.js} +9465 -6366
- package/src/tools/catalog.mjs +29 -0
- package/src/{conformance.mjs → tools/conformance.mjs} +2 -2
- package/src/tools/definitions.mjs +288 -0
- package/src/tools/graph-load.mjs +20 -0
- package/src/tools/handlers/index.mjs +54 -0
- package/src/tools/handlers/kit.mjs +33 -0
- package/src/tools/handlers/tmct-architecture.mjs +7 -0
- package/src/tools/handlers/tmct-ask.mjs +14 -0
- package/src/tools/handlers/tmct-callees.mjs +6 -0
- package/src/tools/handlers/tmct-callers.mjs +6 -0
- package/src/tools/handlers/tmct-calls.mjs +6 -0
- package/src/tools/handlers/tmct-class-history.mjs +6 -0
- package/src/tools/handlers/tmct-cochanges.mjs +6 -0
- package/src/tools/handlers/tmct-context-more.mjs +9 -0
- package/src/tools/handlers/tmct-context.mjs +163 -0
- package/src/tools/handlers/tmct-describe.mjs +15 -0
- package/src/tools/handlers/tmct-exports.mjs +9 -0
- package/src/tools/handlers/tmct-file-history.mjs +6 -0
- package/src/tools/handlers/tmct-history.mjs +6 -0
- package/src/tools/handlers/tmct-impact.mjs +9 -0
- package/src/tools/handlers/tmct-members.mjs +16 -0
- package/src/tools/handlers/tmct-method-history.mjs +6 -0
- package/src/tools/handlers/tmct-search.mjs +22 -0
- package/src/tools/handlers/tmct-signature.mjs +6 -0
- package/src/tools/handlers/tmct-snippet.mjs +37 -0
- package/src/tools/handlers/tmct-subclasses.mjs +16 -0
- package/src/tools/handlers/tmct-tests-for.mjs +6 -0
- package/src/tools/handlers/tmct-untested.mjs +7 -0
- package/src/tools/memory-fallthrough.mjs +65 -0
- package/src/{schema-docs.mjs → tools/schema-docs.mjs} +1 -1
- package/src/tools/server.mjs +61 -0
- package/src/answer-variants.mjs +0 -39
- package/src/hash.mjs +0 -24
- package/src/server.mjs +0 -501
- /package/src/{corpus → adapters/corpus}/unknown-ingest.mjs +0 -0
- /package/src/{graph-merge.mjs → adapters/graph-merge.mjs} +0 -0
- /package/src/{memory → adapters/memory}/inspect.mjs +0 -0
- /package/src/{memory → adapters/memory}/shacl.mjs +0 -0
- /package/src/{prose-nlp.mjs → adapters/prose-nlp.mjs} +0 -0
- /package/src/{repository-interface.mjs → adapters/repository-interface.mjs} +0 -0
- /package/src/{uuid.mjs → adapters/uuid.mjs} +0 -0
- /package/src/{wink-model.mjs → adapters/wink-model.mjs} +0 -0
- /package/src/{completions → domain/completions}/prune.mjs +0 -0
- /package/src/{concept.mjs → domain/concept.mjs} +0 -0
- /package/src/{domain.mjs → domain/domain.mjs} +0 -0
- /package/src/{interpret → domain/interpret}/merge.mjs +0 -0
- /package/src/{interpret → domain/interpret}/strategies/grammar.mjs +0 -0
- /package/src/{interpret → domain/interpret}/strategies/noise-strip.mjs +0 -0
- /package/src/{memory → domain/memory}/bias.mjs +0 -0
- /package/src/{planning.mjs → domain/planning.mjs} +0 -0
- /package/src/{viz-theme.mjs → services/viz-theme.mjs} +0 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// interpret/nlp-registry.mjs — the default lemma/POS adapter slot. The domain
|
|
2
|
+
// parser never imports the wink adapter; a composition point (chat.mjs,
|
|
3
|
+
// server.mjs, index.mjs) registers ask-nlp.mjs's factory here at load time.
|
|
4
|
+
// Nothing registered (the browser bundle, a pure-domain unit test) means
|
|
5
|
+
// defaultNlp() is null — the same adapter-less tier the old
|
|
6
|
+
// `typeof nlpAdapter === "function"` guard selected.
|
|
7
|
+
|
|
8
|
+
let defaultAdapterFactory = null;
|
|
9
|
+
|
|
10
|
+
/** Register the process-default adapter FACTORY (ask-nlp.mjs's nlpAdapter —
|
|
11
|
+
* a function returning the { lemma, isStopWord, posTags } adapter or null). */
|
|
12
|
+
export function setDefaultNlpAdapter(factory) {
|
|
13
|
+
defaultAdapterFactory = factory;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** The default lemma/POS adapter instance, or null when no factory is
|
|
17
|
+
* registered (or the registered one reports the deps unavailable). */
|
|
18
|
+
export function defaultNlp() {
|
|
19
|
+
return typeof defaultAdapterFactory === "function" ? defaultAdapterFactory() : null;
|
|
20
|
+
}
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
import {
|
|
9
9
|
CONTRACTIONS, MISSPELLINGS, WRONG_WORDS, G_DROP, FILLER_WORDS,
|
|
10
10
|
NEGATION_FRAMES, COMMIT_CONTENT_FRAMES, VERB_TO_KIND, ENTITY_TO_TYPE,
|
|
11
|
-
TRAILING_SCOPE_FILLER,
|
|
11
|
+
TRAILING_SCOPE_FILLER, TRAILING_TEMPORAL_ADVERBS,
|
|
12
12
|
} from "../ask-vocab.mjs";
|
|
13
13
|
|
|
14
14
|
export function escapeRegex(s) {
|
|
@@ -337,15 +337,35 @@ export function applyConditionalFrames(text) {
|
|
|
337
337
|
* force lowercase — object/subject terms (module/class names) are
|
|
338
338
|
* meaningfully cased, and every substitution already matches
|
|
339
339
|
* case-insensitively. */
|
|
340
|
+
/** The contraction table applied on its own — "what's on peg-a" -> "what is on
|
|
341
|
+
* peg-a". normalizeQuery's first pass, lifted out for callers that count words
|
|
342
|
+
* and need "what's" counted as the two it stands for, without the filler strip
|
|
343
|
+
* that would take words back off. Pure and idempotent. */
|
|
344
|
+
export function expandContractions(text) {
|
|
345
|
+
return String(text || "").replace(CONTRACTION_RE, (m) => CONTRACTIONS[m.toLowerCase()]);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/** "where is disk-1 now" -> "where is disk-1". A locative question's answer is the
|
|
349
|
+
* same with the adverb or without it, so the word carries nothing to read — but
|
|
350
|
+
* the where template's term capture runs to the end of the string and binds it,
|
|
351
|
+
* which then shows up in the receipt as part of the term. Stripped here, in the
|
|
352
|
+
* shared pre-pass, so both parse strategies see one string and cannot disagree
|
|
353
|
+
* about where the term ends. Anchored to "where is/are/was/were" and to the end:
|
|
354
|
+
* a term of the same name elsewhere in a question is untouched. */
|
|
355
|
+
const WHERE_TRAILING_TEMPORAL_RE = new RegExp(
|
|
356
|
+
`^(where\\s+(?:is|are|was|were)\\s+.+?)\\s+(?:${TRAILING_TEMPORAL_ADVERBS.map(escapeRegex).join("|")})(\\s*[?.!]*)$`,
|
|
357
|
+
"i",
|
|
358
|
+
);
|
|
359
|
+
|
|
340
360
|
export function normalizeQuery(text) {
|
|
341
|
-
let q =
|
|
342
|
-
q = q.replace(CONTRACTION_RE, (m) => CONTRACTIONS[m.toLowerCase()]);
|
|
361
|
+
let q = expandContractions(text);
|
|
343
362
|
q = q.replace(MISSPELLING_RE, (m) => MISSPELLINGS[m.toLowerCase()]);
|
|
344
363
|
q = q.replace(WRONG_WORD_RE, (m) => WRONG_WORDS[m.toLowerCase()]);
|
|
345
364
|
q = q.replace(W_SLASH_RE, "with");
|
|
346
365
|
q = q.replace(FOR_DIGIT_THANKS_RE, (_, w) => `${w} for`);
|
|
347
366
|
q = q.replace(FOR_DIGIT_EXAMPLE_RE, (_, w) => `for ${w}`);
|
|
348
367
|
q = q.replace(KIND_NOUN_ANAPHORA_RE, (_, pron) => pron);
|
|
368
|
+
q = q.replace(WHERE_TRAILING_TEMPORAL_RE, "$1$2");
|
|
349
369
|
q = q.replace(G_DROP, "$1ing");
|
|
350
370
|
q = applyPreambleFrames(q);
|
|
351
371
|
// self-correction runs before subordination/conditional: a false start can
|
|
@@ -495,12 +515,22 @@ export function applyPhrasingFrames(text) {
|
|
|
495
515
|
// <verb> Y") and returns a descriptor, distinct from the rhetorical
|
|
496
516
|
// double-negative rewriter above (which REMOVES negation instead of
|
|
497
517
|
// preserving it as a set operation).
|
|
518
|
+
// The modal negations ("which X cannot <verb> Y", "which X can't <verb> Y")
|
|
519
|
+
// ride the same shape: the graph only records what IS, so the nearest
|
|
520
|
+
// answerable reading of a modal negation is the same factual complement "do
|
|
521
|
+
// not" asks for. Leaving them out doesn't produce a miss — a later strategy
|
|
522
|
+
// reads the modal as filler and answers with the POSITIVE set, the exact
|
|
523
|
+
// inverse of the question. The fused forms are matched HERE rather than
|
|
524
|
+
// expanded in the shared CONTRACTIONS table because that table also feeds the
|
|
525
|
+
// teach lane, where "a penguin cannot fly" is a negative capability teach with
|
|
526
|
+
// its own grounding gate; rewriting it upstream diverted it past that gate.
|
|
498
527
|
const NEGATION_SET_RE = new RegExp(
|
|
499
528
|
"^(?:which|what|who|list|show(?:\\s+me)?|find|give\\s+me)?\\s*(?:the\\s+|all\\s+)?"
|
|
500
529
|
+ "([a-z][a-z-]*)\\s+"
|
|
501
530
|
+ "(?:(?:that|which|who)\\s+)?"
|
|
502
|
-
+ "(?:(?:do|does|did|are|is|was|were|have|has)\\s+
|
|
503
|
-
+ "
|
|
531
|
+
+ "(?:(?:do|does|did|are|is|was|were|have|has|can|could|will|would|should)\\s+not"
|
|
532
|
+
+ "|cannot|can't|won't|couldn't|shouldn't|wouldn't"
|
|
533
|
+
+ "|not)\\s+(.+)$",
|
|
504
534
|
"i",
|
|
505
535
|
);
|
|
506
536
|
|
|
@@ -19,7 +19,7 @@ import { noiseStripStrategy } from "./strategies/noise-strip.mjs";
|
|
|
19
19
|
import { aceStrategy } from "./strategies/ace.mjs";
|
|
20
20
|
import { constructionsStrategy } from "./strategies/constructions.mjs";
|
|
21
21
|
import { mergeStrategyResults } from "./merge.mjs";
|
|
22
|
-
import {
|
|
22
|
+
import { defaultNlp } from "./nlp-registry.mjs";
|
|
23
23
|
|
|
24
24
|
/** Registered strategies, in precedence order: grammar + keyword-spot (shared
|
|
25
25
|
* "graph-query" class), noise-strip (fires only when the anchored grammar
|
|
@@ -43,10 +43,6 @@ export function normalizeInput(input) {
|
|
|
43
43
|
return { raw, text, changed: text !== raw };
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
function defaultNlp() {
|
|
47
|
-
return typeof nlpAdapter === "function" ? nlpAdapter() : null;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
46
|
/** Synchronous strategy run (ask.mjs's parseQuery path). Skips a Promise-
|
|
51
47
|
* returning strategy and drops a throwing one; returns results in precedence order. */
|
|
52
48
|
export function runStrategiesSync(text, ctx = {}, strategies = STRATEGIES) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// interpret/strategies/ace.mjs — the ACE-OWL controlled-fragment grammar
|
|
2
|
-
// (src/grammar/ace.mjs) wired into the pipeline as an additive strategy: its
|
|
2
|
+
// (src/domain/grammar/ace.mjs) wired into the pipeline as an additive strategy: its
|
|
3
3
|
// own class ("ace-fact"), so a clean parse surfaces as an alternate, never
|
|
4
4
|
// displacing a graph-query winner.
|
|
5
5
|
|
|
@@ -1,58 +1,24 @@
|
|
|
1
1
|
// interpret/strategies/constructions.mjs — construction-grammar template
|
|
2
|
-
// banks
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
2
|
+
// banks (pattern -> AST skeleton, slot types validated against
|
|
3
|
+
// RELATIONS/ENTITY_TO_TYPE), registered as their own additive class
|
|
4
|
+
// ("construction") so a match outranks a same-text keyword-spot guess rather
|
|
5
|
+
// than colliding with it.
|
|
6
6
|
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
import { parse as parseToml } from "smol-toml";
|
|
7
|
+
// The banks are DATA that rides in from outside: a composition point
|
|
8
|
+
// (chat.mjs, server.mjs, index.mjs) reads data/templates/constructions/*.toml
|
|
9
|
+
// through corpus/construction-banks.mjs and registers the raw tables here via
|
|
10
|
+
// setConstructionBanks — this strategy never touches the filesystem. Compile
|
|
11
|
+
// is cached once per registration and defensive: an invalid entry is silently
|
|
12
|
+
// dropped, never thrown; no registration means an empty bank (the strategy
|
|
13
|
+
// simply never fires).
|
|
15
14
|
|
|
16
15
|
import { RELATIONS, ENTITY_TO_TYPE } from "../../ask-vocab.mjs";
|
|
17
16
|
import { escapeRegex } from "../normalize.mjs";
|
|
18
17
|
|
|
19
|
-
const STRATEGY_DIR = dirname(fileURLToPath(import.meta.url));
|
|
20
|
-
/** The construction-bank directory (data, not code) — every *.toml file inside
|
|
21
|
-
* is loaded, in filename order, so a future bank is a new committed file, not
|
|
22
|
-
* an edit to this loader. */
|
|
23
|
-
export const CONSTRUCTIONS_DIR = join(STRATEGY_DIR, "..", "..", "..", "data", "templates", "constructions");
|
|
24
|
-
|
|
25
18
|
const VALID_KINDS = new Set(Object.keys(RELATIONS));
|
|
26
19
|
const VALID_ENTITY_TYPES = new Set(Object.values(ENTITY_TO_TYPE));
|
|
27
20
|
const VALID_SHAPES = new Set(["ask", "reverse", "forward", "where", "when", "meta", "mentions"]);
|
|
28
21
|
|
|
29
|
-
/** Read every *.toml file in `dir` (sorted, deterministic) and return the raw
|
|
30
|
-
* parsed tables concatenated: {relations:[...], constructions:[...]}. A
|
|
31
|
-
* missing directory or an unparseable file is DEFENSIVE (per-file: a broken
|
|
32
|
-
* file is skipped, not fatal to the others) — callers get whatever validly
|
|
33
|
-
* parsed, never a thrown error from a data-authoring mistake. */
|
|
34
|
-
export function readConstructionFiles(dir = CONSTRUCTIONS_DIR) {
|
|
35
|
-
let files;
|
|
36
|
-
try {
|
|
37
|
-
files = readdirSync(dir).filter((f) => f.endsWith(".toml")).sort();
|
|
38
|
-
} catch {
|
|
39
|
-
return { relations: [], constructions: [] };
|
|
40
|
-
}
|
|
41
|
-
const relations = [];
|
|
42
|
-
const constructions = [];
|
|
43
|
-
for (const file of files) {
|
|
44
|
-
let parsed;
|
|
45
|
-
try {
|
|
46
|
-
parsed = parseToml(readFileSync(join(dir, file), "utf8"));
|
|
47
|
-
} catch {
|
|
48
|
-
continue; // one malformed file never takes the others down
|
|
49
|
-
}
|
|
50
|
-
if (Array.isArray(parsed.relation)) relations.push(...parsed.relation);
|
|
51
|
-
if (Array.isArray(parsed.construction)) constructions.push(...parsed.construction);
|
|
52
|
-
}
|
|
53
|
-
return { relations, constructions };
|
|
54
|
-
}
|
|
55
|
-
|
|
56
22
|
/** Validate + index the raw [[relation]] rows into noun -> {kind, entityType}.
|
|
57
23
|
* Closed-vocabulary validation: `kind` MUST be
|
|
58
24
|
* one of RELATIONS' own keys and `entityType` (when present) MUST be one of
|
|
@@ -122,24 +88,36 @@ export function buildConstructionTemplates(constructions, agentNounTable) {
|
|
|
122
88
|
return out;
|
|
123
89
|
}
|
|
124
90
|
|
|
91
|
+
let bankSource = null; // raw tables, or a () => raw-tables loader
|
|
125
92
|
let bankCache = null;
|
|
126
93
|
|
|
94
|
+
/** Register the raw construction tables ({relations, constructions}) or a
|
|
95
|
+
* lazy loader returning them (corpus/construction-banks.mjs's
|
|
96
|
+
* readConstructionFiles, in the live wiring). Re-registering invalidates the
|
|
97
|
+
* compiled cache. */
|
|
98
|
+
export function setConstructionBanks(source) {
|
|
99
|
+
bankSource = source ?? null;
|
|
100
|
+
bankCache = null;
|
|
101
|
+
}
|
|
102
|
+
|
|
127
103
|
/** The cached, compiled construction bank (relations table + runnable
|
|
128
|
-
* templates),
|
|
129
|
-
* load/validate/compile chain degrades to an empty bank (the
|
|
130
|
-
* never fires) rather than crashing the pipeline that
|
|
131
|
-
|
|
132
|
-
|
|
104
|
+
* templates), compiled once per registration. Defensive: any failure
|
|
105
|
+
* anywhere in the load/validate/compile chain degrades to an empty bank (the
|
|
106
|
+
* strategy simply never fires) rather than crashing the pipeline that
|
|
107
|
+
* imports this module. */
|
|
108
|
+
export function constructionBank() {
|
|
109
|
+
if (bankCache !== null) return bankCache;
|
|
133
110
|
let bank;
|
|
134
111
|
try {
|
|
135
|
-
const
|
|
112
|
+
const raw = typeof bankSource === "function" ? bankSource() : bankSource;
|
|
113
|
+
const { relations, constructions } = raw || { relations: [], constructions: [] };
|
|
136
114
|
const agentNounTable = buildAgentNounTable(relations);
|
|
137
115
|
const templates = buildConstructionTemplates(constructions, agentNounTable);
|
|
138
116
|
bank = { agentNounTable, templates };
|
|
139
117
|
} catch {
|
|
140
118
|
bank = { agentNounTable: {}, templates: [] };
|
|
141
119
|
}
|
|
142
|
-
|
|
120
|
+
bankCache = bank;
|
|
143
121
|
return bank;
|
|
144
122
|
}
|
|
145
123
|
|
|
@@ -92,14 +92,20 @@ export function parseKeywordSpot(text, nlp = null) {
|
|
|
92
92
|
verbHit = findPhrase(lemmaWords, VERB_TO_KIND);
|
|
93
93
|
if (verbHit) canonWords = lemmaWords;
|
|
94
94
|
}
|
|
95
|
-
let fuzzyVerb =
|
|
95
|
+
let fuzzyVerb = null;
|
|
96
96
|
if (!verbHit) {
|
|
97
97
|
// tier 3: bounded-edit-distance rewrite toward verb/modifier keywords only
|
|
98
98
|
// ("impotr" -> "import"); ≥4-char words only — below that the bound covers
|
|
99
99
|
// half of English (and "and" is 1 edit from the "land in" constituent).
|
|
100
100
|
const fuzzyWords = lcWords.map((w) => (w.length >= 4 && eligibleForCanon(w) ? fuzzyVocabWord(w) || w : w));
|
|
101
101
|
verbHit = findPhrase(fuzzyWords, VERB_TO_KIND);
|
|
102
|
-
if (verbHit) {
|
|
102
|
+
if (verbHit) {
|
|
103
|
+
canonWords = fuzzyWords;
|
|
104
|
+
const offset = lcWords.slice(verbHit.start, verbHit.end)
|
|
105
|
+
.findIndex((w, i) => w !== fuzzyWords[verbHit.start + i]);
|
|
106
|
+
const at = verbHit.start + Math.max(offset, 0);
|
|
107
|
+
fuzzyVerb = { from: lcWords[at], to: fuzzyWords[at] };
|
|
108
|
+
}
|
|
103
109
|
}
|
|
104
110
|
if (!verbHit && lcWords.includes("by")) {
|
|
105
111
|
// A participle with no active verb entry still marks a passive when a passive
|
|
@@ -111,8 +117,10 @@ export function parseKeywordSpot(text, nlp = null) {
|
|
|
111
117
|
}
|
|
112
118
|
if (!verbHit) return null;
|
|
113
119
|
// A tier-3 verb is a REPAIR, not a reading — downstream consumers (the teach
|
|
114
|
-
// lane's canonical receipt
|
|
115
|
-
|
|
120
|
+
// lane's canonical receipt, and the chat surface's fuzzy-verb decline) need
|
|
121
|
+
// to know the difference AND which word was rewritten, so {from, to} rides
|
|
122
|
+
// the AST.
|
|
123
|
+
const stamp = (ast) => (fuzzyVerb ? { ...ast, fuzzyVerb } : ast);
|
|
116
124
|
// POS rescue (Node-side only): a relation word used as a NOUN in a "the
|
|
117
125
|
// <imports> of <term>" frame would otherwise misparse; only fires inside this
|
|
118
126
|
// exact det+NOUN+"of" shape, since the same word tags NOUN in genuine verb use too.
|
|
@@ -171,31 +179,28 @@ export function parseKeywordSpot(text, nlp = null) {
|
|
|
171
179
|
|
|
172
180
|
// Reversible passive ("PATIENT is VERBed BY AGENT"): a passive auxiliary plus a
|
|
173
181
|
// standalone agent-marking "by" (not already swallowed into a multi-word verb
|
|
174
|
-
// phrase)
|
|
175
|
-
//
|
|
182
|
+
// phrase) splits the sentence into a patient before "by" and an agent after it.
|
|
183
|
+
// How many of the two the sentence actually names picks the shape: both named
|
|
184
|
+
// is the yes/no ask, agent alone reads forward from the agent, patient alone
|
|
185
|
+
// reads reverse over the patient.
|
|
176
186
|
const byIdx = lcWords.indexOf("by");
|
|
177
187
|
const hasPassiveAux = lcWords.slice(0, verbHit.start).some((w) => PASSIVE_AUX.has(w));
|
|
178
188
|
if (byIdx >= 0 && !consumed.has(byIdx) && hasPassiveAux) {
|
|
179
|
-
const
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
nextAfterBy = lcWords[i]; break;
|
|
195
|
-
}
|
|
196
|
-
const agentNamed = nextAfterBy != null && !WH_WORDS.has(nextAfterBy) && !ENTITY_TO_TYPE[nextAfterBy];
|
|
197
|
-
return stamp({ shape: agentNamed ? "forward" : "reverse", entityType, modifier, kind, object });
|
|
198
|
-
}
|
|
189
|
+
const roleText = (from, to) => words
|
|
190
|
+
.slice(from, to)
|
|
191
|
+
.filter((_, j) => {
|
|
192
|
+
const i = from + j;
|
|
193
|
+
const w = lcWords[i];
|
|
194
|
+
return !consumed.has(i) && !STOPWORDS.has(w) && w !== "by" && !PASSIVE_AUX.has(w)
|
|
195
|
+
&& !WH_WORDS.has(w) && !PLACEHOLDER_SET.has(w);
|
|
196
|
+
})
|
|
197
|
+
.join(" ")
|
|
198
|
+
.trim();
|
|
199
|
+
const patient = roleText(0, byIdx);
|
|
200
|
+
const agent = roleText(byIdx + 1, words.length);
|
|
201
|
+
if (patient && agent) return stamp({ shape: "ask", entityType: null, modifier: "direct", kind, subject: agent, object: patient });
|
|
202
|
+
if (agent) return stamp({ shape: "forward", entityType, modifier, kind, object: agent });
|
|
203
|
+
if (patient) return stamp({ shape: "reverse", entityType, modifier, kind, object: patient });
|
|
199
204
|
}
|
|
200
205
|
|
|
201
206
|
if (beforeText && afterText) {
|
|
@@ -215,6 +220,23 @@ export function parseKeywordSpot(text, nlp = null) {
|
|
|
215
220
|
const entityText = canonWords.slice(entityHit.start, entityHit.end).join(" ");
|
|
216
221
|
if (entityText) return stamp({ shape: "reverse", entityType: null, modifier, kind, object: entityText });
|
|
217
222
|
}
|
|
223
|
+
// Bare passive ("was X touched", "is X imported"): a passive auxiliary before a
|
|
224
|
+
// participle, with no agent tail, makes X the PATIENT, so the active forward
|
|
225
|
+
// read below would answer a question nobody asked. The participle is looked up
|
|
226
|
+
// in lcWords rather than canonWords because the lemma tier rewrites it away
|
|
227
|
+
// ("imported" -> "import"); that same lookup is what leaves the present
|
|
228
|
+
// progressive ("what is X importing") on the active path, since a gerund is no
|
|
229
|
+
// participle. "touches" answers as a when-question, matching what "has X been
|
|
230
|
+
// touched" already says — one question must not get two answers.
|
|
231
|
+
if (beforeText && !afterText && hasPassiveAux && verbHit.end - verbHit.start === 1
|
|
232
|
+
&& PASSIVE_PARTICIPLE_TO_KIND[lcWords[verbHit.start]]) {
|
|
233
|
+
// A patient of more than one token is the wreckage of a clause no tier here
|
|
234
|
+
// parses. Resolving past it would cite a confident answer to a question
|
|
235
|
+
// nobody asked, so this declines and the sentence misses honestly.
|
|
236
|
+
if (beforeText.split(/\s+/).length > 1) return null;
|
|
237
|
+
if (kind === "touches") return stamp({ shape: "when", entityType: null, modifier: "direct", kind, object: beforeText });
|
|
238
|
+
return stamp({ shape: "reverse", entityType, modifier, kind, object: beforeText });
|
|
239
|
+
}
|
|
218
240
|
// forward keeps the spotted entityType (traverse()'s commit-as-subject grain
|
|
219
241
|
// selection); modifier stays hardcoded since no forward closure traversal exists.
|
|
220
242
|
if (beforeText) return stamp({ shape: "forward", entityType, modifier: "direct", kind, object: beforeText });
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
// memory/capability.mjs — negative facts, and capability read across
|
|
2
|
+
// rdfs:subClassOf with a specificity override. The Tweety problem: birds fly,
|
|
3
|
+
// penguins are birds, penguins do not fly.
|
|
4
|
+
//
|
|
5
|
+
// This is DEFEASIBLE inheritance, and it is deliberately NOT OWL. OWL's
|
|
6
|
+
// cls-svf1/scm-svf1 propagate owl:someValuesFrom restrictions; mgx:capableOf is
|
|
7
|
+
// a flat property assertion on a punned class, and OWL does not carry that down
|
|
8
|
+
// a subclass edge at all. Nothing here should be routed through syllogise.mjs's
|
|
9
|
+
// rule engine, because the conclusion is non-monotonic: a later, more specific
|
|
10
|
+
// fact retracts it. The chase reads at query time and writes nothing, so an
|
|
11
|
+
// override never has to un-say a materialised entailment.
|
|
12
|
+
//
|
|
13
|
+
// Polarity lives IN the predicate, under its own prefix. It cannot live in a
|
|
14
|
+
// separate property: factIdForTriple content-addresses (subject, predicate,
|
|
15
|
+
// object), so a positive and a negative sharing one predicate would hash to the
|
|
16
|
+
// same id, merge into one Fact, and union their mgx:statedBy edges — the source
|
|
17
|
+
// indexing that lets two sources disagree without either being deleted.
|
|
18
|
+
//
|
|
19
|
+
// Pure and import-free of core.mjs, exactly like trust.mjs beside it.
|
|
20
|
+
|
|
21
|
+
import { findIsaChain, SUBCLASS_PREDICATE, TYPE_PREDICATE } from "../syllogise.mjs";
|
|
22
|
+
|
|
23
|
+
/** The negative-polarity CURIE prefix. A separate prefix, never an
|
|
24
|
+
* "mgx:not-<lemma>" mint: chat.mjs's predicatePhrase reads "mgx:not-fly" as
|
|
25
|
+
* lemma "not" plus tail "fly" and renders "nots fly", and its preposition fold
|
|
26
|
+
* is guarded on /^mgx:[a-z]+$/, which a hyphenated mint fails — so
|
|
27
|
+
* "a penguin cannot rest on water" would strand "on water" inside the object
|
|
28
|
+
* where no read-back can match it. */
|
|
29
|
+
export const NEG_PREDICATE_PREFIX = "mgxneg:";
|
|
30
|
+
const POSITIVE_PREDICATE_PREFIX = "mgx:";
|
|
31
|
+
|
|
32
|
+
/** Swap an mgx: predicate onto its negative twin. Applied AFTER the preposition
|
|
33
|
+
* fold, so mgx:rest-on becomes mgxneg:rest-on and the object keeps nothing
|
|
34
|
+
* meaning-bearing inside it. */
|
|
35
|
+
export function negatedPredicate(predicate) {
|
|
36
|
+
const p = String(predicate || "");
|
|
37
|
+
if (!p.startsWith(POSITIVE_PREDICATE_PREFIX)) return p;
|
|
38
|
+
return NEG_PREDICATE_PREFIX + p.slice(POSITIVE_PREDICATE_PREFIX.length);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** The inverse: mgxneg:capableOf -> mgx:capableOf, or null for any predicate
|
|
42
|
+
* that isn't negative at all. */
|
|
43
|
+
export function positivePredicate(predicate) {
|
|
44
|
+
const p = String(predicate || "");
|
|
45
|
+
if (!p.startsWith(NEG_PREDICATE_PREFIX)) return null;
|
|
46
|
+
return POSITIVE_PREDICATE_PREFIX + p.slice(NEG_PREDICATE_PREFIX.length);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export const isNegatedPredicate = (predicate) => String(predicate || "").startsWith(NEG_PREDICATE_PREFIX);
|
|
50
|
+
|
|
51
|
+
export const CAPABLE_OF_PREDICATE = "mgx:capableOf";
|
|
52
|
+
export const NEG_CAPABLE_OF_PREDICATE = negatedPredicate(CAPABLE_OF_PREDICATE);
|
|
53
|
+
|
|
54
|
+
/** How many rows a base-rate or extension report lists before it stops, and the
|
|
55
|
+
* order it lists them in. One constant each, in one place, so the verbosity of
|
|
56
|
+
* every case-4 answer is tuned by editing two lines. */
|
|
57
|
+
export const CAPABILITY_REPORT_CAP = 6;
|
|
58
|
+
export const byTrustThenName = (a, b) => (b.trust || 0) - (a.trust || 0) || String(a.subject).localeCompare(String(b.subject));
|
|
59
|
+
|
|
60
|
+
const asSet = (v) => (v instanceof Set ? v : new Set(Array.isArray(v) ? v : [v]));
|
|
61
|
+
|
|
62
|
+
const isaEdgesOf = (facts) => ({
|
|
63
|
+
typeEdges: facts.filter((f) => f.predicate === TYPE_PREDICATE).map((f) => [f.subject, f.object]),
|
|
64
|
+
subClassEdges: facts.filter((f) => f.predicate === SUBCLASS_PREDICATE).map((f) => [f.subject, f.object]),
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
/** The shortest isa chain from any spelling of `subjects` up to `target`, or
|
|
68
|
+
* null. The chase is corpus-INCLUSIVE on purpose, and that is a considered
|
|
69
|
+
* difference from the is-a ladder's own taught-only chase. The ladder excludes
|
|
70
|
+
* corpus edges because a coincidental ConceptNet chain answering "is X a Y"
|
|
71
|
+
* would be fabrication. Here the premise being chased to — "bird can fly" — is
|
|
72
|
+
* itself corpus data on a fresh install, so a taught-only chase would find
|
|
73
|
+
* nothing to inherit and the whole feature would never fire. */
|
|
74
|
+
function shortestChainTo(subjects, target, typeEdges, subClassEdges, maxHops) {
|
|
75
|
+
let best = null;
|
|
76
|
+
for (const s of subjects) {
|
|
77
|
+
if (s === target) return [];
|
|
78
|
+
const chain = findIsaChain(s, new Set([target]), typeEdges, subClassEdges, { maxHops });
|
|
79
|
+
if (chain && (!best || chain.length < best.length)) best = chain;
|
|
80
|
+
}
|
|
81
|
+
return best;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Does `subject` have `object` as a capability? Pure over already-fetched Fact
|
|
86
|
+
* rows ({subject, predicate, object, provenance, trust}).
|
|
87
|
+
*
|
|
88
|
+
* Returns { verdict, hops, positive, negative, chain, overrides, baseRate }:
|
|
89
|
+
* verdict "yes" — the winning rank is positive only
|
|
90
|
+
* verdict "no" — the winning rank is negative only
|
|
91
|
+
* verdict "both" — the winning rank holds both polarities; report the sources
|
|
92
|
+
* and pick NOTHING
|
|
93
|
+
* verdict "none" — nothing is known; `baseRate` carries the class split
|
|
94
|
+
* `hops` is 0 for a fact directly about the subject, else the chain length.
|
|
95
|
+
* `overrides` names the more general default a direct answer beat, so case 2
|
|
96
|
+
* can say what it is overriding instead of silently winning.
|
|
97
|
+
*
|
|
98
|
+
* SPECIFICITY IS CHAIN LENGTH, AND IT OUTRANKS TRUST UNCONDITIONALLY. The
|
|
99
|
+
* candidates are sorted by hop count first and trust only second, so trust can
|
|
100
|
+
* never reorder across ranks — it only ever breaks a tie inside one. Trust
|
|
101
|
+
* looks like it solves this for free (SOURCE_PRIOR puts entailed 0.3 under
|
|
102
|
+
* teach 0.95, so a taught negative already outranks a derived positive) but
|
|
103
|
+
* that is luck: flip the sources — the corpus says penguins cannot fly, a
|
|
104
|
+
* visitor taught that birds can — and trust alone answers that penguins fly.
|
|
105
|
+
*/
|
|
106
|
+
export function resolveCapabilityPolarity(subject, object, facts, { maxHops = 3 } = {}) {
|
|
107
|
+
const subjects = asSet(subject);
|
|
108
|
+
const objects = asSet(object);
|
|
109
|
+
const rows = Array.isArray(facts) ? facts : [];
|
|
110
|
+
const { typeEdges, subClassEdges } = isaEdgesOf(rows);
|
|
111
|
+
|
|
112
|
+
const carriers = rows.filter(
|
|
113
|
+
(f) => (f.predicate === CAPABLE_OF_PREDICATE || f.predicate === NEG_CAPABLE_OF_PREDICATE) && objects.has(f.object),
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
const candidates = [];
|
|
117
|
+
for (const fact of carriers) {
|
|
118
|
+
const polarity = fact.predicate === NEG_CAPABLE_OF_PREDICATE ? "negative" : "positive";
|
|
119
|
+
if (subjects.has(fact.subject)) {
|
|
120
|
+
candidates.push({ fact, polarity, hops: 0, chain: null });
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
const chain = shortestChainTo(subjects, fact.subject, typeEdges, subClassEdges, maxHops);
|
|
124
|
+
if (chain && chain.length) candidates.push({ fact, polarity, hops: chain.length, chain });
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (!candidates.length) {
|
|
128
|
+
return {
|
|
129
|
+
verdict: "none", hops: 0, positive: [], negative: [], chain: null, overrides: null,
|
|
130
|
+
baseRate: capabilityBaseRate(subjects, objects, rows, { maxHops }),
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// hop count first, trust only within a rank — the whole point of the design
|
|
135
|
+
candidates.sort((a, b) => a.hops - b.hops || (b.fact.trust || 0) - (a.fact.trust || 0));
|
|
136
|
+
const hops = candidates[0].hops;
|
|
137
|
+
const winning = candidates.filter((c) => c.hops === hops);
|
|
138
|
+
const positive = winning.filter((c) => c.polarity === "positive").map((c) => c.fact);
|
|
139
|
+
const negative = winning.filter((c) => c.polarity === "negative").map((c) => c.fact);
|
|
140
|
+
|
|
141
|
+
// the beaten default: the closest OPPOSITE-polarity claim further up the
|
|
142
|
+
// chain, so a direct answer can name what it overrides
|
|
143
|
+
const answered = negative.length && !positive.length ? "negative" : "positive";
|
|
144
|
+
const beaten = candidates.find((c) => c.hops > hops && c.polarity !== answered) || null;
|
|
145
|
+
|
|
146
|
+
return {
|
|
147
|
+
verdict: positive.length && negative.length ? "both" : (negative.length ? "no" : "yes"),
|
|
148
|
+
hops,
|
|
149
|
+
positive,
|
|
150
|
+
negative,
|
|
151
|
+
chain: candidates[0].chain,
|
|
152
|
+
overrides: beaten ? { fact: beaten.fact, chain: beaten.chain } : null,
|
|
153
|
+
baseRate: null,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Nothing is known about the subject, so report the CLASS it belongs to and how
|
|
159
|
+
* that class's other kinds split — never a yes or a no.
|
|
160
|
+
*
|
|
161
|
+
* The subject is excluded from its own base rate: you do not cite ostrich as
|
|
162
|
+
* evidence about ostrich. The denominator is the subclass count, and the split
|
|
163
|
+
* is three-way (positive, negative, unknown) so it accounts for every kind it
|
|
164
|
+
* counted. Say 5 and split only 4 and the arithmetic lies about what you know.
|
|
165
|
+
*
|
|
166
|
+
* The sample is never generalised to the population. "of the 4 kinds of bird I
|
|
167
|
+
* know" is a count this can prove; "most birds fly" is a claim about birds it
|
|
168
|
+
* has never seen.
|
|
169
|
+
*
|
|
170
|
+
* Returns null when the subject belongs to no known class.
|
|
171
|
+
*/
|
|
172
|
+
export function capabilityBaseRate(subject, object, facts, { maxHops = 3 } = {}) {
|
|
173
|
+
const subjects = asSet(subject);
|
|
174
|
+
const objects = asSet(object);
|
|
175
|
+
const rows = Array.isArray(facts) ? facts : [];
|
|
176
|
+
const isaRows = rows.filter((f) => f.predicate === SUBCLASS_PREDICATE || f.predicate === TYPE_PREDICATE);
|
|
177
|
+
|
|
178
|
+
const parents = isaRows.filter((f) => subjects.has(f.subject)).map((f) => f.object);
|
|
179
|
+
if (!parents.length) return null;
|
|
180
|
+
const klass = parents[0];
|
|
181
|
+
|
|
182
|
+
const siblings = [...new Set(
|
|
183
|
+
isaRows.filter((f) => f.object === klass && !subjects.has(f.subject)).map((f) => f.subject),
|
|
184
|
+
)];
|
|
185
|
+
|
|
186
|
+
const capabilityOf = (name) => {
|
|
187
|
+
const hit = rows.find(
|
|
188
|
+
(f) => f.subject === name && objects.has(f.object)
|
|
189
|
+
&& (f.predicate === CAPABLE_OF_PREDICATE || f.predicate === NEG_CAPABLE_OF_PREDICATE),
|
|
190
|
+
);
|
|
191
|
+
if (!hit) return { name, polarity: "unknown", fact: null };
|
|
192
|
+
return { name, polarity: hit.predicate === NEG_CAPABLE_OF_PREDICATE ? "negative" : "positive", fact: hit };
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
const split = siblings.map(capabilityOf);
|
|
196
|
+
const { typeEdges, subClassEdges } = isaEdgesOf(rows);
|
|
197
|
+
return {
|
|
198
|
+
klass,
|
|
199
|
+
kinds: siblings.length,
|
|
200
|
+
positive: split.filter((s) => s.polarity === "positive"),
|
|
201
|
+
negative: split.filter((s) => s.polarity === "negative"),
|
|
202
|
+
unknown: split.filter((s) => s.polarity === "unknown"),
|
|
203
|
+
chain: shortestChainTo(subjects, klass, typeEdges, subClassEdges, maxHops),
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Every subject the memory says CAN do `object` — the predicate's extension.
|
|
209
|
+
* Backs both the "what can fly" listing and the case-4 pivot ("I don't know if
|
|
210
|
+
* a penguin can fly, but I do know 3 things that can fly").
|
|
211
|
+
*
|
|
212
|
+
* A subject only lists if the RESOLVER says yes, not merely if a positive row
|
|
213
|
+
* exists. A subject the store explicitly says cannot do this is not an answer
|
|
214
|
+
* here, even when a corpus row also says it can — otherwise this listing and
|
|
215
|
+
* "can a penguin fly" would contradict each other over the same facts.
|
|
216
|
+
*
|
|
217
|
+
* `exclude` drops the subject and the class already reported on: pivoting to an
|
|
218
|
+
* extension whose only member IS the class being asked about is circular, not
|
|
219
|
+
* informative.
|
|
220
|
+
*/
|
|
221
|
+
export function capabilityExtension(object, facts, { exclude = [], maxHops = 3 } = {}) {
|
|
222
|
+
const objects = asSet(object);
|
|
223
|
+
const skip = asSet(exclude);
|
|
224
|
+
const rows = Array.isArray(facts) ? facts : [];
|
|
225
|
+
const seen = new Set();
|
|
226
|
+
const out = [];
|
|
227
|
+
for (const f of rows) {
|
|
228
|
+
if (f.predicate !== CAPABLE_OF_PREDICATE || !objects.has(f.object)) continue;
|
|
229
|
+
if (skip.has(f.subject) || seen.has(f.subject)) continue;
|
|
230
|
+
seen.add(f.subject);
|
|
231
|
+
if (resolveCapabilityPolarity(new Set([f.subject]), objects, rows, { maxHops }).verdict !== "yes") continue;
|
|
232
|
+
out.push(f);
|
|
233
|
+
}
|
|
234
|
+
return out.sort(byTrustThenName);
|
|
235
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// memory/fold.mjs — the pure cleaning rules a session log passes through on its
|
|
2
|
+
// way into the text-block corpus: pair each turn with its answer prose, drop
|
|
3
|
+
// slash-commands, filler and misses, and render what survives as corpus text.
|
|
4
|
+
//
|
|
5
|
+
// Pure by construction — the reading, writing and speculative pass live in
|
|
6
|
+
// services/fold.mjs, which calls cleanSessionText below.
|
|
7
|
+
|
|
8
|
+
import { turnKey } from "./session-turns.mjs";
|
|
9
|
+
|
|
10
|
+
/** Conversational filler a transcript doesn't need: greetings, thanks, byes,
|
|
11
|
+
* bare acknowledgements. Catches the one-liners chat.mjs did NOT flag
|
|
12
|
+
* `conversational` (e.g. typed straight at an older tmct). */
|
|
13
|
+
const FILLER_RE = new RegExp(
|
|
14
|
+
"^(?:(?:hi|hiya|hello|hey|yo|howdy)(?:\\s+there)?|good\\s*(?:morning|afternoon|evening)|" +
|
|
15
|
+
"thanks(?:\\s+(?:a\\s+lot|so\\s+much))?|thank\\s*you|thx|ty|cheers|" +
|
|
16
|
+
"ok(?:ay)?|cool|nice|great|sure|yes|no|yep|nope|" +
|
|
17
|
+
"bye(?:\\s+bye)?|goodbye|see\\s*(?:ya|you)|later)[\\s!.?,]*$",
|
|
18
|
+
"i",
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
const squash = (s) => String(s ?? "").replace(/\s+/g, " ").trim();
|
|
22
|
+
|
|
23
|
+
/** The grammar wall's opening line. Source of truth: chat.mjs's WALL_MISS_RE —
|
|
24
|
+
* repeated here as a one-line local anchor so the memory layer stays decoupled
|
|
25
|
+
* from the chat surface. Checked on squashed (single-line) answer text. */
|
|
26
|
+
const WALL_ANSWER_RE = /^couldn't parse this as a graph question\. Try:/;
|
|
27
|
+
/** chat.mjs's recall frame preamble. An answer carrying it is a REPLAY of an
|
|
28
|
+
* earlier session (possibly with the wall appended below it), never fresh
|
|
29
|
+
* content — folding it would nest recalls-of-recalls. */
|
|
30
|
+
const RECALL_PREAMBLE_RE = /you asked about this before/;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Clean one parsed session record into corpus text (pure). `answers` is
|
|
34
|
+
* parseSessionLog()'s Map (may be empty — a vanished transcript degrades to
|
|
35
|
+
* question-only lines, honestly, rather than losing the session).
|
|
36
|
+
* Returns "" when nothing survives.
|
|
37
|
+
*/
|
|
38
|
+
export function cleanSessionText(record, answers = new Map()) {
|
|
39
|
+
const parts = [];
|
|
40
|
+
for (const t of record?.turns || []) {
|
|
41
|
+
const query = squash(t?.query);
|
|
42
|
+
if (!query) continue;
|
|
43
|
+
if (t.command || query.startsWith("/")) continue; // slash-commands + their outputs
|
|
44
|
+
if (t.conversational || FILLER_RE.test(query)) continue; // greetings/thanks/bye
|
|
45
|
+
if (t.miss) continue; // honest-miss boilerplate is not corpus content
|
|
46
|
+
const answer = squash(answers.get(turnKey(t.ts, t.query)));
|
|
47
|
+
// Belt-and-braces recall hygiene: even when a turn was NOT recorded as a
|
|
48
|
+
// miss, a grammar-wall answer or a recall replay never re-enters the
|
|
49
|
+
// corpus — re-folding also cleans stores poisoned by older builds.
|
|
50
|
+
if (WALL_ANSWER_RE.test(answer) || RECALL_PREAMBLE_RE.test(answer)) continue;
|
|
51
|
+
parts.push(answer ? `Q: ${query}\nA: ${answer}` : `Q: ${query}`);
|
|
52
|
+
}
|
|
53
|
+
return parts.join("\n");
|
|
54
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// The pure key both session artifacts agree on: the transcript parser writes it
|
|
2
|
+
// and the fold's cleaner reads it, so it lives where each can legally import it.
|
|
3
|
+
|
|
4
|
+
/** Key a transcript answer by its turn: ts + query (ts alone can collide when
|
|
5
|
+
* two instant turns land in the same millisecond). The separator is NUL so it
|
|
6
|
+
* cannot occur in either half. */
|
|
7
|
+
export const turnKey = (ts, query) => `${ts}\0${query}`;
|