Changelog¶
All notable changes to Orphan Parts Lens. Versions follow SemVer.
Unreleased¶
Added¶
- Search the web for a part, beside the Marketplace links in Plan the search: one Muse Spark call with web search (
POST /api/searches/{id}/web, operationpart-search, at mostPART_MAX_SEARCHESsearches, default 4, 1 to 10), only on a click. The brief is the plan's (never a serial number), the owner'sparent_devicewords included as the search notes are, led by the device the part came from when it is recorded (the maker's own part for that device first). A page the model names is kept only when a search result or a citation of the same answer carries it, query and all (a fragment andutm_*parameters let go), it is a web link and it has text to score, under the URL the search returned (price guidance now matches pages the same way); each kept page (ten at most, one per page, not again when already listed) becomes a listing with its link, scored like a pasted one and read by Check fit the same way, the candidate keeping the link. Counted against the allowances like the plan; the demo visitor does not reach Find a part, and a session that ends meanwhile shows Sign in with the access code to search the web. Marketplace is still not searched. Thepart-searchfixture is a stub onexample.org, not a recording. - The app asks which device a part came from when it cannot tell: an item with a part profile (
part_of_device: true, set forblender_agitatorandvacuum_attachment), no model or part number of its own (a part number counts whatever the profile's rules), no parent item and no device recorded is served withneeds_parent: true, and the item dialog and the saved Read a label result show We couldn't tell which device this part came from. Tell us more about it, or add a photo of that device (the label under its base is best). with Device it came from (saved asparent_device, the owner's words: never sent to the demo visitor and kept out of every brief but the part's own searches) and Add a photo of the device (POST /api/items/{id}/parent-photo: one label reading, the brand and model proposed asparent_device_model, sourcephoto; the photo kept as a device photo apart from the part's own,GET /api/items/{id}/device-photos(/{n}), owner only, outsideMAX_PHOTOS, drafts and the photo zip). Once answered the dialog says Came from: ..., with the device photos and, while the read model is to confirm, Read from the photo: .... The demo visitor sees the question without inputs. The demo set's frappé maker agitator is answered by the Hamilton Beach drink mixer it came off (model 729, type DM01): three new photos underdemo/photos(parent_photos), their label read recorded, kept as the agitator's device photos.
0.3.1 - 2026-10-31¶
Changed¶
GET /api/version, and with it the footer of every page, lists only the models the app calls (Muse Spark, Muse Image, Muse Voice Transcribe, SAM 3.1); Muse Wonder stays a setting checked by./dev check-apiand is no longer named there.- The judges' guide (
JUDGES.md, For judges on the docs site) starts with a fast path on the hosted app, and every docs page describes the current app; the pairing screenshots are regenerated. - The demo visitor reads the demo set's own photos instead of uploading: the web app shows no file input, drop zone or record button without the access code. Read a label is a gallery of the set's 19 photos grouped by item, with a checkbox on each, Select all photos of this item and Read the label; an item's every photo picked is one reading of them all, any other photo is read alone, and the readings are shown side by side, each under Recorded answer for this sample. Measure in the item dialog marks a sample photo; the Inbox's capture form, the listing's voice note and Find a part show Sign in with the access code to use this. On the server
/api/extract,/api/segmentand/api/measurerefuse a file from the visitor with 403 (Sign in with the access code to read your own photos.) and take sample ids instead (sample_ids,sample_id); the stored file the id names runs the normal flow, so the recording of that exact photo answers, and a sample with no recording says No recorded answer for this sample yet. instead of an operation's stub. Signed in, uploads are read live as before. Until the browser has heard fromGET /api/access(or while that check fails), every upload surface shows the samples or the line, never a file input; the inputs appear once the answer says signed in, or an open server. A visitor's reading of several samples costs one try per reading, and a recorded reading is kept in memory for the same pick again. The Modal app setsDEMO_SET=/root/demo/set.yaml, where its image ships the set, so the hosted gallery is filled; Pillow is pinned to 11.3.0, the version the recordings were made with, since the recordings are keyed on the bytes it writes. - The demo set is built from the owner's own photos: three monitors (Dell U2419H, Dell 1800FP, a Sun flat panel), a ThinkPad T510, an older ThinkPad (type 9461), a spare laptop board, an EVGA GeForce 9800 GT, an Intel slot 1 processor, a TP-Link TL-PA2010 powerline adapter and the frappé maker's worn agitator, 19 photos under
demo/photoswith serial numbers, MAC addresses and keys masked and no metadata, and every model answer for them recorded, each photo's own segmentation and read included, so the hosted demo shows real items at no cost and./dev evalscores 14 of 14 expected values with no stub answers. The agitator search brings back four written listings: one documented candidate, two rejected on a rule, one that needs evidence../dev record-fixturestakes the key from the shell as well as from.env, and a recording names the digest of its requestdigestrather thankey, which secret scanners read as a credential (recordings that saykeystill load); the model's encrypted reasoning is kept empty in a recording and left out of its key. - The item dialog is laid out in sections: the facts (photos, intent, availability, profile, the values to confirm, details, a separated part), Decide, what it is worth, prepare to sell or give away, measure and fit, and the picture last, each a card with its title; a bar of section links under the name stays in view and scrolls to each. The photos are a row of thumbnails in the flow instead of a strip that covered the fields under it; rows of fields are a grid with the label above each input and the button on their line; checkboxes and radios sit on the line with their text, the mode as one line (Mode: Sell / Give away); the demo switch, History and Delete (outlined in red, at the right) are the footer. The picture section no longer shows a field and a disabled button while pictures are off on the server, only a line saying so; when on, the as a... field is empty with an example use as its placeholder and a line under it says the drawing is an illustration, labeled as drawn, never evidence; the Decide row's link reads See picture controls.
- The forms on the pages share the item dialog's layout: the Inbox's Send a capture from this computer, Read a label and its save, and Build's preferences, proposal and outcome forms put the label above each control in a grid that drops to one column on a phone, show a file input as its button and file name with a help line under it (types, size), mark optional fields with a muted (optional), and put the submit button on its own row at the left, with one gap between fields and a wider one between groups.
- The phone guide, step 1 of the README and the docs home, and the Android guide say the app is not on Google Play: the APK comes from the releases, signed with the build's development key, so Android asks to allow installs from an unknown source (per app under Install unknown apps on Android 8 and newer), a Play Protect scan from the Play Store checks it once installed, and an earlier release is uninstalled before the next one, since each release carries a fresh key.
Added¶
- The demo samples:
GET /api/sampleslists the set's photos by item with its listings and voice notes,GET /api/samples/{id}/fileand/thumbserve them, andPOST /api/samples/readreads a selection (one result per item group or photo); all open to the demo visitor, read fromDEMO_SET(demo/set.yaml, shipped in the dev stack and the Modal image)../dev record-fixturesnow also records every reading a sample click can ask for (each photo alone, each item's photos together, with the label found first and without), replaying what is already kept;python -m app.demo sampleschecks in fixture mode that each one has its recording. - A demo set item may name a
picture:(a template id or a few words), drawn by Muse Image after its drafts; four items do. A live load draws only withPICTURE_ENABLED=1, which./dev record-fixturessets for its run; fixture mode shows a drawing only once a record run has kept it and says not recorded yet until then, never the stub image as a drawing. One./dev record-fixturesrun (about $0.04) adds the four. - Facebook Marketplace (US) listings, prepared and handed off: Listing details on the item (condition in the owner's words, what is included, faults, what was not tested, whether data was wiped, pickup or shipping, a city, ZIP or neighborhood, asking and lowest price, reason, availability; versioned, the lowest price and the reason never in the listing, a street address, phone or email refused); the draft is one Muse Spark call in Marketplace's shape (title up to 100 characters, price in whole dollars from the asking price or the named price guidance figure, never
0for a sale,0for a give-away under Free Stuff, a category and condition checked againstbackend/app/listing/marketplace_us.yaml, the description with the disclosures, up to 20 tags, the photo order with a reason each); the handoff panel copies each field, downloads the photos in order as a zip without metadata and opens the create page; Mark as listed, Mark as sold (also a recorded sale for price guidance) and Mark as given keep what happened (a sale's event, reported sale and gone mark in one write; nothing follows a sale or a gift, so a sale is never counted twice); the checklist asks to confirm Marketplace's Commerce Policies allow the item (not recalled, not a medical device, nothing that unlocks paid streaming); none of the listing routes is open to the demo visitor; a serial number, MAC address or IMEI never reaches the draft call, and one the model reads off a photo is taken out of the title, the description and the tags. No public API posts a personal listing and automating the site is against its terms, so posting stays with the owner (docs/guides/seller.md). - Draw it for a finished project, on each Reuse it scenario in the decision view and on each Build project's panel: Muse Image draws how the finished thing could look, built from the items the card uses, one image call per click (Draw how the finished project could look. One image call.); nothing is drawn when a card appears. The prompt is written by the rules from the title, one line on what it is and each item's kind with up to three accepted values, never an identifying value (serial, MAC, IMEI), a price, a name, the location or the notes, every text capped. The pictures are kept with the card (
DATA_DIR/items/<id>/scenarios-<sid>-reuse-<k>/, gone with the item) or the project (DATA_DIR/projects/<id>/, cleared by./dev demo), captioned as drawn, and served byPOST/GET /api/items/{id}/scenarios/{sid}/reuse/{k}/picture(s)and/api/projects/{id}/picture(s). The draw routes count as calls that spend and are refused to the demo visitor (whose button is disabled with Sign in with the access code to draw.); withPICTURE_ENABLEDoff they answer503and the card shows Pictures are off on this server. instead of the button. - Plan the search for a missing item, on Find a part and in an item's Measure and fit section: one Muse Spark call (
POST /api/searches/{id}/plan, operationsearch-plan) writes three to six phrasings, each a Marketplace search link with the top price that the owner opens in a new tab, one match pattern over listing text and the exclusion words. Marketplace has no public search API and its search box takes no regular expressions, so the app does not search it: the pattern runs on RE2 (google-re2), linear in the text, so no pattern can hold the backend; it is checked for the owner's sake (at most 300 characters, compiles under RE2, no lookarounds, backreferences,\C, comments or inline flags other than(?i), counted repeats expanding to at most 400, never matching empty text), can be edited and tried on a line, and ranks the listings brought back, several pasted at once or as screenshots (one allowance hit each), as fits, maybe or does not fit with the matched span highlighted, free of model calls; a listing is kept to its first 4,000 characters and a search to 50 listings, the oldest unchecked dropped first; the asking price is the first one stated (several amounts either side of the limit leave it a maybe), withkfor thousands, free only when no amount is given,CA$not read as US dollars and$1 200not guessed at; Check fit reads one listing into a candidate with the seller questions, once even when asked twice at once; Plan again keeps the pattern and words the owner saved and offers the new pattern beside them. Seedocs/guides/search.md. - A Profile page in the web header and What should I do with it? on the item's Decide section. The profile rates skills none, some, confident or pro (ten listed, from software and electronics to plumbing and home repair, plus your own) beside the tools, effort, opening devices, budget, currency and radius; it is the preferences record, whose
skillsare now{skill, level}and whose plain names, stored or sent, read as some. The button asks Muse Spark once (POST /api/items/{id}/scenarios, low effort, refused to the demo visitor) for three cards written for that profile: keep and fix (likely problems, steps at your level, difficulty for you and why, parts, cost, time, or not worth fixing), reuse (two or three, naming what else you own) and sell or give away (with the last price guidance or a button to ask for one), and one recommendation with its reason; without a profile they are written for an average person and say so. Choose this records the choice and moves the focus to where it leads (keep_fixis new; it adds the repair steps to the item's notes while the item is as the cards saw it, whole steps left out when the notes would overflow, and on stale cards the button says it comes without them; a decision does not make the cards or the price guidance stale, and once the steps are in the card says so), and the cards are kept on the item, stale once it changes; a reuse card names the kinds you own, without the counts the brief gave. Preferences written by the demo loader do not count as a profile. - The hosted deploy writes
PICTURE_ENABLEDinto the Modal secret from the repository variable of that name (PICTURE_ENABLED_HOSTEDfor./dev deploy hosted), so the hosted demo can draw pictures with Muse Image; it defaults to0and a value other than0or1is refused before anything is written. ./dev live-check: one real Model API call per operation the product makes (segment,transcribe,extract,listing-text,draft,uses,propose,price, andpicturewhen named), through the product's own services on one item from the demo set, each line with the model that answered, PASS or FAIL, milliseconds, tokens and cost; a live run spends real credit, held under--max-usd(default 0.25; an operation whose ceiling, retries counted, is alone above it is named and skipped up front) and refused when the spend ledger has less headroom; it never records fixtures,OPL_FAKE_API=1(or true/yes/on) replays the recordings for nothing, and a value that is neither on nor off is refused.--against local|hosted|URLruns the proof over HTTP against a running backend: behind a gate, an extract without the code is answered from the recordings and a write is refused with 403, then a session withLIVECHECK_ACCESS_CODE(from the environment, never an argument;./devpasses it and the key on by name and reads neither) reads the live models and signs out. The check lives inlivecheck/at the repository root, mounted read-only into the development backend container and in no image or deploy;backend/tests/test_livecheck.pypins the Modal app, the build contexts, the Dockerfiles and the production volumes and runs the dry run with a placeholder key that must not appear in the output,scripts/tests/test_livecheck.shscans the folder for anything that reads like a credential, runs gitleaks over it when installed and allows only tracked Python and Markdown in it.- A diagram of how Orphan Parts Lens works (
docs/assets/process.svg, drawn as vector text and shapes): capture, the reading and the four paths with their outcomes, the preferences that shape proposals, and the private inventory; shown under the intro of the README and the docs home. - Sign out in the web header while signed in on a gated host:
DELETE /api/accessclears the session cookie with the attributes it was set with (HttpOnly,SameSite=Strict,Securewhen the cookie is), the web app drops the token it keeps across origins even when the call fails, and the page loads again as the demo visitor, the footer's pairing code with it. - A pictured guide to pairing the phone with the web app (
docs/start/pairing.md): sign in, the footer's pairing code signed in and signed out, the scan, the typed address, a photo sent to the Inbox, and what a visitor gets with the code and without it; linked from the judges' guide, the README and the docs home. - Unpair on the app's Backend tab, shown while the phone holds an address, a code or a session: after a confirmation that names the phone's own code by its label and where it is revoked, the app asks the backend to forget the session (
DELETE /api/access, a short wait, any answer), empties the address, the code, its label and expiry and the token on disk, cancels a claim still running and shows the empty form. The phone's code stays issued until./dev keys revoke "phone …"on the server ends it.
Fixed¶
- A call that spends is counted, limited and paused whatever way its id is written: the gate matched ids as digits only, so a path the router still accepts (
/api/items/+1/picture,01,%201,1%20) went past the allowances and the operator pause. Every id segment of a spending route now matches any segment; the demo visitor's list still takes only plain digits, so an odd spelling there is refused. - A picture (Muse Image) is tried once and never retried on a timeout, so one click is never billed twice.
- Fields named for an IMSI, ICCID, MEID or service tag are kept out of every brief, the build pack and the picture prompts, like a serial number.
- The web header always shows its door on a gated host: Sign out when signed in, Sign in with the code otherwise. Access was read in one request with
/readyzand/api/version, so a slow start or any one of them failing left the header with neither link and nothing tried again;GET /api/accessis now its own request, retried (1 s, doubling up to 30 s) and asked again when the tab comes back into view, unknown reads as signed out, and signing in or out turns the link at once. On a phone the door sat at the end of the pages' sideways-scrolling strip, off screen; it now has its own line above the pages. A host without a code says Open server (no code needed) there. The sign-in dialog notes that for now signing in uses an access code only and proper accounts come later; the roadmap lists accounts and proper sign-in. ./dev -d(andbash -xover the deploy scripts) no longer prints values read from.envor secrets from the shell in its trace:env_get,env_loadandenv_set, the key check beforeup,gate, the live check's settings, the e2e access code,deploy.sh's.envreader and the hosted secret step run with tracing off and turn it back on after.scripts/tests/test_trace.shruns these paths traced against sentinel values that must appear nowhere in the output../dev deploy hosted backendand the deploy workflow restart the deployed app's running containers after the deploy and wait until they are gone before asking/readyz, so a changed Modal secret applies; a warm container (OPL_MIN_CONTAINERS=1) kept the secret it started with until it was stopped by hand. The workflow installsmodal1.6.3 or later.- The demo visitor no longer gets the owner's private text on a public item or capture:
GET /api/items/{id}answered a public item'slocationandnotesin full and the item dialog showed them. Wherever an item is answered to the demo visitor itslocationandnotesare now blank and its voice notes (values with the sourcevoice_note) are left out; a capture'snoteis blank and its clip and spoken note are reported as absent (clip,audiofalse, their sizes 0). This is done where the shapes are serialized, so every route the visitor reaches, and any added later, leaves them out; the owner, signed in with the code, still gets everything. An item served this way says so (redacted: true). The item dialog shows the visitor the details read-only, with no location or notes boxes and no Save details, and the same holds for any item that comes back redacted (a session that ended unnoticed). A save of the details now sends only the fields that were changed, so blanks the visitor was served are never written over the owner's values, even after a session opens in another tab. A capture read without a name or photos now names its item Capture instead of the first words of the phone's line, which stays in the notes. - Text typed in the item's details and not saved yet is kept when another part of the dialog changes the item (a decision, a confirmed value, a picture); only the fields you did not touch take the saved values.
- A value no longer carries an as printed line that only repeats it in another letter case, spacing or punctuation (a label's cULus read as CULUS); the line stays when the label says more, such as a unit or a prefix.
- Live calls no longer stop with
response incomplete: max_output_tokensbefore the answer: reasoning tokens count againstmax_output_tokenson the Responses API, and a label read at effortlowspent 798 of its 800 on reasoning. A caller'smax_output_tokensis now the answer budget alone; the client adds a headroom by effort (1,024minimal, 4,096low, 8,192medium, 16,384high, 32,768xhigh), underMODEL_MAX_OUTPUT_TOKENSwhen set (0, the default, is no ceiling). Tokens are billed as used, so the larger cap costs nothing unless the model uses it. Fixtures stay keyed on the answer budget, so recordings still match. A response that still stops at the cap says which operation, how many output tokens were used and how many of them were reasoning. - Price guidance no longer fails with
output did not match Findings: Invalid JSON ... at line 1 column 1: with the search tool the model may write a line before the search ("Tracking your ... down") and the answer in the message after it, and the client read every message as one text. A typed answer is now read from the final message alone, forrespond_structuredand the tool loop alike; a final message that carries prose ahead of the object is read from the object it ends with; one with no object raises naming the operation and the checks that failed, with the text cut short. - Price guidance is bounded to
PRICE_MAX_SEARCHESweb searches (4; 1 to 10), sent asmax_tool_callson the request and said in the instructions: left to itself a guidance ran eleven or twelve searches over 16 to 31 seconds, read 70,000 to 100,000 input tokens and cost 7 to 8 cents before the answer.
0.3.0 - 2026-10-31¶
Changed¶
- The logo is larger where it stands for the app: the Android icon is the viewfinder on its dark ground, filling the launcher shape (no white rim); the web header shows the same viewfinder crop at the height of the name and the tagline, while the browser tab keeps the whole logo.
scripts/app-icon.shrenders the icon PNGs and writes the header's mark fromdocs/assets/logo.svg. - Dialogs stand clear of the page: a deeper, layered shadow around the panel and a darker, more blurred see-through backdrop behind it, in light and dark mode; in dark mode a light hairline edge outlines the panel, and on a phone the backdrop blurs less.
.env.examplelists every name the hosted deploy and the planned second model route use (Modal, Cloudflare, the host's key, code, cap and mode; Baseten and Fireworks), with empty values and where to get each;./dev deploy hostedreads them from.envwhen the shell does not set them.- The docs site's tabs bar under the header is a mid gray in both schemes instead of the palette's light blue: all tab labels stay fully opaque white, with the active page in bold and an underline on hover or keyboard focus, so inactive tabs remain readable too; the logo in the header fills the header's full height.
- The docs site tells the story: how it fits together (one clickable diagram and two paths through the demo set), lessons learned, next steps (the roadmap folded into it) and the pitch.
- The README and the docs home show the phone setup as five numbered steps (install the app, open the web app, sign in, pair, send a photo) ahead of the long guide; the repeated lines in both intros are gone.
Added¶
- A phone paired by QR gets an access code of its own: the QR of a signed-in page carries a pairing, a session of its own kind good for
PAIR_TOKEN_MINUTES(15), and the app claims a code with it (POST /api/pair/code, once per pairing; a browser's session is refused, and each try counts against the address's code attempts), a record like any issued code, labeledphoneand part of the pairing's id, good forPAIR_CODE_DAYS(30) and never past the code that paired it, and one code pairs at mostPAIR_CODE_MAX(10) phones with current codes; the app then opens sessions with that code, with no second scan. Revoking a guest's code ends the phone codes it paired; a phone paired under the master code ends whenDEMO_ACCESS_CODEchanges;./dev keys revoke "phone …"ends that phone (schema v12 links a phone's record to what paired it). The pairing endpoints draw only an origin the server is reached at../dev keys listshows the phone codes; the app's Backend tab names the code by label and expiry, and Check says which code opens the door. The footer's pairing code shows for the demo visitor too, with the address alone, so a phone paired from an anonymous page is the demo visitor as well. - A QR code that opens the newest release's APK: each release attaches the APK a second time as
orphan-parts.apk, sohttps://github.com/nikolareljin/orphan-parts/releases/latest/download/orphan-parts.apkis a fixed address; the judges' guide, the README and the docs home show the code (docs/assets/apk-qr.svg) with the link; a release is marked the latest only once its APK is attached, and only when it is the highest version. ./dev gate on|off|show: the access code on your own machine, made when.envhas none, the backend restarted with the gate, a code for the phone or a guest issued and both printed to copy; never part of a deployment. The scripts read.envas Compose does: a#comment after a value, the text inside quotes, the last of a repeated key, Windows line endings.- A picture of what an item could become: What it could look like on the item dialog, under the photos, with a field for the use and Draw it; Muse Image draws it only on that click (
POST /api/items/{id}/picture), from the item's type, accepted values (never a private one such as a serial number, MAC or IMEI), condition and faults; kept beside the photos aspicture-<n>.jpgand never counted as one, served byGET /api/items/{id}/pictures/{n}, captioned as drawn and never evidence, charged in the ledger per image; the demo visitor sees the pictures of public items and is refused a new one. Off unlessPICTURE_ENABLEDis set; the stubimages-generations/picture.jsonanswers fixture mode. - Decide what it becomes: a panel on the item dialog, below the values and the details, with six rows (what it is, what it is worth, what it is good for, what would complete it, what it could look like, the decision) gathered from what the app already answers;
POST /api/items/{id}/useshas Muse Spark propose up to three uses from the item's accepted values, condition, faults, tests and the owner's preferences, beside the project templates the rules say the item fills, kept on the item (schema v11) and labeled as the model's proposals;GET /api/items/{id}/decideis the view;POST /api/items/{id}/decision(keep and build, sell, give away, not worth it, with a note) sets the intent and writesdecided: <choice>: <note>to the history, bumping the version only when the intent changes or a note is given; the view names the selected projects that count on the item (committed_to) and the panel says so before the buttons; a refused decision is said by the buttons and keeps the note; a decision sent with the itemversionit was made from is refused with 409 when the item changed meanwhile, so a stale tab cannot overwrite a newer choice; the panel reads the view again after every decision and after the Value panel writes a guidance, and names each committed project once. The picture row waits for its call. - The evaluation loop:
./dev eval(python -m app.eval run) loads the demo set as the demo does and scores it by the rules intoeval/(a report as JSON and Markdown, two charts): values read per item against anexpect:block indemo/set.yaml, fit verdicts between items, proposals written against kept, stub answers and spend;exportwritestrain.jsonlfrom the accepted reads,diffcompares two reports; the guide Training and improving the reads says where the rules and the prompts live and what fine-tuning Muse Spark Open would take.
Fixed¶
- The deploy workflow installs
python-dotenvnext tomodal:modal secret create --from-dotenvneeds it, and the backend job stopped at the secret step without it; the install lines for the by-hand path name it too. - The hosted backend serves no API docs: the secret step writes
API_DOCS=0, and/docs,/redocand/openapi.jsonanswer 404 on the host; on a machine of your own they stay on (API_DOCS, default1). - Generated pictures are decoded and stripped of metadata before storage; the decision panel links to the picture controls.
- The local gate checks labels before changing state; environment readers agree on duplicate values and quoted comments, and the setter refuses malformed assignments.
- Ideas keep the item version used for their brief when an edit happens during the request; decision rows fit a phone dialog.
- Material matching preserves uncertain claims, mixed wood or ceramic descriptions, and component or coating descriptions instead of approving the whole part as metal or plastic.
- The frappé maker's agitator in the demo set asks for a metal replacement: the material on the need side is the one wanted, the worn rod's plastic is a fault note, so a rod of the same dimensions that a listing calls all-metal or stainless steel passes and a plastic one is a conflict (a material is read as the one word metal or plastic, however it is spelled; a mix stays as written and does not pass).
- A material followed by punctuation or in parentheses ("Metal.", "(stainless)") reads as metal or plastic instead of a conflict, as do POM, nylon and "BPA-free plastic"; a negation ("non-metal", "no plastic", "plastic-free"), a part or a surface ("mostly metal", "chrome-plated") stays as written; the agitator's material question asks the seller whether the rod is metal throughout.
- A release merge whose tag the ruleset refuses now fails with the exact command an admin runs to tag the merge commit, in the run summary, instead of an opaque rule violation.
Security¶
- No credential or personal data ships in an image:
.envfiles and logs are kept out of both build contexts (and local data out of the backend's), and the release workflow scans each built image (scripts/scan-image.sh: its environment, the project's files, gitleaks) before anything is pushed. - A hosted deploy never ships a local secret file: the Modal image leaves out
.env*, key, certificate and keystore files and Python caches wherever they turn up under the three directories it carries, and the web step refuses to build while afrontend/.env*exists (Vite would bake itsVITE_names into the public bundle) and refuses to upload adist/holding one.
0.2.0 - 2026-10-30¶
Changed¶
- The docs site is complete: the demo walkthrough, find and rank, a new category by hand, the agent tools, the data model and the history pages written; the pages on what is not in this version (catalog feed, messaging, glasses, the other Muse models, provider routing) say so and point at the roadmap; the architecture and identification pages describe the service as it is.
- The README, the docs home, the roadmap, the limitations and the site description now say what the product is: a private inventory of what you own, four paths on it (catalog, sell and give away, find the one that fits, and build and reuse to follow), what is built and what comes next.
Added¶
- The public shelf: an item or a capture marked public (
POST /api/items/{id}/public, the switch on the item dialog) is shown to the demo visitor without a code, its values and photos and nothing else; the demo set loads as public, so anyone can look at it and try the readings while what a signed-in user saves stays private. Schema v10. - Five more project templates for a shelf of old computers: an arcade coffee table, a smart mirror, a home automation server, a home server with backups and a retro console on the TV; a profile for a blender or frappé maker agitator whose rules include the shaft, the coupling and the material (a metal replacement for a worn plastic one); the demo set gains the owner's shelf (laptops, RAM, tablets, monitors, mice, switches, a phone with a broken screen, the worn agitator and a search for its metal replacement).
scripts/scan-security.shruns gitleaks over the history and foxguard over the committed tree, never over uncommitted files. - Uploads without the camera: the Inbox's Send a capture from this computer form takes photos, one MP4 or 3GP video, a name and a line from a laptop, the same bundle the phone sends (
/api/versionnow tellsmax_clip_mb); on the phone, Photos from the gallery and A video from the gallery take media already there, checked by their bytes before they are sent. - The path for a first-time user:
JUDGES.md(the app from the release page on the phone itself, the hosted server with the access code, the local server as the alternative, what to do when a step fails) andscripts/phone-install.sh/scripts/phone-install.ps1, one command that installs the released app over USB with no development setup. - Two doors on one deployment (every session open before this change ends: what the token signs changed): the access code (the configured master code or a code issued as a record with
./dev keys issue|list|revoke, kept as a hash with a label and an expiry) opens the live door, the full inventory and the live models when the host has a key; without it a visitor is the demo: the pages to look at, the four stateless tries answered from the recordings by a fixture twin of the model client (nothing spent, nothing in the ledger), every write refused with one line;GET /api/accesssaysgrantedandlive; the web app keeps its pages open with Sign in with the code in the header; the phone says so on its Capture tab. - The item dialog shows the item's photos above the values to confirm, in view while the dialog scrolls, one click to enlarge: the capture's photos for an item read from the phone, the label photos for an item saved from the web form (
GET /api/items/{id}/photos,/photos/{n},POST /api/items/{id}/photos); the dialog says so when no photo was kept. - The app in the web app's look: the same colors in light and dark (the text on the accent readable in both, on the phone and in the web app), the header with the mark and the tabs, bordered panels, one scale of buttons, pills for status and a service line at the foot; a unit test keeps the palette equal to the web app's stylesheet. A pairing code that points at localhost (scanned from a browser open on the computer itself) is refused by the app with its own line, and the web app's footer says so instead of showing such a code; the Inbox says when a clip cannot be played here and offers the file; the browser tests delete the captures they send.
- Hosting on Cloudflare Pages and Modal:
deploy/modal_app.pyruns the backend on Modal as one container with the inventory, the captures and the ledger on a volume and the settings from one secret;./dev deploy modal; the web app on Pages calls it across origins with the session as a bearer header and the pairing code carries the backend's origin (api=); a key that cannot work at startup (refused, unpaid, forbidden, not found) puts the service on recorded answers and/readyzsays so (OPL_KEY_PROBE), while a busy API changes nothing; the web container reads back whatload_democommitted;SQLITE_JOURNAL_MODEfor the volume; the project templates are packaged with the backend. The deploy workflow (deploy.yml) puts the backend on Modal and the web app on Pages from the repository's secrets and variables, by hand or after a release tag, inert untilHOSTED_DEPLOYistrue;./dev deploy hostedruns the same steps from a shell. - Clearer reads: what goes to the model is contrast-stretched and sharpened a little (
EXTRACT_ENHANCE), the reading saysenhanced, and a label that scores underEXTRACT_BLUR_THRESHOLDon sharpness gets a warning to retake it closer and steadier; the photo kept is the one taken. - Pairing the phone:
GET /api/pair.svgand Pair the phone at the foot of every page show a QR code with the web origin and, when the demo is gated, a session for the phone, so the access code never leaves the server; the app's Scan to pair reads it../dev setup --androidinstalls a JDK and the Android command-line tools on Linux and macOS;./dev phonebuilds the app, installs it on the connected phone, launches it and prints the pairing code (LAN_ADDRESSnames the address when the first one found is not the right one); a phone that has not accepted the computer is said. The backend gains thesegnodependency for the code: a stack built before this is rebuilt with./dev up --build. - Captures from the phone:
POST /api/captureskeeps what the app sends in one go (photos re-encoded on arrival, a clip of at mostMAX_CLIP_MB, a WAV note, a name and a line) underDATA_DIR/captures/, privately, until deleted;POST /api/captures/{id}/processreads it into an item (the label found first, the fields observed, the note as the owner's words, the line as notes), records a failure on the capture and keeps the media for another try; the media served back; the Inbox page in the web app (thumbnails, the clip, the note, read, open the item, delete). The phone's Capture tab is a session (photos, a 30-second clip through the camera app, hold to talk, a line, Send) and a Captures tab lists what was sent and asks the backend to read it. - The demo set (
demo/set.yaml):./dev demo-scrubre-encodes the owner's photos without metadata,./dev record-fixturesloads the set once with a key and keeps every model answer as a fixture,./dev demoresets the inventory and loads the set in fixture mode, so every take and every visitor starts from the same screen and the public demo spends nothing;python -m app.demobehind them; the set loads with whatever files are present and says what it skipped. - Android app (
android/): the phone as the capture device for the same inventory. A backend page (URL and access code, kept in the app's private storage; a session opened with the code and again after a 401), a capture page (a photo through the system camera, re-encoded on the phone to pixels only, read byPOST /api/extract, saved withPOST /api/items), and a talk page (hold to talk, 16 kHz mono WAV toPOST /api/items/{id}/voice, the transcript kept as your words and read aloud)../dev android test|build|release|install|smoke, unit tests on the JVM, a smoke run on a phone or a headless emulator checked with stand-ins, theandroid.ymlchecks, the release APK attached to each release. - Build and reuse:
PUT /api/preferenceskeeps what you want from a build;GET /api/templateslists the projects kept as data (five ship);POST /api/projects/proposehas Muse Spark read the inventory and the templates through tools and propose projects, of which the rules keep only what names items you have, usable and of the right kind, saying what was dropped; projects move through proposed, shortlisted, selected (items committed and reserved, one selected build per item, a conflict answered with 409 and moved only withtake), building, done and abandoned;GET /api/projectscarries the budget line across the selected set;POST /api/projects/{id}/requirements/{key}/searchturns a missing requirement into a search request and.../fillfills it with an item of yours. The Build and reuse page. - Build pack and outcome:
POST /api/projects/{id}/packwrites a Markdown build pack by the rules from what stands (goal, items with accepted values and never a serial, location or note, what is still needed with its search and documented candidates, costs with dates, tools, steps with progress, safety, unknowns, guidance, limits, the outcome), versioned with a digest of its inputs and markedstalewhen an item, the project or the budget changed;GET /api/packs/{id}.mdas a file;POST /api/projects/{id}/progressticks a step;POST /api/projects/{id}/outcomerecords what was set up, how it was tested, the day, whether it works and the limits, moving a working project to done and keeping its items reserved;.../outcome/shareturns it on and gives a text for others with no names, ids, values, places or prices. On the Build and reuse page. - Demo gate: with
DEMO_ACCESS_CODEset, every page and API call needs a session opened with the code atPOST /api/access(a cookie, or the token as a bearer header), signed with a key derived from the code and stored nowhere; so many tries per address; every call that spends is counted per session and hour (DEMO_VISITOR_CALLS_PER_HOUR) and for everyone per day (DEMO_DAILY_CALLS), over which the answer is 429 withRetry-After; every request has a per-address limit;DEMO_PAUSED=1refuses what spends;/readyzreports the gate underdemo; behind nginx the forwarded address counts whenDEMO_TRUST_PROXY=1. The web app asks for the code and again on any 401. - Missing items:
POST /api/searchesrecords what an item needs (or values written in under a profile) with a top price, currency and radius, and hands off a Marketplace search for the owner to run;POST /api/searches/{id}/candidatestakes a listing brought back as text (Muse Spark reads it into fields, with the profile's rule keys), as a screenshot (read by the extractor) or as typed values, dates it, assesses it by the profile's rules and names its state from the ladder; recheck, withdraw, andGET /api/candidates/{id}/questions, the rules' questions as a message to the seller. Nothing is fetched or posted; a link is kept as given. The Find a part page. - Releases by hand and in full:
VERSIONis the one place the version is read from, and./dev versionwrites it with the manifests;./dev releasetagsVERSIONon a clean, levelmainand pushes the tag, which only the repository admin may do; the release workflow assembles the release notes from the changelog section, the pull requests merged since the previous release, the compare link and the images, and carries two jobs that stay inert until there is something for them: a release APK when an Android app is in the tree, and a deploy to the host when the repository variableDEPLOY_ENABLEDand the host secrets are set;./dev deploydoes the same from a machine. - Price guidance:
POST /api/items/{id}/pricehas Muse Spark search the web (search grounding) for listings and sales of the same model and keeps only comparables a search result backs; four figures never merged (asking, sold, what you reported, the model's estimate) each with count, range, typical, source and date; three labelled strategies; weak evidence called weak; whole against parts from the parts' own guidance;POST /api/items/{id}/price/reportsfor a sale you know of; searches counted into the spend ledger. The What is it worth? panel on each item. - Sell or give away:
POST /api/items/{id}/draftswrites a listing draft with Muse Spark from the item's accepted values; the rules append the faults, what was tested, what is included and the price or free pickup, and drop any line that rests on nothing confirmed; a checklist before handover (preparation, photos, data removal when the item holds data) kept per draft; drafts versioned and marked stale when the item changes; copy the draft and open the Marketplace create page.POST /api/items/{id}/voicetranscribes a WAV note with Muse Voice Transcribe and keeps it as the owner's words. The Prepare to sell or give away panel on each item.
0.1.0 - 2026-10-29¶
Added¶
- Release on merge: a pull request from
release/x.y.zmerged intomaintags the merge commit and publishes the images and the GitHub release, after the version is checked against every manifest and the changelog; an existing tag is never moved. - Assessment:
POST /api/assesschecks a candidate against what is needed, rule by rule from the profile (GET /api/profiles/{id}/rules): pass, fail or unknown, never a guess; measured values carry their uncertainty into the check; one verdict from the ladder (EXACT_MATCH,COMPATIBLE_LIKELY,VISUALLY_SIMILAR,CONFLICT,INSUFFICIENT_INFO); a question for every unknown on the candidate's side. The Does it fit? panel on each item. - Measurement:
POST /api/measureandPOST /api/items/{id}/measurementscompute a dimension from marks on a photo with a scale reference (two points of known length, or four corners of a known rectangle through a homography), refuse captures that are blurred, short on reference, off the photo, too small or not straight on, and report the uncertainty with its basis; profile features with tolerances inGET /api/profiles; accepted captures are proposed on the item as observed values. The marking canvas under each item. - Docs site: every page carries share metadata (title, description, one card image), so a link pasted into a chat or a post shows the project card.
- Private inventory:
POST /api/itemssaves a reading as an item whose observed values are confirmed or rejected one by one; owner values sit beside observations with their provenance; condition, faults, tests, accessories, location, notes, intent and availability; parts separated into their own records; versions, optimistic writes and a per-item history; SQLite underDATA_DIR. The My inventory page and Save privately in the web app. - Segmentation with SAM 3.1:
POST /api/segmentfinds the item, the label and the connector (one concept per call, more on request), decodes the masks with Meta's parser, and returns one region per match with a box, a confidence, a crop and the mask outline, plus the overlay;python -m app.segment; recorded fixtures per concept.POST /api/extracttakes label crops and sources their evidencelabel_crop; the web form finds the label first by default and shows the overlay and the crops. ./dev upstarts in fixture mode when.envhas noMODEL_API_KEY, and shows the backend log when the stack fails to start.- Web: upload up to four photos and read the evidence table (value, unit, confidence, source, text as printed), warnings and the label transcription.
GET /api/versionreports the upload limits and the form follows them, refusing an oversized photo before it is sent. Playwright end-to-end checks against the dev stack in fixture mode,./dev e2e, and ane2eCI job. - Label extraction with Muse Spark:
POST /api/extractreads up to four photos into typed fields with a confidence, a source and the raw text per field; deterministic normalisers for volts, amps, watts, codes, connectors, polarity and lists; photos re-encoded without metadata;python -m app.extract; a recorded fixture so tests and the offline demo need no key. - Model API client: bearer auth, timeouts, retries with backoff for 429/500/503 honouring
Retry-After, strict structured output, a spend cap with a persistent cost ledger priced from the published rates, fixture and record modes,GET /api/spend,./dev check-api. - Backend service skeleton: settings validated at startup,
/healthz,/readyz,/api/version, JSON logs with request IDs. - Frontend shell that shows API status.
./devcommand runner with a small bash helper library, Docker dev and production stacks.- CI on Linux and macOS, docs site skeleton published to GitHub Pages.