Skip to content

engine 0.4.44: W7 NLQ typed-plan compiler — question → sealed, provenance-carrying plan - #31

Merged
mdheller merged 1 commit into
mainfrom
feat/nlq-compiler
Jul 29, 2026
Merged

engine 0.4.44: W7 NLQ typed-plan compiler — question → sealed, provenance-carrying plan#31
mdheller merged 1 commit into
mainfrom
feat/nlq-compiler

Conversation

@mdheller

Copy link
Copy Markdown
Member

The W7 NLQ typed-plan compiler (ts/src/nlq.ts): a natural-language question becomes a sealed, provenance-carrying plan over a registry of typed Semantic Actions — without executing anything.

question
  → deterministic tokenization (spans preserved)
  → ontology-annotated tokens (pluggable annotators)
  → side-effect-free restricted search over the typed action registry
  → sense-metric-ranked variant plans
  → winner sealed against the graph snapshot

The contract is vendored and tamper-evident

SemanticAction.json from SourceOS-Linux/sourceos-spec (PR #210, ee7e43a4) lands byte-identical at ontology/semantic-action/ and is embedded into ts/src/semantic-action-data.ts by scripts/gen-semantic-action.mjs — the same way KKO ships, because tsup bundles to one file per format and there is no runtime asset to read.

Its sha256 fd01c834… is asserted at import: a drifted or hand-edited contract fails LOUDLY at load, never silently at plan time. The digest is sealed into every receipt, so a plan names the exact contract it was compiled against. ActionRegistry.declare validates against the schema's own bytes, plus the two invariants the contract documents but JSON Schema cannot express (input names unique within an action; every constraint subject resolving to a declared input or output).

Typed unification, not prompting

An annotation or output of type T binds a slot declared U exactly when isA(T, U) in the AtomSpace TypeLattice — so :ContactList satisfies a :List slot by subsumption, and nothing satisfies a slot it is not under. Cardinality is enforced (a set never fills a single-valued slot) and every binding records a SubsumptionWitness. instanceOf is necessarily approximated by the same static check at search time, since no values exist yet — documented in code, not silently conflated.

Three normative rules

  1. Purity. Search never mutates the store — store.version() is identical before and after, proved by test.
  2. Effects are proposed, never taken. An effect-request action is a leaf of the dataflow: never offered as an input provider (consuming its output is what would imply running it), so it only ever appears as the plan root carrying an EffectRequestProposal{status:'proposed'}. Its own arguments may still be computed by pure actions — computing an argument is not taking the effect. The MPCC lifecycle (EffectRequest → EffectDecision → EffectRecord) is never bypassed.
  3. Mentions are not values. A concept in the text is evidence a type is in play, not that a value is in hand, so non-literal slots must be filled by an action or a registry default. This is what forces "contact lists in my org" to decompose into GetContactLists ← GetOrganization instead of collapsing into one node with an imaginary argument.

The creativity penalty IS the admissibility discount

coverage · groundedness · similarity, composite with explicit weights carried in the result (0.5 / 0.3 / 0.2).

Every plan node not grounded in a token span or a registry default is an invention, so it is routed through admitClaim (merged in 0.4.43) as a model-generated claim and its weight becomes the admissibility discount (×0.5) — one mechanism, not a parallel heuristic. Under admissibility: { allowOpinion: false } an invented node is inadmissible and the whole variant is dropped, because an excluded claim carries weight 0 and "must not enter the reasoning context at all". A question the registry cannot serve then yields no variants rather than a guess.

Sealed exactly like enrich / explore: sha256 over ranked output + {seq,nodes,edges} snapshot + pinned contract digest, where seq is the store's monotonic logical clock. Same inputs against the same graph state ⇒ byte-identical seal.

Evidence (EBA-style fixture)

Toy CRM registry — GetContactLists / GetOrganization / GetMailingsByList / FindListByName / NumberOf / Count (polymorphic over subClassOf :List) / SendMailing (effect-request).

question winner composite
"show me all contact lists in my org" GetContactLists ← GetOrganization 1.000
"how many mailings on list Acme" NumberOf → GetMailingsByList → FindListByName("Acme") 1.000
"send mailing to list Acme" SendMailing[effect proposed] ← FindListByName("Acme") 1.000

The polymorphic Count(:List) ← FindListByName(:ContactList) variant is produced (proving subsumption binding where the direct type differs) and is correctly ranked below the winner on coverage — it never consumes "mailings".

Gates

  • Full suite 371/371 (was 347) — +24 new tests, none broken.
  • npm run typecheck clean · npm run check:dist exit 0 · dist rebuilt and smoke-tested from the bundle.
  • Zero upstream drift at push.

Docs: docs/specs/15_NLQ_Typed_Plan_Compiler_v0_1.md + spec-index entry.

Reviewer note — one interpretation call

The brief said effect-request actions may appear "ONLY as plan leaves". Taken as no children, such an action could never receive a computed argument, which makes the feature vacuous. I implemented the dataflow reading — its output is never consumed, so it can only be the plan root — which is the invariant that actually protects search-time purity, while its arguments may be computed by pure actions. Flagging it explicitly in case the stricter reading was intended.

…ance-carrying plan

nlq: a natural-language question becomes a SEALED plan over a registry of
typed Semantic Actions, without executing anything.

  question → deterministic tokenization (spans preserved) → ontology-annotated
  tokens → side-effect-free restricted search over the typed action registry →
  sense-metric-ranked variants → winner sealed against the graph snapshot

- Contract: SemanticAction.json vendored from sourceos-spec (PR #210,
  ee7e43a4) at ontology/semantic-action/, embedded by
  scripts/gen-semantic-action.mjs the way KKO ships (tsup bundles to one file
  per format, so there is no runtime asset to read). Its sha256 fd01c834… is
  asserted AT IMPORT — a drifted contract fails at load, never silently at
  plan time — and is sealed into every receipt. ActionRegistry.declare
  validates against the schema's own bytes plus the two invariants the
  contract documents but JSON Schema cannot express (unique input names,
  constraint subjects resolving to a declared input or 'output').
- Typed unification: an annotation/output of type T binds a slot declared U
  exactly when isA(T,U) in the AtomSpace TypeLattice, so :ContactList
  satisfies a :List slot by subsumption. Cardinality enforced; every binding
  records a SubsumptionWitness. instanceOf is necessarily approximated by the
  same static check at search time (no values exist yet) — documented, not
  silently conflated.
- Three normative rules. (1) PURITY: search never mutates the store —
  store.version() identical before and after, proved by test. (2) EFFECTS ARE
  PROPOSED, NEVER TAKEN: an effect-request action is a leaf of the DATAFLOW —
  never offered as an input provider, so it only ever appears as the plan root
  carrying an EffectRequestProposal{status:'proposed'}; its own arguments may
  still be computed by pure actions, since computing an argument is not taking
  the effect. (3) MENTIONS ARE NOT VALUES: non-literal slots must be filled by
  an action or a registry default, which is what forces "contact lists in my
  org" to DECOMPOSE into GetContactLists ← GetOrganization instead of
  collapsing into one node with an imaginary argument.
- Annotation: dependency-free Unicode tokenizer preserving exact char spans,
  plus a pluggable (tokens, ctx) => TokenAnnotation[] interface. Two built-ins
  — lexicon (registry typeRefs + action names + curated domain terms; longest
  match wins, curated beats URI-derived) and KKO-semantic (reuses semantic.ts
  EmbedFn + cosineSim, drawing concepts from the registry and the KkoClass
  nodes loaded in the store).
- Sense metric: coverage · groundedness · similarity, composite with explicit
  weights in the result (0.5/0.3/0.2). Every plan node NOT grounded in a token
  span or a registry default is an invention, so it is routed through
  admitClaim as a 'model-generated' claim and its weight BECOMES the
  admissibility discount (×0.5) — the creativity penalty IS the opinion
  discount, unified rather than parallel. Under allowOpinion:false an invented
  node is inadmissible and the whole variant is dropped, so an unservable
  question yields no variants rather than a guess.
- Sealed exactly like enrich/explore: sha256 over ranked output + {seq,nodes,
  edges} snapshot + pinned contract digest; seq is the store's logical clock.
  Same inputs against the same graph state ⇒ byte-identical seal.

Fixture (EBA style): toy CRM registry — GetContactLists / GetOrganization /
GetMailingsByList / FindListByName / NumberOf / Count (polymorphic over
subClassOf :List) / SendMailing (effect-request). "show me all contact lists in
my org" → GetContactLists ← GetOrganization at composite 1.0; "how many
mailings on list Acme" → NumberOf → GetMailingsByList → FindListByName("Acme")
at composite 1.0, with the polymorphic Count(:List) ← FindListByName
(:ContactList) variant produced but correctly ranked below on coverage.

24 tests. Full suite 371/371 (was 347). Rebuilt dist.
Docs: docs/specs/15_NLQ_Typed_Plan_Compiler_v0_1.md + index entry.
@mdheller
mdheller merged commit 17c30c7 into main Jul 29, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant