From f2719eca1dae476f6b94c2ab9b66bab4861638b2 Mon Sep 17 00:00:00 2001 From: Erin Spencer Date: Tue, 4 Aug 2026 01:08:19 -0700 Subject: [PATCH 1/2] Add NGSL lexical floor and layered snapshots --- docs/NGSL_LEXICAL_FLOOR_V1.md | 93 + pyproject.toml | 2 +- src/ucns/data/ngsl_1_2_source.json | 32 + src/ucns/data/ngsl_1_2_words.txt | 2809 ++++++++++++++++++++++++++++ src/ucns/lexical_floor.py | 598 ++++++ tests/test_lexical_floor.py | 175 ++ 6 files changed, 3708 insertions(+), 1 deletion(-) create mode 100644 docs/NGSL_LEXICAL_FLOOR_V1.md create mode 100644 src/ucns/data/ngsl_1_2_source.json create mode 100644 src/ucns/data/ngsl_1_2_words.txt create mode 100644 src/ucns/lexical_floor.py create mode 100644 tests/test_lexical_floor.py diff --git a/docs/NGSL_LEXICAL_FLOOR_V1.md b/docs/NGSL_LEXICAL_FLOOR_V1.md new file mode 100644 index 00000000..e8647c6a --- /dev/null +++ b/docs/NGSL_LEXICAL_FLOOR_V1.md @@ -0,0 +1,93 @@ +# NGSL 1.2 lexical floor v0.1 + +**Authority:** Erin Spencer +**Recorded:** 2026-08-04 +**Status:** executable first assignment; source-admitted candidate +**Selection effect:** none + +This slice admits one word-only collection of 2,809 general-English spellings, +applies the existing UCNS glyph canon, creates one word gonol per exact glyph +sequence, and opens a character-derived projection potential. Affixiation and +compounding begin as orthographic candidate layers. Definitions attach later as +plural context-sourced senses. + +## Decisions + +1. **Source order has no semantic purpose.** Frequency rank and source row order + are excluded from word-gonol identity. The text file uses deterministic + Unicode casefold ordering with an exact code-point tie-break only so builds + and snapshots reproduce byte-for-byte. + +2. **One spelling, one word gonol.** Exact duplicate spellings fail admission. + Each retained spelling has one exact ordered glyph tuple and one deterministic + gonol identity. Definitions never create another copy of the word. + +3. **Glyph law is inherited, not rebuilt.** Each Unicode scalar occurrence uses + the carrier assignment already implemented by `src/ucns/edcm.py`. No + normalization, case folding, glyph replacement, or source-order rank enters + identity. + +4. **The hyperspace is initially potential.** It can project exact character + relationships between any two retained word gonols: shared glyphs, common + prefix and suffix lengths, containment, and edit distance. This is not yet + the deep-recursion hyperdimensional embedding and does not imply semantic or + morphological relation. + +5. **Metadata is append-only and snapshotted.** The layer sequence is: + + 1. word-only source; + 2. glyph definitions; + 3. unique word gonols; + 4. character-relationship hyperspace potential; + 5. affixiation candidates; + 6. compound candidates; + 7. context-derived definitions. + + `snapshot_layers()` emits a parent-linked digest boundary after every layer. + Later metadata cannot rewrite an earlier snapshot. + +## Affixiation + +An affixiation candidate exists when one retained word can be produced by adding +a nonempty prefix or suffix to another retained word. The result is +`orthographic-candidate`, not attested morphology. It does not establish +allomorphy, historical derivation, morpheme identity, or meaning. + +## Compounding + +A compound candidate exists when one retained word can be divided at an exact +glyph boundary into two other retained words. It is likewise +`orthographic-candidate` until separately attested. + +## Definitions + +Definitions are keyed to the existing word-gonol identity. Each sense must retain +a context identity and source identity. Many senses may attach to one word gonol; +the source word itself remains singular. + +## Files + +- `src/ucns/data/ngsl_1_2_words.txt` — word strings only. +- `src/ucns/data/ngsl_1_2_source.json` — separate provenance and admission policy. +- `src/ucns/lexical_floor.py` — gonols, projection potential, layers, and snapshots. +- `tests/test_lexical_floor.py` — collection, identity, candidate, definition, and + snapshot boundaries. + +## Nonclaims + +This slice does not provide: + +- a canonical linguistic affix inventory; +- adjudicated compound-word status; +- a definitions corpus; +- a semantic metric; +- geometric coordinates for lexical relations; +- deep-recursion hyperdimensional embedding; or +- EDCM measurement activation. + +## hmmm + +The 2,809 spellings are now an executable candidate lexical floor. Independent +reconciliation against an official NGSL 1.2 download, attested morphology, +context-corpus custody, and the law that embeds these retained relationships into +UCNS deep recursion remain living boundaries. diff --git a/pyproject.toml b/pyproject.toml index 51faf439..7e025fce 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,7 +28,7 @@ package-dir = {"" = "src"} where = ["src"] [tool.setuptools.package-data] -ucns = ["option_registry.json"] +ucns = ["option_registry.json", "data/*.txt", "data/*.json"] [tool.pytest.ini_options] testpaths = ["tests"] diff --git a/src/ucns/data/ngsl_1_2_source.json b/src/ucns/data/ngsl_1_2_source.json new file mode 100644 index 00000000..511ca15d --- /dev/null +++ b/src/ucns/data/ngsl_1_2_source.json @@ -0,0 +1,32 @@ +{ + "schema_id": "ucns.lexical-source/0.1.0", + "collection_id": "ngsl-1.2-general-english-2809", + "title": "New General Service List 1.2 — word-only lexical floor", + "creators": [ + "Charles Browne", + "Brent Culligan", + "Joseph Phillips" + ], + "license": "CC BY-SA 4.0", + "official_source": "https://www.newgeneralservicelist.com/new-general-service-list", + "word_count": 2809, + "word_file": "ngsl_1_2_words.txt", + "acquisition": { + "mirror_repository": "uigywnkiub/english-of-avail", + "mirror_commit": "05f9e92d570feaa1f6fd4d3145151b49afab93d5", + "mirror_path": "dictionary/NGSL_1.2/NGSL_1.2_alphabetized_description.txt", + "mirror_git_blob": "4a89f46ae6461d121f9bcc30a07e23af04cf40a5", + "target_git_blob": "81b1ac81086e3f7a2963e5d07a0a2ad9291887bb" + }, + "identity_policy": { + "word_identity": "exact ordered Unicode scalar sequence", + "normalization": "none", + "case_folding": "none", + "duplicate_policy": "reject exact duplicate spellings", + "source_rank_retained": false, + "frequency_retained": false, + "serialization_order": "Unicode casefold key, then exact code-point tuple; ordering has no semantic standing" + }, + "standing": "source-admitted candidate", + "hmmm": "Reconcile the exact packaged spellings against an independently downloaded official NGSL 1.2 file and freeze that official file checksum before canonical promotion." +} diff --git a/src/ucns/data/ngsl_1_2_words.txt b/src/ucns/data/ngsl_1_2_words.txt new file mode 100644 index 00000000..81b1ac81 --- /dev/null +++ b/src/ucns/data/ngsl_1_2_words.txt @@ -0,0 +1,2809 @@ +a +abandon +ability +able +abortion +about +above +abroad +absence +absolute +absolutely +abstract +abuse +academic +accept +acceptable +access +accident +accommodation +accompany +accomplish +accord +account +accurate +accuse +achieve +achievement +acknowledge +acquire +acquisition +across +act +action +active +activity +actor +actual +actually +ad +adapt +add +addition +additional +address +adequate +adjust +adjustment +administration +admire +admit +adopt +adult +advance +advantage +adventure +advertise +advertisement +advice +advise +adviser +advocate +affair +affect +afford +afraid +after +afternoon +again +against +age +agency +agenda +agent +aggressive +ago +agree +agreement +agricultural +ahead +aid +aim +air +aircraft +airline +alarm +album +alcohol +alive +all +allege +allow +ally +almost +alone +along +alongside +already +alright +also +alter +alternative +although +altogether +always +amaze +amendment +among +amount +analysis +analyst +analyze +ancient +and +anger +angle +angry +animal +announce +announcement +annual +another +answer +anticipate +anxiety +anxious +any +anybody +anymore +anyone +anything +anyway +anywhere +apart +apartment +apologize +apparent +apparently +appeal +appear +appearance +application +apply +appoint +appointment +appreciate +approach +appropriate +approval +approve +approximately +architecture +area +argue +argument +arise +arm +army +around +arrange +arrangement +arrest +arrival +arrive +art +article +artist +as +ashamed +aside +ask +assess +assessment +asset +assign +assist +assistance +assistant +associate +association +assume +assumption +assure +at +athlete +atmosphere +attach +attachment +attack +attempt +attend +attendance +attention +attitude +attract +attraction +attractive +attribute +audience +aunt +author +automatically +autumn +available +average +avoid +award +aware +awareness +away +awful +baby +back +background +bad +badly +bag +balance +ball +ban +band +bank +bar +barely +barrier +base +basic +basically +basis +bath +battle +be +beach +bear +beat +beautiful +beauty +because +become +bed +bedroom +beer +before +begin +behave +behavior +behind +belief +believe +bell +belong +below +belt +bend +beneath +benefit +beside +besides +bet +between +beyond +bias +bid +big +bike +bill +bin +bind +biological +bird +birth +bit +bite +black +blame +bless +blind +block +blog +blood +bloody +blow +blue +board +boat +body +bomb +bond +bone +book +boom +boost +boot +border +bore +borrow +boss +both +bother +bottle +bottom +boundary +bowl +box +boy +brain +branch +brand +bread +break +breakfast +breast +breath +breathe +breed +bridge +brief +briefly +bright +brilliant +bring +broad +broadcast +brother +brown +brush +budget +build +bunch +burden +burn +burst +bury +bus +business +busy +but +button +buy +buyer +by +cable +cake +calculate +call +calm +camera +camp +campaign +can +cancel +cancer +candidate +cap +capability +capable +capacity +capital +capture +car +carbon +card +care +career +careful +carefully +carpet +carry +case +cash +cast +castle +cat +catalog +catch +category +cause +celebrate +celebration +cell +cent +center +central +century +ceremony +certain +certainly +chain +chair +chairman +challenge +chamber +champion +championship +chance +change +channel +chapter +character +characteristic +characterize +charge +charity +charm +chart +chase +chat +cheap +check +cheek +cheese +chemical +chest +chicken +chief +child +childhood +chip +chocolate +choice +choose +church +cigarette +circle +circumstance +cite +citizen +city +civil +civilian +claim +class +classic +classical +clause +clean +clear +clearly +click +climate +climb +clinical +clock +close +closely +clothes +clothing +cloud +club +cluster +coach +coal +coast +coat +code +coffee +coin +cold +collapse +colleague +collect +collection +college +color +column +combination +combine +come +comedy +comfort +comfortable +command +comment +commercial +commission +commit +commitment +committee +common +communicate +communication +community +company +compare +comparison +compensation +compete +competition +competitive +competitor +complain +complaint +complete +completely +complex +complexity +complicate +component +compose +composition +compound +comprehensive +comprise +compromise +compute +computer +concentrate +concentration +concept +concern +concert +conclude +conclusion +concrete +condition +conduct +conference +confidence +confident +confirm +conflict +confuse +confusion +connect +connection +consequence +consequently +conservative +consider +considerable +consideration +consist +consistent +constant +constantly +constitute +constraint +construct +construction +consult +consultant +consume +consumer +contact +contain +contemporary +content +contest +context +continue +continuous +contract +contrast +contribute +contribution +control +controversial +convention +conventional +conversation +convert +convince +cook +cool +cooperation +cope +copy +core +corner +corporate +corporation +correct +correspond +cost +cough +could +council +counsel +count +counter +country +county +couple +course +court +cousin +cover +coverage +cow +crack +craft +crash +crazy +cream +create +creation +creative +creature +credit +crew +crime +criminal +crisis +criteria +critic +critical +criticism +criticize +crop +cross +crowd +crucial +cry +cultural +culture +cup +curious +currency +current +currently +curtain +curve +custom +customer +cut +cycle +dad +daily +damage +damn +dance +danger +dangerous +dare +dark +darkness +data +database +date +daughter +day +dead +deal +dealer +dear +death +debate +debt +decade +decide +decision +declare +decline +decrease +dedicate +deep +deeply +defeat +defend +defense +deficit +define +definitely +definition +degree +delay +delight +deliver +delivery +demand +democracy +democratic +demonstrate +demonstration +density +deny +department +depend +dependent +deposit +depress +depression +depth +derive +describe +description +desert +deserve +design +designer +desire +desk +despite +destroy +destruction +detail +detect +determination +determine +develop +development +device +devote +dialog +die +diet +differ +difference +different +differently +difficult +difficulty +dig +digital +dimension +dinner +direct +direction +directly +director +dirty +disagree +disappear +disappoint +disaster +discipline +discount +discover +discovery +discuss +discussion +disease +dish +disk +dismiss +disorder +display +dispute +distance +distant +distinct +distinction +distinguish +distribute +district +disturb +diversity +divide +division +divorce +do +doctor +document +dog +dollar +domestic +dominate +door +double +doubt +down +dozen +draft +drag +drama +dramatic +dramatically +draw +dream +dress +drink +drive +driver +drop +drug +dry +due +during +dust +duty +each +ear +early +earn +earth +ease +easily +east +eastern +easy +eat +economic +economy +edge +edit +edition +editor +educate +education +educational +effect +effective +effectively +efficiency +efficient +effort +egg +either +elderly +elect +election +electric +electricity +electronic +element +eliminate +else +elsewhere +email +embarrass +embrace +emerge +emergency +emotion +emotional +emphasis +emphasize +empire +employ +employee +employer +employment +empty +enable +encounter +encourage +end +enemy +energy +engage +engine +engineer +enhance +enjoy +enormous +enough +ensure +enter +enterprise +entertain +entertainment +entire +entirely +entitle +entrance +entry +envelope +environment +environmental +episode +equal +equally +equation +equipment +equivalent +era +error +escape +especially +essay +essential +establish +establishment +estate +estimate +ethnic +evaluate +evaluation +even +evening +event +eventually +ever +every +everybody +everyday +everyone +everything +everywhere +evidence +evil +evolution +evolve +exact +exactly +exam +examination +examine +example +exceed +excellent +except +exception +excess +exchange +excite +excitement +exclude +excuse +executive +exercise +exhaust +exhibit +exhibition +exist +existence +expand +expansion +expect +expectation +expenditure +expense +expensive +experience +experiment +experimental +expert +explain +explanation +explore +export +expose +exposure +express +expression +extend +extension +extensive +extent +external +extra +extract +extraordinary +extreme +extremely +eye +face +facility +fact +factor +factory +fade +fail +failure +fair +fairly +faith +faithfully +fall +false +familiar +family +famous +fan +fancy +fantastic +far +farm +farmer +fascinate +fashion +fast +fat +father +fault +favor +favorite +fear +feature +federal +fee +feed +feel +fellow +female +fence +festival +few +fiction +field +fight +figure +file +fill +film +filter +final +finally +finance +financial +find +fine +finger +finish +fire +firm +firmly +first +firstly +fish +fit +fix +flag +flash +flat +flexible +flight +float +flood +floor +flow +flower +fly +focus +fold +folk +follow +food +fool +foot +football +for +force +forecast +foreign +forest +forever +forget +form +formal +format +formation +former +formula +forth +fortunate +fortune +forward +found +foundation +fragment +frame +framework +free +freedom +freeze +frequency +frequent +frequently +fresh +friend +friendly +friendship +frighten +from +front +fruit +fuel +fulfill +full +fully +fun +function +functional +fund +fundamental +funny +furniture +further +furthermore +future +gain +gallery +game +gap +garden +gas +gate +gather +gay +gaze +gear +gender +gene +general +generally +generate +generation +genetic +gentle +gentleman +gently +genuine +gesture +get +giant +gift +girl +give +glad +glance +glass +global +go +goal +god +gold +golden +golf +good +govern +government +governor +grab +grade +gradually +graduate +grain +grammar +grand +grandmother +grant +grass +grateful +gray +great +greatly +green +greet +grin +ground +group +grow +growth +guarantee +guard +guess +guest +guide +guideline +guilty +guitar +gun +guy +habit +hair +half +hall +hand +handle +hang +happen +happiness +happy +harbor +hard +hardly +harm +hat +hate +have +he +head +health +healthy +hear +heart +heat +heavily +heavy +height +hell +hello +help +helpful +hence +here +hero +herself +hesitate +hi +hide +high +highlight +highly +hill +himself +hint +hire +historian +historic +historical +history +hit +hold +holder +hole +holiday +home +honest +honor +hook +hope +hopefully +horrible +horse +hospital +host +hot +hotel +hour +house +household +how +however +huge +human +humor +hunger +hunt +hurry +hurt +husband +hypothesis +I +ice +idea +ideal +identify +identity +if +ignore +ill +illegal +illness +illustrate +illustration +image +imagination +imagine +immediate +immediately +immigrant +immigration +implement +implementation +implication +imply +import +importance +important +impose +impossible +impress +impression +impressive +improve +improvement +in +incentive +inch +incident +include +income +incorporate +increase +increasingly +indeed +independence +independent +index +indicate +indication +individual +industrial +industry +infant +infection +inflation +influence +inform +information +initial +initially +initiative +injure +injury +inner +innocent +innovation +input +inquiry +inside +insight +insist +inspire +install +instance +instead +institution +institutional +instruction +instrument +insurance +insure +integrate +intellectual +intelligence +intend +intense +intention +interaction +interest +interior +internal +international +interpret +interpretation +intervention +interview +into +introduce +introduction +invent +invest +investigate +investigation +investment +investor +invitation +invite +involve +involvement +iron +island +isolate +issue +it +item +itself +jacket +jail +job +join +joint +joke +journal +journalist +journey +joy +judge +judgment +jump +jury +just +justice +justify +keen +keep +key +kick +kid +kill +kind +king +kiss +kitchen +knee +knife +knock +know +knowledge +label +labor +laboratory +lack +lady +lake +land +landscape +language +large +largely +last +late +latter +laugh +laughter +launch +law +lawyer +lay +layer +lazy +lead +leader +leadership +league +lean +leap +learn +least +leather +leave +lecture +left +leg +legal +legislation +lend +length +less +lesson +let +letter +level +liability +liberal +library +license +lie +life +lift +light +like +likely +limit +limitation +line +link +lip +liquid +list +listen +listener +literally +literary +literature +little +live +load +loan +local +locate +location +lock +log +logic +long +look +loose +lose +loss +lot +loud +love +lovely +lover +low +luck +lucky +lunch +luxury +machine +mad +magazine +magic +mail +main +mainly +maintain +maintenance +major +majority +make +maker +male +man +manage +management +manager +manner +manufacture +manufacturer +many +map +march +margin +mark +market +marriage +marry +mass +massive +master +match +mate +material +mathematics +matter +mature +maximum +may +maybe +mayor +meal +mean +meanwhile +measure +measurement +meat +mechanism +medical +medicine +medium +meet +member +membership +memory +mental +mention +menu +mere +merely +mess +message +metal +meter +method +middle +might +mile +military +milk +mind +mine +minimum +minister +minor +minority +minute +mirror +miss +mission +mistake +mix +mixture +mobile +mode +model +moderate +modern +modify +module +mom +moment +money +monitor +month +monthly +mood +moon +moral +more +moreover +morning +mortgage +most +mostly +mother +motion +motivate +motivation +motor +mount +mountain +mouse +mouth +move +movement +movie +much +multiple +murder +muscle +museum +music +musical +musician +must +mutual +myself +mystery +name +narrative +narrow +nation +national +native +natural +naturally +nature +near +nearby +nearly +necessarily +necessary +neck +need +negative +neglect +negotiate +negotiation +neighbor +neighborhood +neither +nerve +nervous +net +network +never +nevertheless +new +newly +news +newspaper +next +nice +night +no +nobody +noise +none +nor +normal +normally +north +northern +nose +not +note +nothing +notice +notion +noun +novel +now +nowadays +nowhere +nuclear +number +numerous +nurse +object +objective +obligation +observation +observe +obvious +obviously +occasion +occasionally +occupy +occur +ocean +odd +of +off +offense +offer +office +officer +official +often +oil +okay +old +on +once +one +online +only +onto +open +opera +operate +operation +operator +opinion +opponent +opportunity +oppose +opposite +opposition +option +or +orange +order +ordinary +organic +organization +organize +origin +original +originally +other +otherwise +ought +ourselves +out +outcome +outline +output +outside +over +overall +overcome +overseas +owe +own +owner +ownership +pace +pack +package +page +pain +paint +pair +pale +panel +panic +paper +paragraph +parallel +parent +park +part +participant +participate +participation +particular +particularly +partly +partner +partnership +party +pass +passage +passenger +passion +past +path +patient +pattern +pause +pay +payment +peace +peak +peer +pen +penalty +pension +people +per +perceive +percent +percentage +perception +perfect +perfectly +perform +performance +perhaps +period +permanent +permission +permit +person +personal +personality +personally +personnel +perspective +persuade +phase +phenomenon +philosophy +phone +photo +photograph +phrase +physical +piano +pick +picture +piece +pig +pile +pilot +pink +pipe +pitch +place +plain +plan +plane +planet +plant +plastic +plate +platform +play +player +pleasant +please +pleasure +plenty +plot +plus +pocket +poem +poet +poetry +point +police +policy +political +politician +politics +poll +pollution +pool +poor +pop +popular +population +port +portion +portrait +pose +position +positive +possess +possession +possibility +possible +possibly +post +pot +potato +potential +potentially +pound +pour +poverty +power +powerful +practical +practice +praise +pray +precise +precisely +predict +prefer +preference +pregnancy +pregnant +premise +preparation +prepare +presence +present +presentation +preserve +president +presidential +press +pressure +presumably +pretend +pretty +prevent +previous +previously +price +pride +primarily +primary +prime +principal +principle +print +printer +prior +priority +prison +prisoner +private +privilege +prize +pro +probability +probably +problem +procedure +proceed +process +produce +producer +product +production +profession +professional +professor +profile +profit +program +progress +project +promise +promote +promotion +prompt +proof +proper +properly +property +proportion +proposal +propose +prospect +protect +protection +protein +protest +proud +prove +provide +province +provision +psychological +pub +public +publication +publisher +pull +pump +pupil +purchase +pure +purpose +pursue +push +put +qualification +qualify +quality +quantity +quarter +question +quick +quickly +quiet +quietly +quite +quote +race +racial +radical +radio +rail +rain +raise +random +range +rank +rapid +rapidly +rare +rarely +rat +rate +rather +ratio +raw +reach +react +reaction +read +reader +ready +real +reality +realize +really +rear +reason +reasonable +reasonably +recall +receive +recent +recently +reckon +recognition +recognize +recommend +recommendation +record +recover +recovery +recruit +red +reduce +reduction +refer +reference +reflect +reflection +reform +refugee +refuse +regard +regardless +region +regional +register +registration +regret +regular +regularly +regulate +regulation +reject +relate +relation +relationship +relative +relatively +relax +release +relevant +reliable +relief +religion +religious +rely +remain +remark +remarkable +remember +remind +remote +remove +rent +repair +repeat +replace +reply +report +reporter +represent +representation +representative +reputation +request +require +requirement +rescue +research +researcher +reserve +resident +resign +resist +resistance +resolution +resolve +resort +resource +respect +respectively +respond +response +responsibility +responsible +rest +restaurant +restore +restrict +restriction +result +retail +retain +retire +retirement +return +reveal +revenue +reverse +review +revise +revolution +reward +rice +rich +rid +ride +right +ring +rise +risk +rival +river +road +rock +role +roll +romantic +roof +room +root +rose +rough +roughly +round +route +routine +row +royal +ruin +rule +run +rural +rush +sad +safe +safety +sail +sake +salary +sale +salt +same +sample +sanction +sand +satisfaction +satisfy +save +say +scale +scan +scare +scene +schedule +scheme +scholar +school +science +scientific +scientist +scope +score +scream +screen +sea +seal +search +season +seat +second +secondary +secondly +secret +secretary +section +sector +secure +security +see +seed +seek +seem +segment +select +selection +self +sell +send +senior +sense +sensitive +sentence +separate +sequence +series +serious +seriously +servant +serve +server +service +session +set +settle +settlement +several +severe +sex +sexual +shade +shadow +shake +shall +shape +share +shareholder +sharp +she +sheep +sheet +shelf +shell +shelter +shift +shine +ship +shirt +shock +shoe +shoot +shop +shore +short +shot +should +shoulder +shout +show +shower +shut +sick +side +sigh +sight +sign +signal +significance +significant +significantly +silence +silent +silly +silver +similar +similarly +simple +simply +since +sing +singer +single +sink +sir +sister +sit +site +situate +situation +size +ski +skill +skin +skirt +sky +slave +sleep +slice +slide +slight +slightly +slip +slope +slow +slowly +small +smart +smell +smile +smoke +smooth +snap +snow +so +social +society +soft +software +soil +solar +soldier +solid +solution +solve +some +somebody +somehow +someone +something +sometimes +somewhat +somewhere +son +song +soon +sorry +sort +soul +sound +source +south +southern +space +spare +speak +speaker +special +specialist +specialize +species +specific +specifically +specify +speech +speed +spell +spend +spin +spirit +split +sponsor +sport +spot +spread +spring +square +stability +stable +staff +stage +stain +stair +stake +stamp +stand +standard +star +stare +start +state +statement +station +statistic +status +stay +steady +steal +steel +stem +step +stick +still +stimulate +stir +stock +stomach +stone +stop +storage +store +storm +story +straight +strain +strange +stranger +strategy +stream +street +strength +strengthen +stress +stretch +strict +strike +string +strip +stroke +strong +strongly +structural +structure +struggle +student +studio +study +stuff +stupid +style +subject +submit +subsequent +subsequently +substance +substantial +substitute +succeed +success +successful +successfully +such +sudden +suddenly +suffer +sufficient +sugar +suggest +suggestion +suit +suitable +sum +summarize +summary +summer +sun +supplement +supplier +supply +support +supporter +suppose +sure +surely +surface +surgery +surprise +surprisingly +surround +survey +survival +survive +suspect +suspend +sustain +swear +sweep +sweet +swim +swing +switch +symbol +symptom +system +table +tackle +tail +take +tale +talent +talk +tall +tank +tap +tape +target +task +taste +tax +taxi +tea +teach +teacher +team +tear +technical +technique +technology +teenager +telephone +television +tell +temperature +temporary +tend +tendency +tender +tennis +tension +tent +term +terrible +territory +terrorist +test +text +than +thank +that +the +theater +theme +themselves +then +theoretical +theory +therapy +there +therefore +they +thick +thin +thing +think +thirst +this +though +threat +threaten +throat +through +throughout +throw +thus +ticket +tie +tight +till +time +tiny +tip +tire +tissue +title +to +today +together +tomorrow +tone +tongue +tonight +too +tool +tooth +top +topic +total +totally +touch +tough +tour +tourism +tourist +tournament +toward +tower +town +toy +trace +track +trade +tradition +traditional +traffic +trail +train +transfer +transform +transition +translate +transport +transportation +trap +travel +treat +treatment +tree +trend +trial +trick +trigger +trip +troop +trouble +truck +true +truly +trust +truth +try +tube +tune +turn +twice +twin +twist +type +typical +typically +ugly +ultimately +unable +uncertainty +uncle +unclear +under +undergo +underlie +understand +undertake +unemployment +unfortunately +uniform +union +unique +unit +unite +universal +universe +university +unknown +unless +unlike +unlikely +until +unusual +up +update +upon +upper +upset +urban +urge +use +useful +user +usual +usually +valley +valuable +value +van +variable +variation +variety +various +vary +vast +vegetable +vehicle +venture +verb +version +versus +very +vessel +veteran +via +vice +victim +victory +video +view +village +violence +violent +virtually +virus +visible +vision +visit +visitor +visual +vital +voice +volume +voluntary +volunteer +vote +voter +wage +wait +wake +walk +wall +wander +want +war +warm +warn +wash +waste +watch +water +wave +way +we +weak +weakness +wealth +wealthy +weapon +wear +weather +web +website +wed +week +weekend +weekly +weigh +weight +weird +welcome +welfare +well +west +western +wet +what +whatever +wheel +when +whenever +where +whereas +wherever +whether +which +while +whilst +whisper +white +who +whole +why +wide +widely +wife +wild +will +win +wind +window +wine +wing +winner +winter +wipe +wire +wise +wish +with +withdraw +within +without +witness +woman +wonder +wonderful +wood +wooden +word +work +worker +world +worry +worth +would +wound +wrap +write +writer +wrong +yard +yeah +year +yellow +yes +yesterday +yet +yield +you +young +yourself +youth +zone diff --git a/src/ucns/lexical_floor.py b/src/ucns/lexical_floor.py new file mode 100644 index 00000000..b4e34005 --- /dev/null +++ b/src/ucns/lexical_floor.py @@ -0,0 +1,598 @@ +# === MODULE_BUILD === +# id: ngsl_lexical_floor +# module_name: lexical_floor +# module_kind: domain +# summary: unique NGSL word gonols, character-relationship hyperspace potential, layered affixiation, compounding, and contextual-definition snapshots +# owner: Erin Spencer +# public_surface: GlyphDefinition, LexicalWordGonol, CharacterRelationship, LexicalHyperspacePotential, AffixiationCandidate, CompoundCandidate, DefinitionSense, LexicalLayerSnapshot, load_ngsl_words, define_glyphs, create_word_gonols, create_hyperspace_potential, derive_affixiation_candidates, derive_compound_candidates, create_definition_layer, snapshot_layers +# internal_surface: _canonical_digest, _word_sort_key, _edit_distance +# auth_boundary: none +# storage_boundary: packaged immutable text source and caller-selected snapshot output +# network_boundary: none +# user_data_boundary: no user data; source spellings remain exact and definitions require explicit context and source identity +# admin_only: false +# tests: tests/test_lexical_floor.py +# rollout: experimental lexical-floor producer; no hyperdimensional embedding or linguistic canon selection +# rollback: remove this module and packaged NGSL artifacts without altering the existing EDCM word-gonol profile +# since: 2026-08-04 +# unresolved: attested affix authority, compound adjudication, contextual definition custody, and the deep-recursion hyperdimensional embedding law +# === END MODULE_BUILD === + +# === CONTRACTS === +# id: lexical_floor_words_are_unique_exact_glyph_sets +# given: the packaged NGSL word-only source is loaded +# then: exactly 2809 nonempty exact spellings exist, no two spellings share one identical ordered glyph tuple, and no normalization or case folding changes identity +# class: correctness +# since: 2026-08-04 +# +# id: lexical_floor_order_is_serialization_only +# given: the source collection is serialized +# then: deterministic casefold-plus-exact-codepoint order supports reproducible builds but contributes no rank, frequency, meaning, or gonol identity +# class: doctrine +# since: 2026-08-04 +# +# id: lexical_floor_reuses_canonical_glyph_assignment +# given: a source spelling is converted to a word gonol +# then: each exact Unicode scalar occurrence uses the existing EDCM carrier assignment and retains its value, position, multiplicity, and order +# class: evidence +# since: 2026-08-04 +# +# id: lexical_hyperspace_is_projection_not_embedding +# given: two word gonols are compared +# then: exact character-derived relations are projectable without claiming semantic relation, linguistic derivation, geometric proximity, or the unresolved hyperdimensional embedding law +# class: safety +# since: 2026-08-04 +# +# id: affixiation_and_compounding_are_candidate_layers +# given: character decomposition finds a retained base or two retained component words +# then: the result is labeled orthographic-candidate until independently attested and never rewrites the word gonol +# class: doctrine +# since: 2026-08-04 +# +# id: definitions_are_context_plural +# given: definitions are added to one word gonol +# then: multiple senses may coexist when each retains distinct context and source identity; no duplicate word gonol is created +# class: evidence +# since: 2026-08-04 +# +# id: every_added_layer_has_a_snapshot +# given: words, glyphs, gonols, hyperspace potential, affixiation, compounding, or definitions are materialized +# then: an ordered snapshot records its parent, count, digest, standing, and unresolved boundary +# class: correctness +# since: 2026-08-04 +# === END CONTRACTS === + +"""Executable NGSL 1.2 lexical floor for UCNS. + +The exact word spelling is the word-gonol identity. Serialization order exists +only to make builds reproducible. Character relationships, affixiation +candidates, compound candidates, and contextual definitions are appended as +separate projection layers and never rewrite the underlying word gonol. +""" + +from __future__ import annotations + +from dataclasses import asdict, dataclass, is_dataclass +from hashlib import sha256 +from importlib.resources import files +import json +from typing import Iterable, Mapping, Sequence + +from .edcm import edcm_carrier_position + + +LEXICAL_FLOOR_ID = "ucns.lexical-floor.ngsl-1.2" +LEXICAL_FLOOR_VERSION = "0.1.0" +NGSL_WORD_COUNT = 2809 +NGSL_WORD_RESOURCE = "data/ngsl_1_2_words.txt" +SERIALIZATION_ORDER = "unicode-casefold-then-exact-codepoint" +WORD_ID_POLICY = "exact-ordered-unicode-scalar-sequence" +ORTHOGRAPHIC_CANDIDATE_STANDING = "orthographic-candidate" +DEFINITION_STANDING = "context-sourced-definition" + + +class LexicalFloorError(ValueError): + """Raised when a lexical-floor invariant is violated.""" + + +def _word_sort_key(word: str) -> tuple[str, tuple[int, ...]]: + return (word.casefold(), tuple(ord(glyph) for glyph in word)) + + +def _is_unicode_scalar(glyph: str) -> bool: + return len(glyph) == 1 and not 0xD800 <= ord(glyph) <= 0xDFFF + + +def _canonical_value(value: object) -> object: + if is_dataclass(value): + return _canonical_value(asdict(value)) + if isinstance(value, Mapping): + return { + str(key): _canonical_value(item) + for key, item in sorted(value.items(), key=lambda pair: str(pair[0])) + } + if isinstance(value, (tuple, list)): + return [_canonical_value(item) for item in value] + return value + + +def _canonical_digest(value: object) -> str: + payload = json.dumps( + _canonical_value(value), + ensure_ascii=False, + sort_keys=True, + separators=(",", ":"), + ).encode("utf-8") + return sha256(payload).hexdigest() + + +def load_ngsl_words() -> tuple[str, ...]: + """Load and validate the exact word-only NGSL 1.2 collection.""" + + resource = files("ucns").joinpath(NGSL_WORD_RESOURCE) + words = tuple(resource.read_text(encoding="utf-8").splitlines()) + _validate_words(words, expected_count=NGSL_WORD_COUNT) + return words + + +def _validate_words( + words: Sequence[str], + *, + expected_count: int | None = None, +) -> None: + if expected_count is not None and len(words) != expected_count: + raise LexicalFloorError( + f"expected {expected_count} words, received {len(words)}" + ) + if any(not word for word in words): + raise LexicalFloorError("word spellings must be nonempty") + if any(any(not _is_unicode_scalar(glyph) for glyph in word) for word in words): + raise LexicalFloorError("word spellings must contain Unicode scalars only") + if any(any(glyph.isspace() for glyph in word) for word in words): + raise LexicalFloorError("word spellings cannot contain whitespace") + if len(set(words)) != len(words): + raise LexicalFloorError("duplicate exact word spellings are prohibited") + if tuple(sorted(words, key=_word_sort_key)) != tuple(words): + raise LexicalFloorError( + "word serialization must use deterministic casefold/exact ordering" + ) + + +@dataclass(frozen=True, slots=True) +class GlyphDefinition: + """One exact glyph already governed by the canonical EDCM carrier.""" + + value: str + code_point: str + carrier_position: int | None + + def __post_init__(self) -> None: + if not _is_unicode_scalar(self.value): + raise LexicalFloorError("a glyph definition requires one Unicode scalar") + if self.code_point != f"U+{ord(self.value):04X}": + raise LexicalFloorError("glyph code point does not match its value") + if self.carrier_position != edcm_carrier_position(self.value): + raise LexicalFloorError( + "glyph carrier position does not match the EDCM profile" + ) + + +@dataclass(frozen=True, slots=True) +class LexicalWordGonol: + """One unique source word represented by its exact ordered glyph sequence.""" + + word: str + glyphs: tuple[str, ...] + gonol_id: str + + def __post_init__(self) -> None: + if not self.word or tuple(self.word) != self.glyphs: + raise LexicalFloorError("word and ordered glyph sequence must match") + if self.gonol_id != word_gonol_id(self.word): + raise LexicalFloorError("word gonol identity mismatch") + + +def word_gonol_id(word: str) -> str: + if not word or any(not _is_unicode_scalar(glyph) for glyph in word): + raise LexicalFloorError("word identity requires nonempty Unicode scalars") + return f"word-gonol:sha256:{_canonical_digest(tuple(word))}" + + +def define_glyphs(words: Sequence[str]) -> tuple[GlyphDefinition, ...]: + _validate_words(words) + glyphs = sorted({glyph for word in words for glyph in word}, key=ord) + return tuple( + GlyphDefinition( + value=glyph, + code_point=f"U+{ord(glyph):04X}", + carrier_position=edcm_carrier_position(glyph), + ) + for glyph in glyphs + ) + + +def create_word_gonols(words: Sequence[str]) -> tuple[LexicalWordGonol, ...]: + _validate_words(words) + return tuple( + LexicalWordGonol( + word=word, + glyphs=tuple(word), + gonol_id=word_gonol_id(word), + ) + for word in words + ) + + +def _common_prefix_length(left: str, right: str) -> int: + count = 0 + for left_glyph, right_glyph in zip(left, right): + if left_glyph != right_glyph: + break + count += 1 + return count + + +def _common_suffix_length(left: str, right: str) -> int: + count = 0 + for left_glyph, right_glyph in zip(reversed(left), reversed(right)): + if left_glyph != right_glyph: + break + count += 1 + return count + + +def _edit_distance(left: str, right: str) -> int: + previous = list(range(len(right) + 1)) + for left_index, left_glyph in enumerate(left, start=1): + current = [left_index] + for right_index, right_glyph in enumerate(right, start=1): + current.append( + min( + current[-1] + 1, + previous[right_index] + 1, + previous[right_index - 1] + (left_glyph != right_glyph), + ) + ) + previous = current + return previous[-1] + + +@dataclass(frozen=True, slots=True) +class CharacterRelationship: + """An exact orthographic projection between two retained word gonols.""" + + left_gonol_id: str + right_gonol_id: str + shared_glyphs: tuple[str, ...] + common_prefix_length: int + common_suffix_length: int + left_contains_right: bool + right_contains_left: bool + edit_distance: int + standing: str = "character-derived-projection" + + +@dataclass(frozen=True, slots=True) +class LexicalHyperspacePotential: + """A non-geometric potential that can project exact character relations.""" + + word_gonols: tuple[LexicalWordGonol, ...] + _by_word: Mapping[str, LexicalWordGonol] + + def __post_init__(self) -> None: + if len(self.word_gonols) != len(self._by_word): + raise LexicalFloorError("hyperspace words must remain unique") + for gonol in self.word_gonols: + if self._by_word.get(gonol.word) != gonol: + raise LexicalFloorError("hyperspace word index mismatch") + + def gonol(self, word: str) -> LexicalWordGonol: + try: + return self._by_word[word] + except KeyError as error: + raise LexicalFloorError( + f"word is outside the lexical floor: {error.args[0]}" + ) from error + + def project(self, left_word: str, right_word: str) -> CharacterRelationship: + left = self.gonol(left_word) + right = self.gonol(right_word) + return CharacterRelationship( + left_gonol_id=left.gonol_id, + right_gonol_id=right.gonol_id, + shared_glyphs=tuple(sorted(set(left.word) & set(right.word), key=ord)), + common_prefix_length=_common_prefix_length(left.word, right.word), + common_suffix_length=_common_suffix_length(left.word, right.word), + left_contains_right=right.word in left.word, + right_contains_left=left.word in right.word, + edit_distance=_edit_distance(left.word, right.word), + ) + + +def create_hyperspace_potential( + word_gonols: Sequence[LexicalWordGonol], +) -> LexicalHyperspacePotential: + ordered = tuple(word_gonols) + by_word = {gonol.word: gonol for gonol in ordered} + if len(by_word) != len(ordered): + raise LexicalFloorError("one word gonol per exact glyph set is required") + return LexicalHyperspacePotential(ordered, by_word) + + +@dataclass(frozen=True, slots=True) +class AffixiationCandidate: + """An orthographic base-plus-affix candidate, not attested morphology.""" + + base_gonol_id: str + derived_gonol_id: str + base_word: str + derived_word: str + affix: str + side: str + standing: str = ORTHOGRAPHIC_CANDIDATE_STANDING + + def __post_init__(self) -> None: + if not self.affix: + raise LexicalFloorError("affixiation candidate requires an affix") + if self.side not in {"prefix", "suffix"}: + raise LexicalFloorError("affixiation side must be prefix or suffix") + expected = ( + self.affix + self.base_word + if self.side == "prefix" + else self.base_word + self.affix + ) + if expected != self.derived_word: + raise LexicalFloorError("affixiation decomposition mismatch") + + +def derive_affixiation_candidates( + potential: LexicalHyperspacePotential, +) -> tuple[AffixiationCandidate, ...]: + words = potential._by_word + candidates: list[AffixiationCandidate] = [] + for derived in potential.word_gonols: + for split in range(1, len(derived.word)): + left = derived.word[:split] + right = derived.word[split:] + if right in words: + base = words[right] + candidates.append( + AffixiationCandidate( + base_gonol_id=base.gonol_id, + derived_gonol_id=derived.gonol_id, + base_word=base.word, + derived_word=derived.word, + affix=left, + side="prefix", + ) + ) + if left in words: + base = words[left] + candidates.append( + AffixiationCandidate( + base_gonol_id=base.gonol_id, + derived_gonol_id=derived.gonol_id, + base_word=base.word, + derived_word=derived.word, + affix=right, + side="suffix", + ) + ) + return tuple( + sorted( + set(candidates), + key=lambda item: ( + _word_sort_key(item.derived_word), + item.side, + _word_sort_key(item.base_word), + item.affix, + ), + ) + ) + + +@dataclass(frozen=True, slots=True) +class CompoundCandidate: + """An exact two-word decomposition candidate.""" + + compound_gonol_id: str + left_gonol_id: str + right_gonol_id: str + compound_word: str + left_word: str + right_word: str + split_offset: int + standing: str = ORTHOGRAPHIC_CANDIDATE_STANDING + + def __post_init__(self) -> None: + if self.left_word + self.right_word != self.compound_word: + raise LexicalFloorError("compound decomposition mismatch") + if self.split_offset != len(self.left_word): + raise LexicalFloorError("compound split offset mismatch") + + +def derive_compound_candidates( + potential: LexicalHyperspacePotential, +) -> tuple[CompoundCandidate, ...]: + words = potential._by_word + candidates: list[CompoundCandidate] = [] + for compound in potential.word_gonols: + for split in range(1, len(compound.word)): + left_word = compound.word[:split] + right_word = compound.word[split:] + if left_word not in words or right_word not in words: + continue + left = words[left_word] + right = words[right_word] + candidates.append( + CompoundCandidate( + compound_gonol_id=compound.gonol_id, + left_gonol_id=left.gonol_id, + right_gonol_id=right.gonol_id, + compound_word=compound.word, + left_word=left.word, + right_word=right.word, + split_offset=split, + ) + ) + return tuple(candidates) + + +@dataclass(frozen=True, slots=True) +class DefinitionSense: + """One context-derived definition attached to an existing word gonol.""" + + word_gonol_id: str + context_identity: str + definition: str + source_identity: str + standing: str = DEFINITION_STANDING + + def __post_init__(self) -> None: + if not all( + ( + self.word_gonol_id, + self.context_identity, + self.definition, + self.source_identity, + ) + ): + raise LexicalFloorError( + "definition senses require gonol, context, text, and source identity" + ) + + +def create_definition_layer( + potential: LexicalHyperspacePotential, + senses: Iterable[DefinitionSense], +) -> Mapping[str, tuple[DefinitionSense, ...]]: + known_ids = {gonol.gonol_id for gonol in potential.word_gonols} + grouped: dict[str, list[DefinitionSense]] = {} + seen: set[tuple[str, str, str]] = set() + for sense in senses: + if sense.word_gonol_id not in known_ids: + raise LexicalFloorError("definition references an unknown word gonol") + identity = ( + sense.word_gonol_id, + sense.context_identity, + sense.source_identity, + ) + if identity in seen: + raise LexicalFloorError("duplicate contextual definition identity") + seen.add(identity) + grouped.setdefault(sense.word_gonol_id, []).append(sense) + return { + gonol_id: tuple(values) + for gonol_id, values in sorted(grouped.items()) + } + + +@dataclass(frozen=True, slots=True) +class LexicalLayerSnapshot: + """One immutable digest boundary in the append-only lexical layer sequence.""" + + layer_id: str + parent_snapshot_id: str | None + item_count: int + content_digest: str + standing: str + hmmm: str + + @property + def snapshot_id(self) -> str: + return ( + f"{self.layer_id}:sha256:" + f"{_canonical_digest((self.parent_snapshot_id, self.item_count, self.content_digest, self.standing, self.hmmm))}" + ) + + +def snapshot_layers( + words: Sequence[str], + *, + definitions: Iterable[DefinitionSense] = (), +) -> tuple[LexicalLayerSnapshot, ...]: + """Materialize a snapshot after every currently declared layer.""" + + _validate_words(words) + glyphs = define_glyphs(words) + gonols = create_word_gonols(words) + potential = create_hyperspace_potential(gonols) + affixiation = derive_affixiation_candidates(potential) + compounds = derive_compound_candidates(potential) + definition_layer = create_definition_layer(potential, definitions) + flattened_definitions = tuple( + sense + for senses in definition_layer.values() + for sense in senses + ) + + payloads: tuple[ + tuple[str, object, int, str, str], + ..., + ] = ( + ( + "00-words", + tuple(words), + len(words), + "source-admitted-word-only", + "Official-source checksum reconciliation remains required before canonical promotion.", + ), + ( + "01-glyphs", + glyphs, + len(glyphs), + "existing-glyph-canon-applied", + "No new glyph law is asserted by this lexical floor.", + ), + ( + "02-word-gonols", + gonols, + len(gonols), + "implemented-exact-word-gonols", + "The source-to-hyperdimensional-coordinate law remains unresolved.", + ), + ( + "03-character-hyperspace-potential", + (("projection-schema", "character-relationship-v1"), gonols), + len(gonols), + "implemented-on-demand-character-projection", + "Projection capability is not a geometric embedding or semantic relation.", + ), + ( + "04-affixiation", + affixiation, + len(affixiation), + ORTHOGRAPHIC_CANDIDATE_STANDING, + "Linguistic affix authority and allomorphy remain unadjudicated.", + ), + ( + "05-compounding", + compounds, + len(compounds), + ORTHOGRAPHIC_CANDIDATE_STANDING, + "Orthographic decomposition is not yet an attested compound judgment.", + ), + ( + "06-definitions", + flattened_definitions, + len(flattened_definitions), + DEFINITION_STANDING, + "Definition custody, sense boundaries, and context corpus admission remain open.", + ), + ) + + snapshots: list[LexicalLayerSnapshot] = [] + parent: str | None = None + for layer_id, payload, item_count, standing, hmmm in payloads: + digest = _canonical_digest(payload) + snapshot = LexicalLayerSnapshot( + layer_id=layer_id, + parent_snapshot_id=parent, + item_count=item_count, + content_digest=digest, + standing=standing, + hmmm=hmmm, + ) + snapshots.append(snapshot) + parent = snapshot.snapshot_id + return tuple(snapshots) diff --git a/tests/test_lexical_floor.py b/tests/test_lexical_floor.py new file mode 100644 index 00000000..05d9a0d6 --- /dev/null +++ b/tests/test_lexical_floor.py @@ -0,0 +1,175 @@ +# === CHECKS === +# id: lexical_floor_exact_collection_check +# resolves: lexical_floor_words_are_unique_exact_glyph_sets, lexical_floor_order_is_serialization_only +# test: test_ngsl_word_only_collection_is_exact_unique_and_reproducible +# class: correctness +# since: 2026-08-04 +# +# id: lexical_floor_glyph_and_gonol_check +# resolves: lexical_floor_reuses_canonical_glyph_assignment +# test: test_existing_glyph_canon_builds_one_gonol_per_word +# class: evidence +# since: 2026-08-04 +# +# id: lexical_floor_projection_check +# resolves: lexical_hyperspace_is_projection_not_embedding +# test: test_hyperspace_projects_character_relations_without_embedding_claim +# class: safety +# since: 2026-08-04 +# +# id: lexical_floor_candidate_layer_check +# resolves: affixiation_and_compounding_are_candidate_layers +# test: test_affixiation_and_compounding_begin_as_orthographic_candidates +# class: doctrine +# since: 2026-08-04 +# +# id: lexical_floor_definition_plurality_check +# resolves: definitions_are_context_plural +# test: test_one_word_gonol_accepts_many_context_sourced_definitions +# class: evidence +# since: 2026-08-04 +# +# id: lexical_floor_snapshot_check +# resolves: every_added_layer_has_a_snapshot +# test: test_each_added_layer_has_a_parented_snapshot +# class: correctness +# since: 2026-08-04 +# === END CHECKS === + +from dataclasses import replace + +import pytest + +from ucns.lexical_floor import ( + DEFINITION_STANDING, + NGSL_WORD_COUNT, + ORTHOGRAPHIC_CANDIDATE_STANDING, + DefinitionSense, + LexicalFloorError, + create_definition_layer, + create_hyperspace_potential, + create_word_gonols, + define_glyphs, + derive_affixiation_candidates, + derive_compound_candidates, + load_ngsl_words, + snapshot_layers, +) + + +def _floor(): + words = load_ngsl_words() + gonols = create_word_gonols(words) + return words, gonols, create_hyperspace_potential(gonols) + + +def test_ngsl_word_only_collection_is_exact_unique_and_reproducible(): + words = load_ngsl_words() + + assert len(words) == NGSL_WORD_COUNT == 2809 + assert len(set(words)) == len(words) + assert all(word and not any(glyph.isspace() for glyph in word) for word in words) + assert words[0] == "a" + assert words[-1] == "zone" + assert "I" in words + assert not any("," in word for word in words) + + +def test_existing_glyph_canon_builds_one_gonol_per_word(): + words, gonols, _ = _floor() + glyphs = define_glyphs(words) + + assert len(glyphs) == 27 + assert {glyph.value for glyph in glyphs} == set("abcdefghijklmnopqrstuvwxyzI") + assert all(glyph.carrier_position is not None for glyph in glyphs) + assert len(gonols) == len(words) + assert len({gonol.gonol_id for gonol in gonols}) == len(words) + assert all(gonol.glyphs == tuple(gonol.word) for gonol in gonols) + + +def test_hyperspace_projects_character_relations_without_embedding_claim(): + _, _, potential = _floor() + + relation = potential.project("act", "action") + + assert relation.common_prefix_length == 3 + assert relation.right_contains_left is True + assert relation.left_contains_right is False + assert relation.edit_distance == 3 + assert relation.standing == "character-derived-projection" + + +def test_affixiation_and_compounding_begin_as_orthographic_candidates(): + _, _, potential = _floor() + + affixiation = derive_affixiation_candidates(potential) + compounds = derive_compound_candidates(potential) + + assert any( + item.base_word == "act" + and item.derived_word == "action" + and item.affix == "ion" + and item.side == "suffix" + and item.standing == ORTHOGRAPHIC_CANDIDATE_STANDING + for item in affixiation + ) + assert any( + item.compound_word == "background" + and item.left_word == "back" + and item.right_word == "ground" + and item.standing == ORTHOGRAPHIC_CANDIDATE_STANDING + for item in compounds + ) + + +def test_one_word_gonol_accepts_many_context_sourced_definitions(): + _, _, potential = _floor() + word = potential.gonol("bank") + river = DefinitionSense( + word_gonol_id=word.gonol_id, + context_identity="context:river-bank", + definition="The land beside a river.", + source_identity="source:test-contexts-v1", + ) + finance = DefinitionSense( + word_gonol_id=word.gonol_id, + context_identity="context:financial-bank", + definition="An institution that holds and lends money.", + source_identity="source:test-contexts-v1", + ) + + layer = create_definition_layer(potential, (river, finance)) + + assert tuple(layer) == (word.gonol_id,) + assert layer[word.gonol_id] == (river, finance) + assert all(item.standing == DEFINITION_STANDING for item in layer[word.gonol_id]) + with pytest.raises(LexicalFloorError, match="duplicate contextual"): + create_definition_layer(potential, (river, replace(river))) + + +def test_each_added_layer_has_a_parented_snapshot(): + words = load_ngsl_words() + snapshots = snapshot_layers(words) + expected_layers = ( + "00-words", + "01-glyphs", + "02-word-gonols", + "03-character-hyperspace-potential", + "04-affixiation", + "05-compounding", + "06-definitions", + ) + + assert tuple(snapshot.layer_id for snapshot in snapshots) == expected_layers + assert snapshots[0].parent_snapshot_id is None + assert all( + current.parent_snapshot_id == previous.snapshot_id + for previous, current in zip(snapshots, snapshots[1:]) + ) + assert snapshots[0].item_count == 2809 + assert snapshots[1].item_count == 27 + assert snapshots[2].item_count == 2809 + assert snapshots[3].item_count == 2809 + assert snapshots[-1].item_count == 0 + assert all(len(snapshot.content_digest) == 64 for snapshot in snapshots) + assert all(snapshot.hmmm for snapshot in snapshots) From 6f3db2fff61d2d685b12baff6989c008bbe88e58 Mon Sep 17 00:00:00 2001 From: Erin Spencer Date: Wed, 5 Aug 2026 23:34:01 -0700 Subject: [PATCH 2/2] Harden lexical-floor evidence boundaries - centralize exact word validation across every public construction path - reject SPACE-origin and unassigned glyphs rather than coercing them - retain shared glyph occurrence addresses and receipt the lossy type-set view - make hyperspace and definition indexes immutable - bind affix, compound, definition, source, and snapshot standings fail-closed - bind snapshots to an executable source receipt and ordered parent chain - package NGSL attribution and exact source-custody evidence - replace deprecated CHECK declarations with canonical skill-lib checks - add adversarial source, mutation, substitution, and snapshot tests Local lexical gate: 3 passed; 8 contracts resolved by 3 checks; compileall passed; wheel built; installed-wheel smoke passed. The complete repository gate and independently downloaded official NGSL checksum remain required. hmmm: linguistic attestation, official-source checksum custody, selector-role interrogation, deep-recursion embedding, and a usable complete GitHub Actions verdict remain unresolved. --- docs/NGSL_LEXICAL_FLOOR_V1.md | 146 +++-- src/ucns/data/NGSL_1_2_ATTRIBUTION.txt | 10 + src/ucns/data/ngsl_1_2_source.json | 9 +- src/ucns/lexical_floor.py | 811 +++++++++++++++++++++---- tests/test_lexical_floor.py | 207 ++----- tests/test_lexical_floor_layers.py | 71 +++ tests/test_lexical_floor_snapshots.py | 50 ++ 7 files changed, 958 insertions(+), 346 deletions(-) create mode 100644 src/ucns/data/NGSL_1_2_ATTRIBUTION.txt create mode 100644 tests/test_lexical_floor_layers.py create mode 100644 tests/test_lexical_floor_snapshots.py diff --git a/docs/NGSL_LEXICAL_FLOOR_V1.md b/docs/NGSL_LEXICAL_FLOOR_V1.md index e8647c6a..1b799c62 100644 --- a/docs/NGSL_LEXICAL_FLOOR_V1.md +++ b/docs/NGSL_LEXICAL_FLOOR_V1.md @@ -1,93 +1,129 @@ -# NGSL 1.2 lexical floor v0.1 +# NGSL 1.2 lexical floor v0.2 **Authority:** Erin Spencer -**Recorded:** 2026-08-04 -**Status:** executable first assignment; source-admitted candidate +**Recorded:** 2026-08-06 +**Status:** executable source-admitted candidate **Selection effect:** none -This slice admits one word-only collection of 2,809 general-English spellings, -applies the existing UCNS glyph canon, creates one word gonol per exact glyph -sequence, and opens a character-derived projection potential. Affixiation and -compounding begin as orthographic candidate layers. Definitions attach later as -plural context-sourced senses. +This slice admits the 2,809-word NGSL 1.2 collection as a word-only candidate +lexical floor. It applies the existing Public Gonol glyph assignments, creates +one word gonol per exact ordered glyph sequence, and exposes an occurrence- +addressed character-relationship potential. Affixiation, compounding, and +context-derived definitions remain separate append-only layers. -## Decisions +## Fixed boundaries -1. **Source order has no semantic purpose.** Frequency rank and source row order - are excluded from word-gonol identity. The text file uses deterministic - Unicode casefold ordering with an exact code-point tie-break only so builds - and snapshots reproduce byte-for-byte. +1. **One fixed carrier.** The lexical floor activates addresses on the existing + 157-position Public Gonol carrier. It does not introduce a 26-, 27-, or + 2,809-based replacement radix and does not renumber existing addresses. -2. **One spelling, one word gonol.** Exact duplicate spellings fail admission. - Each retained spelling has one exact ordered glyph tuple and one deterministic - gonol identity. Definitions never create another copy of the word. +2. **One exact spelling, one word gonol.** Case, glyph order, and multiplicity + remain authoritative. Exact duplicate spellings fail admission. Source rank, + frequency, and serialization position do not enter identity. -3. **Glyph law is inherited, not rebuilt.** Each Unicode scalar occurrence uses - the carrier assignment already implemented by `src/ucns/edcm.py`. No - normalization, case folding, glyph replacement, or source-order rank enters - identity. +3. **SPACE remains the nesting boundary.** Every public word-construction path + rejects the profile-pinned Unicode SPACE manifestations. A caller cannot use + the direct word identifier or dataclass constructor to smuggle a boundary + into one word gonol. A glyph without a Public Gonol carrier assignment also + fails admission rather than being silently coerced. -4. **The hyperspace is initially potential.** It can project exact character - relationships between any two retained word gonols: shared glyphs, common - prefix and suffix lengths, containment, and edit distance. This is not yet - the deep-recursion hyperdimensional embedding and does not imply semantic or - morphological relation. +4. **Source custody is executable.** The JSON source record, attribution notice, + exact word bytes, terminal-newline policy, Git blob, byte SHA-256, ordered + word-sequence digest, source standing, and unresolved official-checksum + boundary produce one immutable `LexicalSourceReceipt`. -5. **Metadata is append-only and snapshotted.** The layer sequence is: +5. **Relationships retain occurrences.** A character relationship stores every + matching left/right occurrence pair with both offsets. The optional glyph- + type set view is explicitly identified by Unicode scalar value and records + that occurrence order, multiplicity, and pairing were lost. - 1. word-only source; - 2. glyph definitions; - 3. unique word gonols; - 4. character-relationship hyperspace potential; - 5. affixiation candidates; - 6. compound candidates; - 7. context-derived definitions. +6. **Layers are immutable and authority-bound.** The hyperspace index and + definition mapping are read-only defensive copies. Relationship, morphology, + definition, source, and snapshot standings reject caller promotion. - `snapshot_layers()` emits a parent-linked digest boundary after every layer. - Later metadata cannot rewrite an earlier snapshot. +7. **Snapshots remain source-linked.** Every current layer records the same + source-receipt identity, exact producer, ordered parent, item count, content + digest, fixed standing, and required `hmmm`. Altering the source, standing, + unresolved boundary, or parent chain fails validation. + +## Current layer sequence + +1. source-receipted word-only collection; +2. fixed-carrier glyph definitions; +3. unique exact word gonols; +4. occurrence-addressed character-relationship potential; +5. orthographic affixiation candidates; +6. orthographic compound candidates; +7. plural context-sourced definitions. + +Later metadata cannot rewrite an earlier source object or snapshot. ## Affixiation An affixiation candidate exists when one retained word can be produced by adding -a nonempty prefix or suffix to another retained word. The result is -`orthographic-candidate`, not attested morphology. It does not establish -allomorphy, historical derivation, morpheme identity, or meaning. +one nonempty prefix or suffix to another retained word. The candidate binds the +exact base and derived word-gonol identities. Its standing remains +`orthographic-candidate`; it does not establish morpheme authority, allomorphy, +historical derivation, or meaning. ## Compounding -A compound candidate exists when one retained word can be divided at an exact -glyph boundary into two other retained words. It is likewise -`orthographic-candidate` until separately attested. +A compound candidate exists when one retained word divides at an exact glyph +boundary into two other retained words. The candidate binds all three exact word- +gonol identities and remains `orthographic-candidate` until independently +attested. ## Definitions -Definitions are keyed to the existing word-gonol identity. Each sense must retain -a context identity and source identity. Many senses may attach to one word gonol; -the source word itself remains singular. +Definitions attach to an existing word-gonol identity. Every sense retains a +context identity, exact definition text, source identity, and fixed +`context-sourced-definition` standing. Multiple senses may coexist; the source +word gonol remains singular. + +## Source and attribution files + +- `src/ucns/data/ngsl_1_2_words.txt` — exact word strings only. +- `src/ucns/data/ngsl_1_2_source.json` — acquisition, identity, serialization, + attribution digest, standing, and custody boundary. +- `src/ucns/data/NGSL_1_2_ATTRIBUTION.txt` — packaged creator, source, license, + transformation, non-endorsement, and unresolved-custody notice. + +The data record declares CC BY-SA 4.0 for NGSL 1.2. This notice addresses the +packaged lexical artifact; it does not manufacture a repository-wide software +license or resolve the repository's broader licensing policy. + +## Verification surface -## Files +The test slice exercises: -- `src/ucns/data/ngsl_1_2_words.txt` — word strings only. -- `src/ucns/data/ngsl_1_2_source.json` — separate provenance and admission policy. -- `src/ucns/lexical_floor.py` — gonols, projection potential, layers, and snapshots. -- `tests/test_lexical_floor.py` — collection, identity, candidate, definition, and - snapshot boundaries. +- exact source receipt and tamper rejection; +- direct and indirect SPACE-boundary bypasses; +- unassigned-glyph rejection; +- unique exact word-gonol identities; +- occurrence multiplicity and address retention; +- explicit loss in the glyph-type set projection; +- immutable indexes and definition layers; +- candidate-identity and standing substitution attacks; and +- source, parent, standing, and unresolved-boundary snapshot attacks. ## Nonclaims This slice does not provide: +- an independently custodied official NGSL checksum; - a canonical linguistic affix inventory; - adjudicated compound-word status; - a definitions corpus; - a semantic metric; -- geometric coordinates for lexical relations; +- geometric coordinates for lexical relationships; +- the seven-gonol construction or pairing plan; - deep-recursion hyperdimensional embedding; or - EDCM measurement activation. ## hmmm -The 2,809 spellings are now an executable candidate lexical floor. Independent -reconciliation against an official NGSL 1.2 download, attested morphology, -context-corpus custody, and the law that embeds these retained relationships into -UCNS deep recursion remain living boundaries. +The 2,809 spellings can now enter one source-bound, occurrence-preserving lexical +producer without crossing SPACE or laundering a lossy set view into exact +evidence. Official-source checksum custody, attested morphology, context-corpus +custody, selector-role interrogation, and the law that embeds retained relations +into UCNS deep recursion remain living boundaries. diff --git a/src/ucns/data/NGSL_1_2_ATTRIBUTION.txt b/src/ucns/data/NGSL_1_2_ATTRIBUTION.txt new file mode 100644 index 00000000..55387dda --- /dev/null +++ b/src/ucns/data/NGSL_1_2_ATTRIBUTION.txt @@ -0,0 +1,10 @@ +NGSL 1.2 word-only lexical-floor attribution + +Work: New General Service List 1.2 +Creators: Charles Browne; Brent Culligan; Joseph Phillips +Official source: https://www.newgeneralservicelist.com/new-general-service-list +License: Creative Commons Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) + +UCNS use: the exact 2,809 source spellings are retained as a word-only candidate lexical floor. Source rank and frequency are omitted from word-gonol identity. Serialization order exists only for reproducibility. No semantic, morphological, geometric, or endorsement claim follows from inclusion. + +Custody boundary: the packaged mirror remains a source-admitted candidate until independently reconciled against an official NGSL 1.2 download and checksum. diff --git a/src/ucns/data/ngsl_1_2_source.json b/src/ucns/data/ngsl_1_2_source.json index 511ca15d..2ce3b15c 100644 --- a/src/ucns/data/ngsl_1_2_source.json +++ b/src/ucns/data/ngsl_1_2_source.json @@ -11,6 +11,8 @@ "official_source": "https://www.newgeneralservicelist.com/new-general-service-list", "word_count": 2809, "word_file": "ngsl_1_2_words.txt", + "attribution_file": "NGSL_1_2_ATTRIBUTION.txt", + "attribution_sha256": "3497532cecce55237e2e58ba09b542f98813ea1d6ac4dfbe59a6003b25e59e6f", "acquisition": { "mirror_repository": "uigywnkiub/english-of-avail", "mirror_commit": "05f9e92d570feaa1f6fd4d3145151b49afab93d5", @@ -27,6 +29,11 @@ "frequency_retained": false, "serialization_order": "Unicode casefold key, then exact code-point tuple; ordering has no semantic standing" }, - "standing": "source-admitted candidate", + "file_serialization": { + "encoding": "UTF-8", + "line_separator": "LF", + "terminal_newline": true + }, + "standing": "source-admitted-candidate", "hmmm": "Reconcile the exact packaged spellings against an independently downloaded official NGSL 1.2 file and freeze that official file checksum before canonical promotion." } diff --git a/src/ucns/lexical_floor.py b/src/ucns/lexical_floor.py index b4e34005..6a8c8293 100644 --- a/src/ucns/lexical_floor.py +++ b/src/ucns/lexical_floor.py @@ -2,26 +2,32 @@ # id: ngsl_lexical_floor # module_name: lexical_floor # module_kind: domain -# summary: unique NGSL word gonols, character-relationship hyperspace potential, layered affixiation, compounding, and contextual-definition snapshots +# summary: source-bound NGSL word gonols, occurrence-addressed character relationships, layered affixiation, compounding, contextual definitions, and immutable snapshots # owner: Erin Spencer -# public_surface: GlyphDefinition, LexicalWordGonol, CharacterRelationship, LexicalHyperspacePotential, AffixiationCandidate, CompoundCandidate, DefinitionSense, LexicalLayerSnapshot, load_ngsl_words, define_glyphs, create_word_gonols, create_hyperspace_potential, derive_affixiation_candidates, derive_compound_candidates, create_definition_layer, snapshot_layers -# internal_surface: _canonical_digest, _word_sort_key, _edit_distance +# public_surface: GlyphDefinition, LexicalSourceReceipt, LexicalWordGonol, SharedGlyphOccurrence, GlyphTypeSetProjection, CharacterRelationship, LexicalHyperspacePotential, AffixiationCandidate, CompoundCandidate, DefinitionSense, LexicalLayerSnapshot, load_ngsl_source_receipt, load_ngsl_words, define_glyphs, create_word_gonols, create_hyperspace_potential, derive_affixiation_candidates, derive_compound_candidates, create_definition_layer, snapshot_layers, validate_snapshot_chain, word_gonol_id +# internal_surface: _canonical_digest, _word_sort_key, _edit_distance, _load_source_bundle, _git_blob_sha1 # auth_boundary: none -# storage_boundary: packaged immutable text source and caller-selected snapshot output +# storage_boundary: packaged immutable text and JSON source evidence; caller-selected snapshot output # network_boundary: none -# user_data_boundary: no user data; source spellings remain exact and definitions require explicit context and source identity +# user_data_boundary: no user data; exact source spellings remain unchanged and definitions require explicit context and source identity # admin_only: false # tests: tests/test_lexical_floor.py # rollout: experimental lexical-floor producer; no hyperdimensional embedding or linguistic canon selection # rollback: remove this module and packaged NGSL artifacts without altering the existing EDCM word-gonol profile # since: 2026-08-04 -# unresolved: attested affix authority, compound adjudication, contextual definition custody, and the deep-recursion hyperdimensional embedding law +# unresolved: independent official-source checksum custody, attested affix authority, compound adjudication, contextual definition custody, and the deep-recursion hyperdimensional embedding law # === END MODULE_BUILD === # === CONTRACTS === +# id: lexical_floor_source_receipt_binds_packaged_bytes +# given: the packaged NGSL source metadata and word file are loaded +# then: exact schema, collection, attribution notice, serialization, count, Git blob, byte digests, word-sequence digest, and unresolved custody boundary are retained in one immutable source receipt +# class: evidence +# since: 2026-08-06 +# # id: lexical_floor_words_are_unique_exact_glyph_sets -# given: the packaged NGSL word-only source is loaded -# then: exactly 2809 nonempty exact spellings exist, no two spellings share one identical ordered glyph tuple, and no normalization or case folding changes identity +# given: any public word-gonol construction path is used +# then: the word is nonempty, contains only assigned Unicode scalars, contains no profile-pinned SPACE manifestation, preserves exact case and order, and cannot duplicate another exact spelling in one floor # class: correctness # since: 2026-08-04 # @@ -37,59 +43,120 @@ # class: evidence # since: 2026-08-04 # -# id: lexical_hyperspace_is_projection_not_embedding +# id: lexical_hyperspace_is_occurrence_preserving_projection_not_embedding # given: two word gonols are compared -# then: exact character-derived relations are projectable without claiming semantic relation, linguistic derivation, geometric proximity, or the unresolved hyperdimensional embedding law +# then: exact cross-word glyph occurrence addresses, prefix, suffix, containment, and edit distance are retained; any glyph-type set view names its identity policy and information loss; no semantic, morphological, geometric, or embedding standing follows # class: safety -# since: 2026-08-04 +# since: 2026-08-06 # # id: affixiation_and_compounding_are_candidate_layers # given: character decomposition finds a retained base or two retained component words -# then: the result is labeled orthographic-candidate until independently attested and never rewrites the word gonol +# then: the result is constructor-bound to exact word identities and labeled orthographic-candidate until independently attested; it never rewrites the word gonol # class: doctrine # since: 2026-08-04 # -# id: definitions_are_context_plural -# given: definitions are added to one word gonol -# then: multiple senses may coexist when each retains distinct context and source identity; no duplicate word gonol is created +# id: definitions_are_context_plural_and_immutable +# given: definitions are added to retained word gonols +# then: multiple senses may coexist with distinct context and source identity, authority standing is fixed, and the returned layer cannot be mutated in place # class: evidence -# since: 2026-08-04 +# since: 2026-08-06 # -# id: every_added_layer_has_a_snapshot +# id: every_added_layer_has_a_source_bound_snapshot # given: words, glyphs, gonols, hyperspace potential, affixiation, compounding, or definitions are materialized -# then: an ordered snapshot records its parent, count, digest, standing, and unresolved boundary +# then: the exact ordered snapshot chain records the source receipt, producer, parent, count, digest, fixed standing, and unresolved boundary; a changed source or parent fails validation # class: correctness -# since: 2026-08-04 +# since: 2026-08-06 # === END CONTRACTS === -"""Executable NGSL 1.2 lexical floor for UCNS. +"""Executable, source-bound NGSL 1.2 lexical floor for UCNS. -The exact word spelling is the word-gonol identity. Serialization order exists -only to make builds reproducible. Character relationships, affixiation -candidates, compound candidates, and contextual definitions are appended as -separate projection layers and never rewrite the underlying word gonol. +The exact spelling is the word-gonol identity. Serialization order exists only +for reproducible builds. Character relationships retain occurrence addresses; +the optional glyph-type set projection names its identity policy and declared +loss. Affixiation candidates, compound candidates, and contextual definitions +are append-only layers that never rewrite the source word gonol. """ from __future__ import annotations -from dataclasses import asdict, dataclass, is_dataclass -from hashlib import sha256 +from dataclasses import dataclass, field, fields, is_dataclass +from functools import lru_cache +from hashlib import sha1, sha256 from importlib.resources import files import json +import re +from types import MappingProxyType from typing import Iterable, Mapping, Sequence from .edcm import edcm_carrier_position LEXICAL_FLOOR_ID = "ucns.lexical-floor.ngsl-1.2" -LEXICAL_FLOOR_VERSION = "0.1.0" +LEXICAL_FLOOR_VERSION = "0.2.0" +LEXICAL_PRODUCER_ID = f"{LEXICAL_FLOOR_ID}/{LEXICAL_FLOOR_VERSION}" +LEXICAL_SOURCE_SCHEMA_ID = "ucns.lexical-source/0.1.0" +LEXICAL_COLLECTION_ID = "ngsl-1.2-general-english-2809" +LEXICAL_SOURCE_STANDING = "source-admitted-candidate" +LEXICAL_SOURCE_RECEIPT_PREFIX = "ucns.lexical-source-receipt:sha256:" NGSL_WORD_COUNT = 2809 NGSL_WORD_RESOURCE = "data/ngsl_1_2_words.txt" +NGSL_SOURCE_RESOURCE = "data/ngsl_1_2_source.json" +NGSL_ATTRIBUTION_RESOURCE = "data/NGSL_1_2_ATTRIBUTION.txt" SERIALIZATION_ORDER = "unicode-casefold-then-exact-codepoint" WORD_ID_POLICY = "exact-ordered-unicode-scalar-sequence" +CHARACTER_RELATIONSHIP_STANDING = "character-derived-occurrence-projection" +GLYPH_TYPE_SET_STANDING = "declared-loss-glyph-type-set-projection" +GLYPH_TYPE_SET_IDENTITY_POLICY = "unicode-scalar-value" +GLYPH_TYPE_SET_INFORMATION_LOSS = ( + "left and right occurrence offsets are omitted", + "occurrence multiplicity is omitted", + "cross-word occurrence pairing is omitted", +) ORTHOGRAPHIC_CANDIDATE_STANDING = "orthographic-candidate" DEFINITION_STANDING = "context-sourced-definition" +_HEX_40 = re.compile(r"^[0-9a-f]{40}$") +_HEX_64 = re.compile(r"^[0-9a-f]{64}$") + +_LAYER_SPECS: tuple[tuple[str, str, str], ...] = ( + ( + "00-words", + "source-admitted-word-only", + "Independent official-source checksum custody remains required before canonical promotion.", + ), + ( + "01-glyphs", + "existing-glyph-canon-applied", + "No new glyph law is asserted by this lexical floor.", + ), + ( + "02-word-gonols", + "implemented-exact-word-gonols", + "The source-to-hyperdimensional-coordinate law remains unresolved.", + ), + ( + "03-character-hyperspace-potential", + "implemented-occurrence-addressed-character-projection", + "Projection capability is not a geometric embedding or semantic relation.", + ), + ( + "04-affixiation", + ORTHOGRAPHIC_CANDIDATE_STANDING, + "Linguistic affix authority and allomorphy remain unadjudicated.", + ), + ( + "05-compounding", + ORTHOGRAPHIC_CANDIDATE_STANDING, + "Orthographic decomposition is not yet an attested compound judgment.", + ), + ( + "06-definitions", + DEFINITION_STANDING, + "Definition custody, sense boundaries, and context-corpus admission remain open.", + ), +) +_LAYER_INDEX = {layer_id: index for index, (layer_id, _, _) in enumerate(_LAYER_SPECS)} + class LexicalFloorError(ValueError): """Raised when a lexical-floor invariant is violated.""" @@ -100,12 +167,16 @@ def _word_sort_key(word: str) -> tuple[str, tuple[int, ...]]: def _is_unicode_scalar(glyph: str) -> bool: - return len(glyph) == 1 and not 0xD800 <= ord(glyph) <= 0xDFFF + return isinstance(glyph, str) and len(glyph) == 1 and not 0xD800 <= ord(glyph) <= 0xDFFF def _canonical_value(value: object) -> object: if is_dataclass(value): - return _canonical_value(asdict(value)) + return { + item.name: _canonical_value(getattr(value, item.name)) + for item in fields(value) + if item.repr or item.compare + } if isinstance(value, Mapping): return { str(key): _canonical_value(item) @@ -126,13 +197,42 @@ def _canonical_digest(value: object) -> str: return sha256(payload).hexdigest() -def load_ngsl_words() -> tuple[str, ...]: - """Load and validate the exact word-only NGSL 1.2 collection.""" +def _git_blob_sha1(payload: bytes) -> str: + header = f"blob {len(payload)}\0".encode("ascii") + return sha1(header + payload).hexdigest() - resource = files("ucns").joinpath(NGSL_WORD_RESOURCE) - words = tuple(resource.read_text(encoding="utf-8").splitlines()) - _validate_words(words, expected_count=NGSL_WORD_COUNT) - return words + +def _require_exact_keys(value: Mapping[str, object], expected: set[str], label: str) -> None: + actual = set(value) + if actual != expected: + missing = sorted(expected - actual) + extra = sorted(actual - expected) + raise LexicalFloorError( + f"{label} keys mismatch; missing={missing}, extra={extra}" + ) + + +def _require_nonempty_string(value: object, label: str) -> str: + if not isinstance(value, str) or not value: + raise LexicalFloorError(f"{label} must be a nonempty string") + return value + + +def _validate_word(word: str) -> None: + if not isinstance(word, str) or not word: + raise LexicalFloorError("word spellings must be nonempty strings") + for glyph in word: + if not _is_unicode_scalar(glyph): + raise LexicalFloorError("word spellings must contain Unicode scalars only") + position = edcm_carrier_position(glyph) + if position == 0: + raise LexicalFloorError( + "word spellings cannot cross a profile-pinned SPACE boundary" + ) + if position is None: + raise LexicalFloorError( + f"word glyph lacks a Public Gonol carrier assignment: U+{ord(glyph):04X}" + ) def _validate_words( @@ -144,12 +244,8 @@ def _validate_words( raise LexicalFloorError( f"expected {expected_count} words, received {len(words)}" ) - if any(not word for word in words): - raise LexicalFloorError("word spellings must be nonempty") - if any(any(not _is_unicode_scalar(glyph) for glyph in word) for word in words): - raise LexicalFloorError("word spellings must contain Unicode scalars only") - if any(any(glyph.isspace() for glyph in word) for word in words): - raise LexicalFloorError("word spellings cannot contain whitespace") + for word in words: + _validate_word(word) if len(set(words)) != len(words): raise LexicalFloorError("duplicate exact word spellings are prohibited") if tuple(sorted(words, key=_word_sort_key)) != tuple(words): @@ -158,23 +254,318 @@ def _validate_words( ) +@dataclass(frozen=True, slots=True) +class LexicalSourceReceipt: + """Producer-issued evidence binding metadata to the exact packaged word bytes.""" + + schema_id: str + collection_id: str + title: str + creators: tuple[str, ...] + license: str + official_source: str + word_count: int + word_file: str + attribution_file: str + attribution_sha256: str + mirror_repository: str + mirror_commit: str + mirror_path: str + mirror_git_blob: str + target_git_blob: str + metadata_sha256: str + word_file_sha256: str + word_sequence_sha256: str + identity_policy: str + serialization_order: str + file_serialization: str + standing: str + hmmm: str + producer_id: str = LEXICAL_PRODUCER_ID + + def __post_init__(self) -> None: + if self.schema_id != LEXICAL_SOURCE_SCHEMA_ID: + raise LexicalFloorError("lexical source schema identity mismatch") + if self.collection_id != LEXICAL_COLLECTION_ID: + raise LexicalFloorError("lexical collection identity mismatch") + if not isinstance(self.title, str) or not self.title: + raise LexicalFloorError("source title must remain explicit") + if not self.creators or not all( + isinstance(creator, str) and creator for creator in self.creators + ): + raise LexicalFloorError("source creators must remain explicit strings") + if not isinstance(self.license, str) or not self.license: + raise LexicalFloorError("source license is required") + if not isinstance(self.official_source, str) or not self.official_source: + raise LexicalFloorError("official source is required") + if self.word_count != NGSL_WORD_COUNT: + raise LexicalFloorError("source receipt word count mismatch") + if self.word_file != NGSL_WORD_RESOURCE.rsplit("/", 1)[-1]: + raise LexicalFloorError("source receipt word file mismatch") + if self.attribution_file != NGSL_ATTRIBUTION_RESOURCE.rsplit("/", 1)[-1]: + raise LexicalFloorError("source receipt attribution file mismatch") + if not _HEX_64.fullmatch(self.attribution_sha256): + raise LexicalFloorError("attribution digest must be lowercase SHA-256") + if not self.mirror_repository or not self.mirror_path: + raise LexicalFloorError("mirror repository and path are required") + if not _HEX_40.fullmatch(self.mirror_commit): + raise LexicalFloorError("mirror commit must be a full Git SHA") + if not _HEX_40.fullmatch(self.mirror_git_blob): + raise LexicalFloorError("mirror Git blob must be a full SHA") + if not _HEX_40.fullmatch(self.target_git_blob): + raise LexicalFloorError("target Git blob must be a full SHA") + for digest_name, digest in ( + ("metadata", self.metadata_sha256), + ("word file", self.word_file_sha256), + ("word sequence", self.word_sequence_sha256), + ): + if not _HEX_64.fullmatch(digest): + raise LexicalFloorError(f"{digest_name} digest must be lowercase SHA-256") + if self.identity_policy != WORD_ID_POLICY: + raise LexicalFloorError("source receipt word identity policy mismatch") + if self.serialization_order != SERIALIZATION_ORDER: + raise LexicalFloorError("source receipt serialization order mismatch") + if self.file_serialization != "utf-8-lf-terminal-newline": + raise LexicalFloorError("source file serialization mismatch") + if self.standing != LEXICAL_SOURCE_STANDING: + raise LexicalFloorError("source receipt standing cannot be promoted") + if not self.hmmm: + raise LexicalFloorError("source receipt must retain an unresolved boundary") + if self.producer_id != LEXICAL_PRODUCER_ID: + raise LexicalFloorError("source receipt producer mismatch") + + @property + def receipt_id(self) -> str: + return f"{LEXICAL_SOURCE_RECEIPT_PREFIX}{_canonical_digest(self)}" + + +def _parse_source_bundle( + metadata_payload: bytes, + word_payload: bytes, + attribution_payload: bytes, +) -> tuple[LexicalSourceReceipt, tuple[str, ...]]: + try: + metadata_text = metadata_payload.decode("utf-8", errors="strict") + metadata = json.loads(metadata_text) + except (UnicodeDecodeError, json.JSONDecodeError) as error: + raise LexicalFloorError("lexical source metadata must be strict UTF-8 JSON") from error + if not isinstance(metadata, dict): + raise LexicalFloorError("lexical source metadata must be a JSON object") + + _require_exact_keys( + metadata, + { + "schema_id", + "collection_id", + "title", + "creators", + "license", + "official_source", + "word_count", + "word_file", + "attribution_file", + "attribution_sha256", + "acquisition", + "identity_policy", + "file_serialization", + "standing", + "hmmm", + }, + "lexical source metadata", + ) + acquisition = metadata["acquisition"] + identity = metadata["identity_policy"] + file_serialization = metadata["file_serialization"] + if not isinstance(acquisition, dict) or not isinstance(identity, dict) or not isinstance(file_serialization, dict): + raise LexicalFloorError("source acquisition, identity, and serialization must be objects") + _require_exact_keys( + acquisition, + { + "mirror_repository", + "mirror_commit", + "mirror_path", + "mirror_git_blob", + "target_git_blob", + }, + "source acquisition", + ) + _require_exact_keys( + identity, + { + "word_identity", + "normalization", + "case_folding", + "duplicate_policy", + "source_rank_retained", + "frequency_retained", + "serialization_order", + }, + "source identity policy", + ) + _require_exact_keys( + file_serialization, + {"encoding", "line_separator", "terminal_newline"}, + "source file serialization", + ) + + for key in ( + "schema_id", + "collection_id", + "title", + "license", + "official_source", + "word_file", + "attribution_file", + "attribution_sha256", + "standing", + "hmmm", + ): + _require_nonempty_string(metadata[key], f"source metadata {key}") + creators = metadata["creators"] + if not isinstance(creators, list) or not creators or not all( + isinstance(creator, str) and creator for creator in creators + ): + raise LexicalFloorError("source metadata creators must be a nonempty string list") + if not isinstance(metadata["word_count"], int) or isinstance(metadata["word_count"], bool): + raise LexicalFloorError("source metadata word_count must be an integer") + for key in acquisition: + _require_nonempty_string(acquisition[key], f"source acquisition {key}") + + if metadata["schema_id"] != LEXICAL_SOURCE_SCHEMA_ID: + raise LexicalFloorError("source schema identity mismatch") + if metadata["collection_id"] != LEXICAL_COLLECTION_ID: + raise LexicalFloorError("source collection identity mismatch") + if metadata["word_count"] != NGSL_WORD_COUNT: + raise LexicalFloorError("source metadata word count mismatch") + if metadata["word_file"] != NGSL_WORD_RESOURCE.rsplit("/", 1)[-1]: + raise LexicalFloorError("source metadata word file mismatch") + if metadata["attribution_file"] != NGSL_ATTRIBUTION_RESOURCE.rsplit("/", 1)[-1]: + raise LexicalFloorError("source metadata attribution file mismatch") + if not _HEX_64.fullmatch(metadata["attribution_sha256"]): + raise LexicalFloorError("source metadata attribution digest is malformed") + if metadata["standing"] != LEXICAL_SOURCE_STANDING: + raise LexicalFloorError("source metadata standing cannot be promoted") + if not isinstance(metadata["hmmm"], str) or not metadata["hmmm"]: + raise LexicalFloorError("source metadata must retain hmmm") + if identity != { + "word_identity": "exact ordered Unicode scalar sequence", + "normalization": "none", + "case_folding": "none", + "duplicate_policy": "reject exact duplicate spellings", + "source_rank_retained": False, + "frequency_retained": False, + "serialization_order": "Unicode casefold key, then exact code-point tuple; ordering has no semantic standing", + }: + raise LexicalFloorError("source identity policy mismatch") + if file_serialization != { + "encoding": "UTF-8", + "line_separator": "LF", + "terminal_newline": True, + }: + raise LexicalFloorError("source file serialization declaration mismatch") + + if not attribution_payload.endswith(b"\n") or b"\r" in attribution_payload: + raise LexicalFloorError("attribution file must use LF and retain one terminal newline") + try: + attribution_text = attribution_payload.decode("utf-8", errors="strict") + except UnicodeDecodeError as error: + raise LexicalFloorError("attribution file must be strict UTF-8") from error + if attribution_text.startswith("\ufeff"): + raise LexicalFloorError("attribution file cannot contain a UTF-8 BOM") + if sha256(attribution_payload).hexdigest() != metadata["attribution_sha256"]: + raise LexicalFloorError("packaged attribution does not match its declared digest") + required_attribution = ( + "New General Service List 1.2", + "Charles Browne; Brent Culligan; Joseph Phillips", + "CC BY-SA 4.0", + metadata["official_source"], + "independently reconciled", + ) + if not all(fragment in attribution_text for fragment in required_attribution): + raise LexicalFloorError("attribution file omits required source or custody evidence") + + if not word_payload.endswith(b"\n") or b"\r" in word_payload: + raise LexicalFloorError("word file must use LF and retain one terminal newline") + try: + word_text = word_payload.decode("utf-8", errors="strict") + except UnicodeDecodeError as error: + raise LexicalFloorError("word file must be strict UTF-8") from error + if word_text.startswith("\ufeff"): + raise LexicalFloorError("word file cannot contain a UTF-8 BOM") + words = tuple(word_text[:-1].split("\n")) + _validate_words(words, expected_count=NGSL_WORD_COUNT) + + target_blob = _git_blob_sha1(word_payload) + if acquisition["target_git_blob"] != target_blob: + raise LexicalFloorError("packaged word bytes do not match the declared Git blob") + + receipt = LexicalSourceReceipt( + schema_id=str(metadata["schema_id"]), + collection_id=str(metadata["collection_id"]), + title=metadata["title"], + creators=tuple(metadata["creators"]), + license=metadata["license"], + official_source=metadata["official_source"], + word_count=metadata["word_count"], + word_file=metadata["word_file"], + attribution_file=metadata["attribution_file"], + attribution_sha256=metadata["attribution_sha256"], + mirror_repository=acquisition["mirror_repository"], + mirror_commit=acquisition["mirror_commit"], + mirror_path=acquisition["mirror_path"], + mirror_git_blob=acquisition["mirror_git_blob"], + target_git_blob=target_blob, + metadata_sha256=sha256(metadata_payload).hexdigest(), + word_file_sha256=sha256(word_payload).hexdigest(), + word_sequence_sha256=_canonical_digest(words), + identity_policy=WORD_ID_POLICY, + serialization_order=SERIALIZATION_ORDER, + file_serialization="utf-8-lf-terminal-newline", + standing=metadata["standing"], + hmmm=metadata["hmmm"], + ) + return receipt, words + + +@lru_cache(maxsize=1) +def _load_source_bundle() -> tuple[LexicalSourceReceipt, tuple[str, ...]]: + package = files("ucns") + metadata_payload = package.joinpath(NGSL_SOURCE_RESOURCE).read_bytes() + word_payload = package.joinpath(NGSL_WORD_RESOURCE).read_bytes() + attribution_payload = package.joinpath(NGSL_ATTRIBUTION_RESOURCE).read_bytes() + return _parse_source_bundle(metadata_payload, word_payload, attribution_payload) + + +def load_ngsl_source_receipt() -> LexicalSourceReceipt: + """Load the immutable receipt binding metadata to exact packaged bytes.""" + + return _load_source_bundle()[0] + + +def load_ngsl_words() -> tuple[str, ...]: + """Load the exact source-receipted NGSL 1.2 word-only collection.""" + + return _load_source_bundle()[1] + + @dataclass(frozen=True, slots=True) class GlyphDefinition: - """One exact glyph already governed by the canonical EDCM carrier.""" + """One exact glyph governed by the fixed Public Gonol carrier.""" value: str code_point: str - carrier_position: int | None + carrier_position: int def __post_init__(self) -> None: if not _is_unicode_scalar(self.value): raise LexicalFloorError("a glyph definition requires one Unicode scalar") if self.code_point != f"U+{ord(self.value):04X}": raise LexicalFloorError("glyph code point does not match its value") - if self.carrier_position != edcm_carrier_position(self.value): - raise LexicalFloorError( - "glyph carrier position does not match the EDCM profile" - ) + expected = edcm_carrier_position(self.value) + if expected is None or expected == 0: + raise LexicalFloorError("lexical glyph must be assigned and non-SPACE") + if self.carrier_position != expected: + raise LexicalFloorError("glyph carrier position does not match the EDCM profile") @dataclass(frozen=True, slots=True) @@ -186,16 +577,17 @@ class LexicalWordGonol: gonol_id: str def __post_init__(self) -> None: - if not self.word or tuple(self.word) != self.glyphs: + _validate_word(self.word) + if tuple(self.word) != self.glyphs: raise LexicalFloorError("word and ordered glyph sequence must match") if self.gonol_id != word_gonol_id(self.word): raise LexicalFloorError("word gonol identity mismatch") def word_gonol_id(word: str) -> str: - if not word or any(not _is_unicode_scalar(glyph) for glyph in word): - raise LexicalFloorError("word identity requires nonempty Unicode scalars") - return f"word-gonol:sha256:{_canonical_digest(tuple(word))}" + _validate_word(word) + evidence = (WORD_ID_POLICY, tuple(ord(glyph) for glyph in word)) + return f"word-gonol:sha256:{_canonical_digest(evidence)}" def define_glyphs(words: Sequence[str]) -> tuple[GlyphDefinition, ...]: @@ -205,7 +597,7 @@ def define_glyphs(words: Sequence[str]) -> tuple[GlyphDefinition, ...]: GlyphDefinition( value=glyph, code_point=f"U+{ord(glyph):04X}", - carrier_position=edcm_carrier_position(glyph), + carrier_position=int(edcm_carrier_position(glyph)), ) for glyph in glyphs ) @@ -257,34 +649,120 @@ def _edit_distance(left: str, right: str) -> int: return previous[-1] +@dataclass(frozen=True, slots=True) +class SharedGlyphOccurrence: + """One exact matching glyph pair with both source occurrence addresses.""" + + glyph: str + left_offset: int + right_offset: int + + def __post_init__(self) -> None: + if not _is_unicode_scalar(self.glyph): + raise LexicalFloorError("shared occurrence requires one Unicode scalar") + if self.left_offset < 0 or self.right_offset < 0: + raise LexicalFloorError("shared occurrence offsets must be nonnegative") + + +@dataclass(frozen=True, slots=True) +class GlyphTypeSetProjection: + """A declared-loss set view over shared occurrence evidence.""" + + glyphs: tuple[str, ...] + identity_policy: str = GLYPH_TYPE_SET_IDENTITY_POLICY + information_loss: tuple[str, ...] = GLYPH_TYPE_SET_INFORMATION_LOSS + standing: str = GLYPH_TYPE_SET_STANDING + + def __post_init__(self) -> None: + if any(not _is_unicode_scalar(glyph) for glyph in self.glyphs): + raise LexicalFloorError("glyph-type projection requires Unicode scalars") + if tuple(sorted(set(self.glyphs), key=ord)) != self.glyphs: + raise LexicalFloorError("glyph-type projection must be unique and code-point ordered") + if self.identity_policy != GLYPH_TYPE_SET_IDENTITY_POLICY: + raise LexicalFloorError("glyph-type set identity policy mismatch") + if self.information_loss != GLYPH_TYPE_SET_INFORMATION_LOSS: + raise LexicalFloorError("glyph-type set information loss cannot be omitted") + if self.standing != GLYPH_TYPE_SET_STANDING: + raise LexicalFloorError("glyph-type set standing cannot be promoted") + + +def _shared_occurrences(left: str, right: str) -> tuple[SharedGlyphOccurrence, ...]: + return tuple( + SharedGlyphOccurrence(glyph, left_offset, right_offset) + for left_offset, glyph in enumerate(left) + for right_offset, right_glyph in enumerate(right) + if glyph == right_glyph + ) + + +def _shared_glyph_type_projection(left: str, right: str) -> GlyphTypeSetProjection: + return GlyphTypeSetProjection(tuple(sorted(set(left) & set(right), key=ord))) + + @dataclass(frozen=True, slots=True) class CharacterRelationship: """An exact orthographic projection between two retained word gonols.""" + left_word: str + right_word: str left_gonol_id: str right_gonol_id: str - shared_glyphs: tuple[str, ...] + shared_occurrences: tuple[SharedGlyphOccurrence, ...] + shared_glyph_type_set: GlyphTypeSetProjection common_prefix_length: int common_suffix_length: int left_contains_right: bool right_contains_left: bool edit_distance: int - standing: str = "character-derived-projection" + standing: str = CHARACTER_RELATIONSHIP_STANDING + + def __post_init__(self) -> None: + _validate_word(self.left_word) + _validate_word(self.right_word) + if self.left_gonol_id != word_gonol_id(self.left_word): + raise LexicalFloorError("left relationship gonol identity mismatch") + if self.right_gonol_id != word_gonol_id(self.right_word): + raise LexicalFloorError("right relationship gonol identity mismatch") + if self.shared_occurrences != _shared_occurrences(self.left_word, self.right_word): + raise LexicalFloorError("shared glyph occurrence evidence mismatch") + if self.shared_glyph_type_set != _shared_glyph_type_projection(self.left_word, self.right_word): + raise LexicalFloorError("shared glyph-type projection mismatch") + if self.common_prefix_length != _common_prefix_length(self.left_word, self.right_word): + raise LexicalFloorError("common prefix evidence mismatch") + if self.common_suffix_length != _common_suffix_length(self.left_word, self.right_word): + raise LexicalFloorError("common suffix evidence mismatch") + if self.left_contains_right is not (self.right_word in self.left_word): + raise LexicalFloorError("left containment evidence mismatch") + if self.right_contains_left is not (self.left_word in self.right_word): + raise LexicalFloorError("right containment evidence mismatch") + if self.edit_distance != _edit_distance(self.left_word, self.right_word): + raise LexicalFloorError("edit-distance evidence mismatch") + if self.standing != CHARACTER_RELATIONSHIP_STANDING: + raise LexicalFloorError("character relationship standing cannot be promoted") @dataclass(frozen=True, slots=True) class LexicalHyperspacePotential: - """A non-geometric potential that can project exact character relations.""" + """An immutable non-geometric potential for exact character projections.""" word_gonols: tuple[LexicalWordGonol, ...] - _by_word: Mapping[str, LexicalWordGonol] + _by_word: Mapping[str, LexicalWordGonol] = field(init=False, repr=False, compare=False) def __post_init__(self) -> None: - if len(self.word_gonols) != len(self._by_word): + ordered = tuple(self.word_gonols) + if not ordered: + raise LexicalFloorError("hyperspace potential requires word gonols") + _validate_words(tuple(gonol.word for gonol in ordered)) + if len({gonol.word for gonol in ordered}) != len(ordered): raise LexicalFloorError("hyperspace words must remain unique") - for gonol in self.word_gonols: - if self._by_word.get(gonol.word) != gonol: - raise LexicalFloorError("hyperspace word index mismatch") + if len({gonol.gonol_id for gonol in ordered}) != len(ordered): + raise LexicalFloorError("hyperspace gonol identities must remain unique") + object.__setattr__(self, "word_gonols", ordered) + object.__setattr__( + self, + "_by_word", + MappingProxyType({gonol.word: gonol for gonol in ordered}), + ) def gonol(self, word: str) -> LexicalWordGonol: try: @@ -298,9 +776,12 @@ def project(self, left_word: str, right_word: str) -> CharacterRelationship: left = self.gonol(left_word) right = self.gonol(right_word) return CharacterRelationship( + left_word=left.word, + right_word=right.word, left_gonol_id=left.gonol_id, right_gonol_id=right.gonol_id, - shared_glyphs=tuple(sorted(set(left.word) & set(right.word), key=ord)), + shared_occurrences=_shared_occurrences(left.word, right.word), + shared_glyph_type_set=_shared_glyph_type_projection(left.word, right.word), common_prefix_length=_common_prefix_length(left.word, right.word), common_suffix_length=_common_suffix_length(left.word, right.word), left_contains_right=right.word in left.word, @@ -312,11 +793,7 @@ def project(self, left_word: str, right_word: str) -> CharacterRelationship: def create_hyperspace_potential( word_gonols: Sequence[LexicalWordGonol], ) -> LexicalHyperspacePotential: - ordered = tuple(word_gonols) - by_word = {gonol.word: gonol for gonol in ordered} - if len(by_word) != len(ordered): - raise LexicalFloorError("one word gonol per exact glyph set is required") - return LexicalHyperspacePotential(ordered, by_word) + return LexicalHyperspacePotential(tuple(word_gonols)) @dataclass(frozen=True, slots=True) @@ -332,17 +809,21 @@ class AffixiationCandidate: standing: str = ORTHOGRAPHIC_CANDIDATE_STANDING def __post_init__(self) -> None: + _validate_word(self.base_word) + _validate_word(self.derived_word) + if self.base_gonol_id != word_gonol_id(self.base_word): + raise LexicalFloorError("affixiation base identity mismatch") + if self.derived_gonol_id != word_gonol_id(self.derived_word): + raise LexicalFloorError("affixiation derived identity mismatch") if not self.affix: raise LexicalFloorError("affixiation candidate requires an affix") if self.side not in {"prefix", "suffix"}: raise LexicalFloorError("affixiation side must be prefix or suffix") - expected = ( - self.affix + self.base_word - if self.side == "prefix" - else self.base_word + self.affix - ) + expected = self.affix + self.base_word if self.side == "prefix" else self.base_word + self.affix if expected != self.derived_word: raise LexicalFloorError("affixiation decomposition mismatch") + if self.standing != ORTHOGRAPHIC_CANDIDATE_STANDING: + raise LexicalFloorError("affixiation standing cannot be promoted") def derive_affixiation_candidates( @@ -405,10 +886,21 @@ class CompoundCandidate: standing: str = ORTHOGRAPHIC_CANDIDATE_STANDING def __post_init__(self) -> None: + _validate_word(self.compound_word) + _validate_word(self.left_word) + _validate_word(self.right_word) + if self.compound_gonol_id != word_gonol_id(self.compound_word): + raise LexicalFloorError("compound identity mismatch") + if self.left_gonol_id != word_gonol_id(self.left_word): + raise LexicalFloorError("left compound identity mismatch") + if self.right_gonol_id != word_gonol_id(self.right_word): + raise LexicalFloorError("right compound identity mismatch") if self.left_word + self.right_word != self.compound_word: raise LexicalFloorError("compound decomposition mismatch") if self.split_offset != len(self.left_word): raise LexicalFloorError("compound split offset mismatch") + if self.standing != ORTHOGRAPHIC_CANDIDATE_STANDING: + raise LexicalFloorError("compound standing cannot be promoted") def derive_compound_candidates( @@ -450,7 +942,8 @@ class DefinitionSense: def __post_init__(self) -> None: if not all( - ( + isinstance(value, str) and value + for value in ( self.word_gonol_id, self.context_identity, self.definition, @@ -460,6 +953,13 @@ def __post_init__(self) -> None: raise LexicalFloorError( "definition senses require gonol, context, text, and source identity" ) + prefix = "word-gonol:sha256:" + if not self.word_gonol_id.startswith(prefix) or not _HEX_64.fullmatch( + self.word_gonol_id.removeprefix(prefix) + ): + raise LexicalFloorError("definition must reference a complete word-gonol identity") + if self.standing != DEFINITION_STANDING: + raise LexicalFloorError("definition standing cannot be promoted") def create_definition_layer( @@ -481,29 +981,97 @@ def create_definition_layer( raise LexicalFloorError("duplicate contextual definition identity") seen.add(identity) grouped.setdefault(sense.word_gonol_id, []).append(sense) - return { + immutable = { gonol_id: tuple(values) for gonol_id, values in sorted(grouped.items()) } + return MappingProxyType(immutable) @dataclass(frozen=True, slots=True) class LexicalLayerSnapshot: - """One immutable digest boundary in the append-only lexical layer sequence.""" + """One immutable, source-bound digest boundary in the lexical layer chain.""" + layer_index: int layer_id: str + source_receipt_id: str parent_snapshot_id: str | None item_count: int content_digest: str standing: str hmmm: str + producer_id: str = LEXICAL_PRODUCER_ID + + def __post_init__(self) -> None: + if not 0 <= self.layer_index < len(_LAYER_SPECS): + raise LexicalFloorError("snapshot layer index is outside the declared sequence") + expected_id, expected_standing, expected_hmmm = _LAYER_SPECS[self.layer_index] + if self.layer_id != expected_id: + raise LexicalFloorError("snapshot layer identity mismatch") + if self.standing != expected_standing: + raise LexicalFloorError("snapshot standing cannot be promoted") + if self.hmmm != expected_hmmm: + raise LexicalFloorError("snapshot unresolved boundary cannot be erased or replaced") + if not self.source_receipt_id.startswith(LEXICAL_SOURCE_RECEIPT_PREFIX): + raise LexicalFloorError("snapshot source receipt identity mismatch") + if not _HEX_64.fullmatch(self.source_receipt_id.removeprefix(LEXICAL_SOURCE_RECEIPT_PREFIX)): + raise LexicalFloorError("snapshot source receipt digest is malformed") + if self.layer_index == 0: + if self.parent_snapshot_id is not None: + raise LexicalFloorError("root lexical snapshot cannot have a parent") + else: + previous_id = _LAYER_SPECS[self.layer_index - 1][0] + expected_prefix = f"{previous_id}:sha256:" + if not isinstance(self.parent_snapshot_id, str) or not self.parent_snapshot_id.startswith(expected_prefix): + raise LexicalFloorError("snapshot parent layer identity mismatch") + if not _HEX_64.fullmatch(self.parent_snapshot_id.removeprefix(expected_prefix)): + raise LexicalFloorError("snapshot parent digest is malformed") + if self.item_count < 0: + raise LexicalFloorError("snapshot item count must be nonnegative") + if not _HEX_64.fullmatch(self.content_digest): + raise LexicalFloorError("snapshot content digest must be lowercase SHA-256") + if self.producer_id != LEXICAL_PRODUCER_ID: + raise LexicalFloorError("snapshot producer mismatch") @property def snapshot_id(self) -> str: - return ( - f"{self.layer_id}:sha256:" - f"{_canonical_digest((self.parent_snapshot_id, self.item_count, self.content_digest, self.standing, self.hmmm))}" + evidence = ( + self.producer_id, + self.layer_index, + self.layer_id, + self.source_receipt_id, + self.parent_snapshot_id, + self.item_count, + self.content_digest, + self.standing, + self.hmmm, ) + return f"{self.layer_id}:sha256:{_canonical_digest(evidence)}" + + +def validate_snapshot_chain( + snapshots: Sequence[LexicalLayerSnapshot], + source_receipt: LexicalSourceReceipt, +) -> tuple[LexicalLayerSnapshot, ...]: + """Fail closed unless the complete current source-bound layer chain is intact.""" + + chain = tuple(snapshots) + if len(chain) != len(_LAYER_SPECS): + raise LexicalFloorError("snapshot chain must contain every declared lexical layer") + parent: str | None = None + seen_ids: set[str] = set() + for index, snapshot in enumerate(chain): + if snapshot.layer_index != index: + raise LexicalFloorError("snapshot chain order mismatch") + if snapshot.source_receipt_id != source_receipt.receipt_id: + raise LexicalFloorError("snapshot chain source receipt mismatch") + if snapshot.parent_snapshot_id != parent: + raise LexicalFloorError("snapshot chain parent mismatch") + if snapshot.snapshot_id in seen_ids: + raise LexicalFloorError("snapshot identities must remain unique") + seen_ids.add(snapshot.snapshot_id) + parent = snapshot.snapshot_id + return chain def snapshot_layers( @@ -511,11 +1079,18 @@ def snapshot_layers( *, definitions: Iterable[DefinitionSense] = (), ) -> tuple[LexicalLayerSnapshot, ...]: - """Materialize a snapshot after every currently declared layer.""" + """Materialize the complete source-bound snapshot sequence.""" - _validate_words(words) - glyphs = define_glyphs(words) - gonols = create_word_gonols(words) + ordered_words = tuple(words) + _validate_words(ordered_words) + source_receipt, packaged_words = _load_source_bundle() + if ordered_words != packaged_words: + raise LexicalFloorError( + "snapshot source does not match the exact packaged source receipt" + ) + + glyphs = define_glyphs(ordered_words) + gonols = create_word_gonols(ordered_words) potential = create_hyperspace_potential(gonols) affixiation = derive_affixiation_candidates(potential) compounds = derive_compound_candidates(potential) @@ -526,73 +1101,39 @@ def snapshot_layers( for sense in senses ) - payloads: tuple[ - tuple[str, object, int, str, str], - ..., - ] = ( - ( - "00-words", - tuple(words), - len(words), - "source-admitted-word-only", - "Official-source checksum reconciliation remains required before canonical promotion.", - ), + payloads: tuple[tuple[object, int], ...] = ( + ((source_receipt, ordered_words), len(ordered_words)), + (glyphs, len(glyphs)), + (gonols, len(gonols)), ( - "01-glyphs", - glyphs, - len(glyphs), - "existing-glyph-canon-applied", - "No new glyph law is asserted by this lexical floor.", - ), - ( - "02-word-gonols", - gonols, - len(gonols), - "implemented-exact-word-gonols", - "The source-to-hyperdimensional-coordinate law remains unresolved.", - ), - ( - "03-character-hyperspace-potential", - (("projection-schema", "character-relationship-v1"), gonols), + ( + ("projection-schema", "character-relationship-occurrence-v2"), + ("glyph-type-set-policy", GLYPH_TYPE_SET_IDENTITY_POLICY), + ("glyph-type-set-loss", GLYPH_TYPE_SET_INFORMATION_LOSS), + gonols, + ), len(gonols), - "implemented-on-demand-character-projection", - "Projection capability is not a geometric embedding or semantic relation.", - ), - ( - "04-affixiation", - affixiation, - len(affixiation), - ORTHOGRAPHIC_CANDIDATE_STANDING, - "Linguistic affix authority and allomorphy remain unadjudicated.", - ), - ( - "05-compounding", - compounds, - len(compounds), - ORTHOGRAPHIC_CANDIDATE_STANDING, - "Orthographic decomposition is not yet an attested compound judgment.", - ), - ( - "06-definitions", - flattened_definitions, - len(flattened_definitions), - DEFINITION_STANDING, - "Definition custody, sense boundaries, and context corpus admission remain open.", ), + (affixiation, len(affixiation)), + (compounds, len(compounds)), + (flattened_definitions, len(flattened_definitions)), ) snapshots: list[LexicalLayerSnapshot] = [] parent: str | None = None - for layer_id, payload, item_count, standing, hmmm in payloads: - digest = _canonical_digest(payload) + for layer_index, ((layer_id, standing, hmmm), (payload, item_count)) in enumerate( + zip(_LAYER_SPECS, payloads, strict=True) + ): snapshot = LexicalLayerSnapshot( + layer_index=layer_index, layer_id=layer_id, + source_receipt_id=source_receipt.receipt_id, parent_snapshot_id=parent, item_count=item_count, - content_digest=digest, + content_digest=_canonical_digest(payload), standing=standing, hmmm=hmmm, ) snapshots.append(snapshot) parent = snapshot.snapshot_id - return tuple(snapshots) + return validate_snapshot_chain(snapshots, source_receipt) diff --git a/tests/test_lexical_floor.py b/tests/test_lexical_floor.py index 05d9a0d6..8edf2e93 100644 --- a/tests/test_lexical_floor.py +++ b/tests/test_lexical_floor.py @@ -1,175 +1,72 @@ # === CHECKS === -# id: lexical_floor_exact_collection_check -# resolves: lexical_floor_words_are_unique_exact_glyph_sets, lexical_floor_order_is_serialization_only -# test: test_ngsl_word_only_collection_is_exact_unique_and_reproducible -# class: correctness -# since: 2026-08-04 -# -# id: lexical_floor_glyph_and_gonol_check -# resolves: lexical_floor_reuses_canonical_glyph_assignment -# test: test_existing_glyph_canon_builds_one_gonol_per_word -# class: evidence -# since: 2026-08-04 -# -# id: lexical_floor_projection_check -# resolves: lexical_hyperspace_is_projection_not_embedding -# test: test_hyperspace_projects_character_relations_without_embedding_claim -# class: safety -# since: 2026-08-04 -# -# id: lexical_floor_candidate_layer_check -# resolves: affixiation_and_compounding_are_candidate_layers -# test: test_affixiation_and_compounding_begin_as_orthographic_candidates -# class: doctrine -# since: 2026-08-04 -# -# id: lexical_floor_definition_plurality_check -# resolves: definitions_are_context_plural -# test: test_one_word_gonol_accepts_many_context_sourced_definitions -# class: evidence -# since: 2026-08-04 -# -# id: lexical_floor_snapshot_check -# resolves: every_added_layer_has_a_snapshot -# test: test_each_added_layer_has_a_parented_snapshot -# class: correctness -# since: 2026-08-04 +# id: lexical_floor_source_and_word_check +# proves: lexical_floor_source_receipt_binds_packaged_bytes, lexical_floor_words_are_unique_exact_glyph_sets, lexical_floor_order_is_serialization_only, lexical_floor_reuses_canonical_glyph_assignment +# call: self::test_source_word_and_glyph_boundaries +# mutates: none +# cleanup: none # === END CHECKS === from dataclasses import replace +from importlib.resources import files +import json import pytest from ucns.lexical_floor import ( - DEFINITION_STANDING, + LEXICAL_SOURCE_STANDING, + NGSL_ATTRIBUTION_RESOURCE, + NGSL_SOURCE_RESOURCE, NGSL_WORD_COUNT, - ORTHOGRAPHIC_CANDIDATE_STANDING, - DefinitionSense, + NGSL_WORD_RESOURCE, LexicalFloorError, - create_definition_layer, + LexicalWordGonol, + _parse_source_bundle, create_hyperspace_potential, create_word_gonols, define_glyphs, - derive_affixiation_candidates, - derive_compound_candidates, + load_ngsl_source_receipt, load_ngsl_words, - snapshot_layers, + word_gonol_id, ) -def _floor(): +def test_source_word_and_glyph_boundaries(): + receipt = load_ngsl_source_receipt() words = load_ngsl_words() gonols = create_word_gonols(words) - return words, gonols, create_hyperspace_potential(gonols) - - -def test_ngsl_word_only_collection_is_exact_unique_and_reproducible(): - words = load_ngsl_words() - - assert len(words) == NGSL_WORD_COUNT == 2809 - assert len(set(words)) == len(words) - assert all(word and not any(glyph.isspace() for glyph in word) for word in words) - assert words[0] == "a" - assert words[-1] == "zone" - assert "I" in words - assert not any("," in word for word in words) - - -def test_existing_glyph_canon_builds_one_gonol_per_word(): - words, gonols, _ = _floor() + potential = create_hyperspace_potential(gonols) glyphs = define_glyphs(words) - - assert len(glyphs) == 27 - assert {glyph.value for glyph in glyphs} == set("abcdefghijklmnopqrstuvwxyzI") - assert all(glyph.carrier_position is not None for glyph in glyphs) - assert len(gonols) == len(words) - assert len({gonol.gonol_id for gonol in gonols}) == len(words) - assert all(gonol.glyphs == tuple(gonol.word) for gonol in gonols) - - -def test_hyperspace_projects_character_relations_without_embedding_claim(): - _, _, potential = _floor() - - relation = potential.project("act", "action") - - assert relation.common_prefix_length == 3 - assert relation.right_contains_left is True - assert relation.left_contains_right is False - assert relation.edit_distance == 3 - assert relation.standing == "character-derived-projection" - - -def test_affixiation_and_compounding_begin_as_orthographic_candidates(): - _, _, potential = _floor() - - affixiation = derive_affixiation_candidates(potential) - compounds = derive_compound_candidates(potential) - - assert any( - item.base_word == "act" - and item.derived_word == "action" - and item.affix == "ion" - and item.side == "suffix" - and item.standing == ORTHOGRAPHIC_CANDIDATE_STANDING - for item in affixiation - ) - assert any( - item.compound_word == "background" - and item.left_word == "back" - and item.right_word == "ground" - and item.standing == ORTHOGRAPHIC_CANDIDATE_STANDING - for item in compounds - ) - - -def test_one_word_gonol_accepts_many_context_sourced_definitions(): - _, _, potential = _floor() - word = potential.gonol("bank") - river = DefinitionSense( - word_gonol_id=word.gonol_id, - context_identity="context:river-bank", - definition="The land beside a river.", - source_identity="source:test-contexts-v1", - ) - finance = DefinitionSense( - word_gonol_id=word.gonol_id, - context_identity="context:financial-bank", - definition="An institution that holds and lends money.", - source_identity="source:test-contexts-v1", - ) - - layer = create_definition_layer(potential, (river, finance)) - - assert tuple(layer) == (word.gonol_id,) - assert layer[word.gonol_id] == (river, finance) - assert all(item.standing == DEFINITION_STANDING for item in layer[word.gonol_id]) - with pytest.raises(LexicalFloorError, match="duplicate contextual"): - create_definition_layer(potential, (river, replace(river))) - - -def test_each_added_layer_has_a_parented_snapshot(): - words = load_ngsl_words() - snapshots = snapshot_layers(words) - expected_layers = ( - "00-words", - "01-glyphs", - "02-word-gonols", - "03-character-hyperspace-potential", - "04-affixiation", - "05-compounding", - "06-definitions", - ) - - assert tuple(snapshot.layer_id for snapshot in snapshots) == expected_layers - assert snapshots[0].parent_snapshot_id is None - assert all( - current.parent_snapshot_id == previous.snapshot_id - for previous, current in zip(snapshots, snapshots[1:]) - ) - assert snapshots[0].item_count == 2809 - assert snapshots[1].item_count == 27 - assert snapshots[2].item_count == 2809 - assert snapshots[3].item_count == 2809 - assert snapshots[-1].item_count == 0 - assert all(len(snapshot.content_digest) == 64 for snapshot in snapshots) - assert all(snapshot.hmmm for snapshot in snapshots) + assert potential.gonol("bank").word == "bank" + assert receipt.word_count == len(words) == NGSL_WORD_COUNT == 2809 + assert receipt.standing == LEXICAL_SOURCE_STANDING + assert receipt.receipt_id.startswith("ucns.lexical-source-receipt:sha256:") + assert all(len(value) == 64 for value in ( + receipt.metadata_sha256, receipt.word_file_sha256, + receipt.word_sequence_sha256, receipt.attribution_sha256, + )) + assert len(set(words)) == len(words) and words[0] == "a" and words[-1] == "zone" and "I" in words + assert len(glyphs) == 27 and all(glyph.carrier_position > 0 for glyph in glyphs) + assert len(gonols) == len({gonol.gonol_id for gonol in gonols}) == len(words) + + package = files("ucns") + metadata = package.joinpath(NGSL_SOURCE_RESOURCE).read_bytes() + word_bytes = package.joinpath(NGSL_WORD_RESOURCE).read_bytes() + attribution = package.joinpath(NGSL_ATTRIBUTION_RESOURCE).read_bytes() + changed = json.loads(metadata.decode()) + changed["acquisition"]["target_git_blob"] = "0" * 40 + with pytest.raises(LexicalFloorError, match="declared Git blob"): + _parse_source_bundle((json.dumps(changed) + "\n").encode(), word_bytes, attribution) + with pytest.raises(LexicalFloorError, match="declared digest"): + _parse_source_bundle(metadata, word_bytes, attribution + b"tamper\n") + with pytest.raises(LexicalFloorError, match="standing"): + replace(receipt, standing="canonical") + + for invalid in ("", "a b", "a\tb", "a\u00a0b", "a🙂"): + with pytest.raises(LexicalFloorError): + word_gonol_id(invalid) + with pytest.raises(LexicalFloorError, match="SPACE"): + LexicalWordGonol("a b", ("a", " ", "b"), "word-gonol:sha256:" + "0" * 64) + with pytest.raises(LexicalFloorError, match="serialization"): + create_word_gonols(("b", "a")) + with pytest.raises(LexicalFloorError, match="duplicate"): + create_word_gonols(("a", "a")) diff --git a/tests/test_lexical_floor_layers.py b/tests/test_lexical_floor_layers.py new file mode 100644 index 00000000..b1555f8b --- /dev/null +++ b/tests/test_lexical_floor_layers.py @@ -0,0 +1,71 @@ +# === CHECKS === +# id: lexical_floor_layer_check +# proves: lexical_hyperspace_is_occurrence_preserving_projection_not_embedding, affixiation_and_compounding_are_candidate_layers, definitions_are_context_plural_and_immutable +# call: self::test_projection_candidate_and_definition_boundaries +# mutates: none +# cleanup: none +# === END CHECKS === + +from dataclasses import replace + +import pytest + +from ucns.lexical_floor import ( + CHARACTER_RELATIONSHIP_STANDING, + DEFINITION_STANDING, + GLYPH_TYPE_SET_IDENTITY_POLICY, + GLYPH_TYPE_SET_INFORMATION_LOSS, + ORTHOGRAPHIC_CANDIDATE_STANDING, + DefinitionSense, + LexicalFloorError, + SharedGlyphOccurrence, + create_definition_layer, + create_hyperspace_potential, + create_word_gonols, + derive_affixiation_candidates, + derive_compound_candidates, + load_ngsl_words, +) + + +def test_projection_candidate_and_definition_boundaries(): + potential = create_hyperspace_potential(create_word_gonols(load_ngsl_words())) + relation = potential.project("all", "ball") + assert relation.shared_occurrences == ( + SharedGlyphOccurrence("a", 0, 1), + SharedGlyphOccurrence("l", 1, 2), SharedGlyphOccurrence("l", 1, 3), + SharedGlyphOccurrence("l", 2, 2), SharedGlyphOccurrence("l", 2, 3), + ) + assert relation.shared_glyph_type_set.glyphs == ("a", "l") + assert relation.shared_glyph_type_set.identity_policy == GLYPH_TYPE_SET_IDENTITY_POLICY + assert relation.shared_glyph_type_set.information_loss == GLYPH_TYPE_SET_INFORMATION_LOSS + assert relation.standing == CHARACTER_RELATIONSHIP_STANDING + with pytest.raises(TypeError): + potential._by_word["forged"] = potential.gonol("all") + with pytest.raises(LexicalFloorError, match="standing"): + replace(relation, standing="canonical") + with pytest.raises(LexicalFloorError, match="occurrence"): + replace(relation, shared_occurrences=relation.shared_occurrences[:-1]) + + action = next(item for item in derive_affixiation_candidates(potential) + if item.base_word == "act" and item.derived_word == "action" and item.affix == "ion") + background = next(item for item in derive_compound_candidates(potential) + if item.compound_word == "background" and item.left_word == "back" and item.right_word == "ground") + assert action.standing == background.standing == ORTHOGRAPHIC_CANDIDATE_STANDING + with pytest.raises(LexicalFloorError, match="base identity"): + replace(action, base_gonol_id="word-gonol:sha256:" + "0" * 64) + with pytest.raises(LexicalFloorError, match="standing"): + replace(background, standing="attested") + + word = potential.gonol("bank") + river = DefinitionSense(word.gonol_id, "context:river", "Land beside a river.", "source:test") + finance = DefinitionSense(word.gonol_id, "context:finance", "An institution holding money.", "source:test") + layer = create_definition_layer(potential, (river, finance)) + assert layer[word.gonol_id] == (river, finance) + assert all(item.standing == DEFINITION_STANDING for item in layer[word.gonol_id]) + with pytest.raises(TypeError): + layer[word.gonol_id] = () + with pytest.raises(LexicalFloorError, match="duplicate contextual"): + create_definition_layer(potential, (river, replace(river))) + with pytest.raises(LexicalFloorError, match="standing"): + replace(river, standing="canonical") diff --git a/tests/test_lexical_floor_snapshots.py b/tests/test_lexical_floor_snapshots.py new file mode 100644 index 00000000..4fa68314 --- /dev/null +++ b/tests/test_lexical_floor_snapshots.py @@ -0,0 +1,50 @@ +# === CHECKS === +# id: lexical_floor_snapshot_check +# proves: every_added_layer_has_a_source_bound_snapshot +# call: self::test_snapshot_chain_is_source_bound_and_fail_closed +# mutates: none +# cleanup: none +# === END CHECKS === + +from dataclasses import replace + +import pytest + +from ucns.lexical_floor import ( + LexicalFloorError, + load_ngsl_source_receipt, + load_ngsl_words, + snapshot_layers, + validate_snapshot_chain, +) + + +def test_snapshot_chain_is_source_bound_and_fail_closed(): + words = load_ngsl_words() + receipt = load_ngsl_source_receipt() + snapshots = snapshot_layers(words) + assert tuple(snapshot.layer_id for snapshot in snapshots) == ( + "00-words", "01-glyphs", "02-word-gonols", + "03-character-hyperspace-potential", "04-affixiation", + "05-compounding", "06-definitions", + ) + assert snapshots[0].parent_snapshot_id is None + assert all(current.parent_snapshot_id == previous.snapshot_id + for previous, current in zip(snapshots, snapshots[1:])) + assert all(snapshot.source_receipt_id == receipt.receipt_id for snapshot in snapshots) + assert (snapshots[0].item_count, snapshots[1].item_count, + snapshots[2].item_count, snapshots[3].item_count, + snapshots[-1].item_count) == (2809, 27, 2809, 2809, 0) + assert validate_snapshot_chain(snapshots, receipt) == snapshots + + changed = list(words) + changed[1] = "ab" + with pytest.raises(LexicalFloorError, match="exact packaged source"): + snapshot_layers(changed) + forged = replace(snapshots[1], parent_snapshot_id="00-words:sha256:" + "0" * 64) + with pytest.raises(LexicalFloorError, match="parent mismatch"): + validate_snapshot_chain((snapshots[0], forged, *snapshots[2:]), receipt) + with pytest.raises(LexicalFloorError, match="standing"): + replace(snapshots[4], standing="attested") + with pytest.raises(LexicalFloorError, match="unresolved boundary"): + replace(snapshots[0], hmmm="resolved")